当前位置: 首页 > 科技观察

Android多渠道打包实践通道分发

时间:2023-03-20 02:05:05 科技观察

01前言多渠道打包应该每个Android开发者都不陌生,从最早的Eclipse上的纯手工打包,到Ant脚本打包,再到现在AndroidStudio自带的Channel配置,和批量打包的gradle脚本。多渠道打包方案不断优化,打包速度从原来几十个渠道一天打包到现在只需几个小时。但是上述方案只是替换了配置文件中的频道信息。如果没有源码,就只有一个apk文件,每个包里的模块和代码都要根据不同渠道定制。有什么解决办法吗?02游戏渠道发布打包目前国内安卓市场渠道众多,包括华为、小米、vivo、oppo等硬核厂商,拥有自己的操作系统或硬件设备,建立了非常庞大的用户群体基于他们自己的移动设备。还有用用宝、久游等,虽然没有自己的移动设备,但凭借着APP的广大受众,积累了大量的用户。在这些渠道发布游戏,需要接入这些渠道不同的SDK,实现渠道的登录和支付能力,不像一般的APP,只需要更改channelId即可。一款游戏接入单个频道可能需要一周或更长时间。如果要同时接入几十个频道,对于游戏开发来说会花费非常大的时间成本。如何解决?打包一个游戏包通常需要几个小时。如果每个频道单独打包,完成几十个频道的打包需要几十上百个小时。再加上后期各个渠道SDK的迭代维护,成本可想而知。为此,我们需要为游戏开发提供一个低成本的打包方案。与传统APP不同,自研产品在编译时可以配置各种脚本,实现多渠道打包。在游戏渠道分发中,发行商不是游戏的开发商(以下简称CP),所以我们只能拿到CP提供的apk(以下简称母包),需要自定义和以母包为基础整合各种渠道。包括为每个渠道集成不同的SDK及其认证、登录、支付等能力,最后为每个渠道创建不同的渠道分包。03目标规划针对以上痛点,我们不妨先定一个小目标:游戏只对接一个sdk,游戏只打包一次。那么要完成这个“小目标”,我们需要解决两个问题:1.整合渠道,2.整合打包3.1整合渠道这里的整合渠道并不是说把所有的渠道都连接起来,放到一个大的sdk中。然后根据channelId调用不同channel的方法,既不优化也不可维护。所谓集成,其实就是通过一个统一的export来封装channel。在我们常规的app开发中,也会有通过不同的flavors或者buildTypes来动态加载依赖的场景。那么我们只需要将每个通道视为一个单独的模块即可。但是由于我们是合作伙伴,只能在游戏结束后才能拿到apk包。我们无法在游戏代码中将频道作为一个模块进行集成,只能作为一个单独的apk进行集成,将sdk集成在每个独立的频道apk中。能力,然后通过统一封装的代理层实现这些接口的对外暴露。具体架构如下:从上图可以看出整体的业务流程是:游戏调用代理的代理接口→代理接口调用具体的集成通道api,这样无论底层如何通道变化,只要代理层的接口设计能够覆盖通道的所有能力,那么通道的变化对于上层游戏来说是察觉不到的,从而实现游戏和通道的完全解耦。渠道整合也已实现。3.2集成打包一个游戏的打包通常需要几个小时。如果每个通道都打包一次,会花费很多时间。但是如果设计成上面的架构,游戏只需要连接一次agentSDK,之后我们只需要对通道进行封装,替换通道模块的代码和资源即可。04技术实现既然目标已经确定,那么就需要具体的封装方案来实现。传统的渠道打包方式无法满足需求,游戏发行需要独特的多渠道打包方式。梳理一下需求:我们有一个apk,还有一个channelsdk代码,我们需要将这些代码合并到apk中,生成一个新的apk,这个过程是不是很熟悉?这不就是反编译再编译的过程吗?Google官方的apktool提供了反编译、重编译等能力。基于它,我们可以设计一个大概的流程:整个流程中的大部分工作都可以通过调用apktoolAPI来完成,但是如何替换注入到通道sdk中的代码呢?熟悉逆向工程的同学一定知道,通过apktool反编译后生成的smali文件大概长这样:别说修改了,这种类汇编代码的可读性很差。换句话说,如果我们是编辑java文件,岂不是方便很多?按照这个思路,如果我们有一个demo.apk集成了channelsdk,然后给CP提供一个代理sdk,通过apktool反编译demo.apk后生成的smali文件替换父包反编译后对应的代理类。这样就可以实现通道码的注入。同样,我们只需要为每个频道开发一个接入demo.apk,就可以在所有游戏中复用。这样既实现了渠道的独立,又实现了横向扩展。因此,我们设计了一套代理层,将登录、支付、鉴权等基础能力暴露给API,并在内部调用通道API。demo.apk就是基于这段代码打包的。其次,渠道之间还有很多差异化的内容需要处理。最简单的就是同一款游戏不同渠道的包名不同。这里使用反编译后的yml文件。该文件记录了反编译的配置信息,用于重新编译时读取。以修改包名为例。只需添加第一行的配置即可更改包名后的重新编译。同时yml文件还可以自定义很多配置,这里就不展开了,有兴趣的同学可以自行了解。最后,在解决了代码合并和渠道区分的配置后,整个打包过程大致有以下几个步骤:1.准备游戏主包和对应的渠道demo.apk2.通过apktooldxxx命令分别反编译两个apk,得到如下文件结构3.合并AndroidManifest.xml,合并assets中的文件,合并lib,合并替换res中对应的资源和配置文件,替换smali中的相关文件4.通过apktoolbxxx命令编译apk5。签名工具对编译后的apk进行签名4.1脚本打包虽然打包步骤是简单的5步,但是第3步的资源整合和替换非常繁琐。AndroidManifest合并:1.使用xml解析器获取所有节点2.合并相同节点3.添加频道特定逻辑4.添加游戏特定逻辑5.替换包名相关节点(provider,permission等)6.新建Manifestassets合并:1.合并assets文件夹2.添加频道特定逻辑3.Splash资源替换4.生成新的assets文件夹。合并相同的libs4。根据游戏支持的cpu复制对应的libs5。生成一个新的libs文件夹。文件夹的合并,以及值的合并;除了values之外的其他文件的合并,和assets的合并类似,替换同一个文件,合并其他文件,生成新的文件夹;values文件夹的合并,需要通过逐一分析文件内容进行合并;最后,我们还需要添加频道的特殊逻辑,生成新的res文件夹。smali合并:首先找到对应的代理层文件夹,将demo.apk文件替换到父包中的对应位置;需要注意的是,很多渠道的sdk比较大,方法数可能会超过65535的限制。在合并的时候,我们通过脚本统计每个smali文件夹中类的方法数。当累计方法数超过阈值时,将新建一个smali_classes2文件夹,并将后续类迁移到后续文件夹中。如果纯手工处理这些操作,不仅耗时而且容易出错。在整理好合并替换规则后,我们实现了一个打包工具来帮助我们处理这些繁琐的工作。以下是资源替换的工具:至此,我们可以将繁琐的手动打包过程转化为简单的脚本命令,既节省时间又保证准确性。4.2工具打包虽然脚本打包很方便,但实际上由于每个同学的电脑环境不同,同一个脚本在不同电脑上运行的结果也会不同,不同环境的报错也会有一定的差异所以我们需要一个相对统一稳定的环境来执行打包任务,可以使用传统的持续集成工具:jenkins所以我们基于打包脚本和jenkins部署了一套高可用的游戏渠道发布打包工具,减少降低了打包的门槛和工作量,进一步提高了打包效率。4.3不管是平台打包脚本还是jenkins,其实都是比较偏向于开发的工具。但是打包不仅仅是为了开发,更重要的是打包后交付给测试方和业务方。如果有非开发也可以使用,更直观、门槛更低、更产品化的解决方案是否有助于提升工作流效率。为此,我们还设计开发了UO打包平台,致力于让商科学生也能轻松导出渠道包。05避坑建议其实整个研发项目并没有上面说的那么顺利,遇到了很多奇怪的问题。这里有几个比较典型的分享给大家:5.1合并游戏主包和频道demo是单dex方式数量超过64K。一些渠道的SDK有很多方法。这时候合并成一个dex文件,方法数可能会超过65536个。其实相信很多Android开发者都遇到过方法数过多的问题。现在我们只需要配置multiDexEnabletrue就可以在编译时自动划分dex包。但是对于游戏来说,我们得到的是编译好的apk,所以没有编译工具会为我们分包dex1和dex2。我们需要通过配置模拟编译工具的分包逻辑,实现手动分包。5.2加固对发包过程的影响部分游戏接入加固平台,会导致生成的游戏频道包合并父包和频道demo.apk后崩溃。在这种情况下,我们需要改变外包流程。流程由原来的:加固后父包->生成未签名通道包->签名->获取通道包,但启动会闪退,将加固动作倒退为:未加固父包->生成未签名通道包->加固->签名->获取可用频道包5.3资源文件由于第二个包aapt会重新生成R.smali文件,所以会出现两个问题:1.R文件的路径发生变化:由于游戏游戏包名不同,最终渠道包的R文件路径也不同。如果直接通过R.id.xxx调用游戏,这里的R指的是游戏原包名,R文件本质上是一个类,如果路径发生变化,我们代码中对R文件的引用就会找不到班级。有两种方法可以解决这个问题,修改所有R文件引用,更改为新的包名,或者在旧包命名路径下复制一个R文件。大多数游戏的原生代码并不多,极少数会通过R.id.xxx获取资源。对于这类游戏,在合并smali文件的时候,我们会根据配置判断是否使用原来的smali文件。R文件保存在包名路径下。同时,为了防止游戏的R文件id发生变化,我们将游戏的R文件复制到新包名目录下,保证游戏的资源id不变。对于我们sdk自己的id,为了不让sdk的resourceid和游戏冲突,我们的sdk的resourceid会通过aapt重新生成。所以我们的sdk无法通过R.id.xxx获取资源id。我们调用Context.getResources().getIdentifier()方法通过包名+资源名获取资源id,避免了二次打包后id变化导致的crash。同样,虽然大部分渠道SDK也会调用getIdentifier获取资源id,但也有部分渠道直接使用R.id.xxx获取资源id,会导致崩溃。这种问题其实和游戏的R文件一样,只要在原包名下有对应的R文件,避免引用错误,就可以解决。2、资源id的变化:由于新增资源,资源id会发生变化。同时,母包和演示包都会有一些共同的基础组件。同一个资源的id在两个包中可能不一样。众所周知,打包后会生成resource.arsc文件。该文件是一个资源索引文件。解包后apktool会根据resource.arsc文件生成public.xml。这个文件包含了资源id的值,所以我们需要统一id,需要用public.xml中的值覆盖demo中原来的值。大致流程如下:我们解析游戏和sdk的public.xml文件,然后使用sdk中的资源与游戏进行对比:1.如果sdk中没有游戏中的资源,保存resourcesinthenewaddedresourcecollectionA.2.如果sdk中有游戏中相同的资源,我们会保留游戏中的id,记录sdk的新旧id映射保存在集合中B.3。合并时会先读取集合A中的id,判断是否有冲突。如果与已有的id冲突,会通过一定的规则重新生成id,并将新旧id的映射保存到集合B中4.读取集合B,搜索旧id的所有值,并替换他们使用新的id5。生成新公众的一般方法。处理完成。5.4Provider和permission是android的四大组件。ContentProvider大家都很熟悉了。使用时需要在manifest中声明,如下:authorities是唯一标识,渠道sdk中ContentProvider的权限会使用包名+类名来声明,对于集成的demo.apkchannelsdk,所有ContentProvider的权限都是一样的,如果一个频道有多个游戏,会导致安装第二个游戏时权限冲突导致失败,所以我们首先需要将包名字段替换成specialplaceholder,然后在manifest合并的时候识别占位符,替换为通道分包的包名。当然,如果能这么好办,就不会出现在“避坑建议”里了。其实很多渠道的ContentProvider声明都是放在自己SDK的manifest中的。对于这样的渠道,我们需要集成渠道SDK,项目中的manifest需要替换权限,所以需要使用replace。这样在打包通道demo的时候,在manifest的合并过程中,通道SDK里面的权限会变成我们自定义的权限。最后,同样的问题也出现在权限上。一些渠道SDK声明自定义权限以限制对其自身服务或组件的调用。这部分问题的处理方法和provider类似,这里不再赘述。06优化对比6.1整个打包平台方案的优化过程,解决了以往打包过程中的耗时点:1.包体多传输打包平台,将原来的上传→下载→上传三个传输过程简化为单个上传。2.人工响应时间工作时间响应时间比较稳定,但遇到紧急情况,非工作时间(午夜、节假日)可能会有打包需求。此时,由于各种原因,技术可能无法及时派送包裹,这种响应问题可以通过平台外包来避免。3.加固过程现在加固基本上是每个APP的必经过程。有的游戏加固是每个频道包在签名前加固一次。但是,目前我们使用第三方解决方案进行强化。基于这种情况,会打断我们的打包过程,需要第三方在签收前加固,然后重新签收,这样也会造成多次游戏包上传下载操作,严重影响包输出效率,以及也增加了很多人工操作的工作量。为了进一步提升包装体验和效率,我们整合了一些加固解决方案,让加固成为包装过程的一部分。流程对比如下:从上图可以看出,通过平台整合加固流程后,每个包减少了额外的6次传输和一系列人工操作,大大提升了打包时间和体验。6.2耗时比较游戏包与其他应用不同。单局1-2G,单次传输3-5分钟。如果是外网环境,速度会慢一些,优化了两次传输。处理后,整个包裹派送流程每个包裹提升了6-10分钟,每场比赛平均有8-10个接入通道,相当于每个包裹派送的耗时缩短一小时。对于加固包,通常整个过程可能需要1-2小时,由于上传、加固等多方响应时间可能会更长,但只需要十几分钟就可以完成所有渠道的外包通过系统。我们用RebornCell做了一个测试,不到20分钟就完成了8个通道的打包、加固和签名。硬化过程是最耗时的部分。可以看到bangGream的7个不需要加固的通道只用了不到20分钟。3分钟。07小结这篇文章看似简单的过程。实际上,由于各个渠道的逻辑差异和底层依赖库的冲突,对ProxySDK的高内聚低耦合的设计要求还是比较高的。在开发过程中,已经采取了一些修改和步骤。最后,我终于交上了阶段性的答卷。其次apktool本身的问题也给我们带来了很大的困扰。在尝试了不同的版本,以及各种配置修改和试错之后,我们最终确定了一个满足我们需求的稳定版本。未来,我们还将在平台化方向进行探索,力争推出高可用的多渠道游戏打包平台。