U8SDK——Application继承关系的终极解决方案
熟悉Android应用开发的同学应该知道, 一般每个Android应用程序都有一个Application类, 整个app进程里面只有该类的一个实例存在。同时,该类的生命周期函数onCreate等函数的执行要早于Activity组件的。那么,很多渠道SDK或者插件开发者,在设计开发SDK的时候,经常会定义一个Application的子类,然后在生命周期函数onCreate中做一些初始化的工作,还有因为该类是单进程中唯一实例, 也会将一些全局的变量放在这里。
所以,我们在接渠道SDK或者插件的时候, 经常遇到需要定义一个Application来继承他们的某个XXXApplication。 这样看, 似乎设计合理,没啥毛病。 但是,实际情况,我们还需要结合当前国内游戏的现状来看。
国内渠道平台保守估计几百家,算上其他功能插件(比如统计、分享、推送,热更等), 估计上千家不在话下。 那么一般情况下,对于单款游戏来说, 可能接入的SDK数量,在几十家左右。这几十家的SDK中, 有上面这个要求的,可能在10家左右。但是, Java语言中, 我们知道类是单继承的, 也就是如果M渠道要求你定义一个Application继承MApplication, N渠道要求你定义一个Application继承NApplication。 那么作为游戏研发或者渠道SDK接入同学, 你如何来解决这样的需求悖论呢? 以前,我们自己直接在游戏中接入每个渠道, 可能会将游戏母工程独立出来, 然后每个渠道一个接入工程,并引用游戏母工程,然后在每个渠道接入工程里面定义该Application,并做一些其他配置,然后每个渠道再单独打包。 但是,这样仅仅解决了上面不同渠道的这个需求。 很多时候, 游戏除了接入渠道, 还同时需要接入统计、推送、分享等插件SDK。 那如果这些插件SDK中也有类似需求的话, 那只有两条路可以走了。 要么让其中一方修改SDK, 要么就是放弃其中一个SDK,另寻其他。
而在U8SDK框架中, 为了将渠道SDK和插件SDK的接入完全和游戏工程分开,达到渠道SDK接入的维护性和复用,不用每个游戏再去接入这些SDK,我们采用了反编译母包,动态合并资源的方式进行渠道打包。 对于上面Application的业务情形,我们采用了代理的方案【具体方案参考这篇文档或者博客】。
但是这种方案, 并没有完全解决上面的问题。如果SDK要求继承XXXApplication, 同时SDK代码中有对XXXApplication做强制类型转换的话, 那么显然这种方式是无效的。强制转换的代码类似如下:
1 2 3 |
XXXApplication app = (XXXApplication)activity.getApplication(); app..... |
因为通过上面u8的默认方案, 最终activity.getApplication()获取到的是U8Application,U8Application是没有继承XXXApplication的, 配置的proxyApplication是在U8Application中被调用的。 所以SDK中有直接的强制类型转换的话,这种做法就行不通了。
那么对于上面这种情况, 我们又引用了第二种方案, 想必同学看到上面的情况, 已经有了这个想法, 那我让U8Application直接继承这个XXXApplication不就可以了吗? 道理是这样, 但是U8Application是抽象层中的类, 抽象层是不会和任何渠道代码有耦合的。 所以, 这种方式不通, 但是思路是对的, 只是我们换一种处理方式。
我们定义一个XXXProxyApplication继承XXXApplication(注意:这个时候不再实现IApplicaitonListener接口了,而是直接继承渠道的XXXApplication)。 但是这样的话, 那U8Application怎么和XXXProxyApplication共存呢, 这个就是我们在XXXProxyApplication的生命周期函数中, 调用U8Application中调用的api。 然后通过该渠道的打包自定义脚本, 将AndroidManifest.xml中application节点的android:name设置为XXXProxyApplication。这种方式相当于让U8Application继承了XXXApplication。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
package com.u8.sdk; public class XXXProxyApplication extends XXXApplication{ public void onCreate(){ super.onCreate(); U8SDK.getInstance().onAppCreate(this); } /** * 注意:这个attachBaseContext方法是在onCreate方法之前调用的 */ public void attachBaseContext(Context base){ super.attachBaseContext(base); U8SDK.getInstance().onAppAttachBaseContext(this, base); } public void onConfigurationChanged(Configuration newConfig){ super.onConfigurationChanged(newConfig); U8SDK.getInstance().onAppConfigurationChanged(this, newConfig); } public void onTerminate(){ super.onTerminate(); U8SDK.getInstance().onTerminate(); } } 然后打包工具自定义脚本sdk_script.py中, 我们将最终AndroidManifest.xml中application设置为XXXProxyApplication: def execute(channel, decompileDir, packageName): manifestFile = decompileDir + "/AndroidManifest.xml" manifestFile = file_utils.getFullPath(manifestFile) ET.register_namespace('android', androidNS) key = '{' + androidNS + '}name' tree = ET.parse(manifestFile) root = tree.getroot() applicationNode = root.find('application') if applicationNode is None: return 1 applicationNode.set(key, 'com.u8.sdk.XXXProxyApplication') tree.write(manifestFile, 'UTF-8') |
这两种方式组合起来基本可以应付80%的情形了。 但是上面分析到的几个点,依然无法解决。 比如如果同时接了第三方统计插件也要求继承他们的MMMApplication, 再比如游戏母包里面, 有自己的Application,他们是直接继承了U8Application, 那么我直接通过自定义脚本将application换成XXXProxyApplication,会导致游戏自己的Application失效。
那到底有没有十全十美的解决方案来兼容上面所说的所有情况呢?十全十美的方案?
仔细想想,天无绝人之路: 其实, 不管我同时打进去多少SDK, 不管里面有多少个MMApplication还是PApplication, 我只要修改他们的继承关系,让他们改祖认宗即可。如下图:
通过上图可以看到, 要想让上面三个部分的application变成单继承,我们需要想办法,修改这些Application类的继承关系。 上面说了, 如果能够要求渠道或者SDK提供方修改实现方式可行的话,这里就没有讨论的必要了,所以,我们还是另寻他路。
了解过U8SDK打包工具原理的同学应该知道, 母包apk、渠道sdk以及插件的jar包,在打包的过程中, 会生成为对应的smali文件(smali文件是啥,可以自行百度google了解)。那么, 如果我们想让这些Application类改祖认宗,我们可能的方式,就是在这里进行操作了。
所以,接下来,我们的思路很简单: 打包的时候, 合并渠道和插件代码是一步步执行的。 执行顺序是, 先合并插件, 再合并渠道。渠道和插件的自定义脚本执行也是这个顺序。 所以, 我们只需要在各自的自定义脚本类中, 将当前AndroidManifest.xml中第一级Application(就是他直接继承了android系统的Application类)继承的父类改为这个SDK要求的XXXApplication即可。如下图示意:
这样打包工具顺序执行下来, 最终每个SDK的Application都会合并到最终的Application继承树里面。 所以,剩下的问题, 就是怎么写逻辑了。 逻辑还是比较简单, 先解析AndroidManifest.xml中当前的Application,找到他的第一级父类, 然后定位该父类的smali文件, 然后将smali文件中该类继承的Application改为我们指定的SDK的Application类。 我们直接定义一个辅助类application_helper.py, 直接上python代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
#从smali文件中,定位父类 def getSuperClassNameInSmali(decompileDir, smaliPath): f = open(smaliPath, 'r') lines = f.readlines() f.close() for line in lines: if line.strip().startswith('.super'): line = line[6:].strip() return line[1:-1].replace('/', '.') return None #查找指定类的smali文件路径 def findSmaliPathOfClass(decompileDir, className): log_utils.debug("findSmaliPathOfClass:%s", className) className = className.replace(".", "/") for i in range(1,10): smaliPath = "smali" if i > 1: smaliPath = smaliPath + str(i) path = decompileDir + "/" + smaliPath + "/" + className + ".smali" log_utils.debug(path) if os.path.exists(path): return path return None #查找当前AndroidManifest.xml中的application类 def findApplicationClass(decompileDir): manifestFile = decompileDir + "/AndroidManifest.xml" manifestFile = file_utils.getFullPath(manifestFile) ET.register_namespace('android', androidNS) key = '{' + androidNS + '}name' tree = ET.parse(manifestFile) root = tree.getroot() applicationNode = root.find('application') if applicationNode is None: return None applicationClassName = applicationNode.get(key) return applicationClassName #查找AndroidManfiest.xml中application类的一级父类 def findRootApplicationSmali(decompileDir): applicationClassName = findApplicationClass(decompileDir) if applicationClassName is None: log_utils.debug("findRootApplicationSmali: applicationClassName:%s", applicationClassName) return None return findRootApplicationRecursively(decompileDir, applicationClassName) #循环定位 def findRootApplicationRecursively(decompileDir, applicationClassName): smaliPath = findSmaliPathOfClass(decompileDir, applicationClassName) if smaliPath is None or not os.path.exists(smaliPath): log_utils.debug("smaliPath not exists or get failed.%s", smaliPath) return None superClass = getSuperClassNameInSmali(decompileDir, smaliPath) if superClass is None: return None if superClass == 'android.app.Application': return smaliPath else: return findRootApplicationRecursively(decompileDir, superClass) #主调用接口, 设置一级Application继承指定的applicationClassName def modifyRootApplicationExtends(decompileDir, applicationClassName): applicationSmali = findRootApplicationSmali(decompileDir) if applicationSmali is None: log_utils.error("the applicationSmali get failed.") return log_utils.debug("modifyRootApplicationExtends: root application smali:%s", applicationSmali) modifyApplicationExtends(decompileDir, applicationSmali, applicationClassName) #将一级Application的父类Application改为继承指定的applicationClassName def modifyApplicationExtends(decompileDir, applicationSmaliPath, applicationClassName): log_utils.debug("modify Application extends %s; %s", applicationSmaliPath, applicationClassName) applicationClassName = applicationClassName.replace(".", "/") f = open(applicationSmaliPath, 'r') lines = f.readlines() f.close() result = "" for line in lines: if line.strip().startswith('.super'): result = result + '\n' + '.super L'+applicationClassName+';\n' elif line.strip().startswith('invoke-direct') and 'android/app/Application;-><init>' in line: result = result + '\n' + ' invoke-direct {p0}, L'+applicationClassName+';-><init>()V' elif line.strip().startswith('invoke-super'): if 'attachBaseContext' in line: result = result + '\n' + ' invoke-super {p0, p1}, L'+applicationClassName+';->attachBaseContext(Landroid/content/Context;)V' elif 'onConfigurationChanged' in line: result = result + '\n' + ' invoke-super {p0, p1}, L'+applicationClassName+';->onConfigurationChanged(Landroid/content/res/Configuration;)V' elif 'onCreate' in line: result = result + '\n' + ' invoke-super {p0}, L'+applicationClassName+';->onCreate()V' elif 'onTerminate' in line: result = result + '\n' + ' invoke-super {p0}, L'+applicationClassName+';->onTerminate()V' else: result = result + line else: result = result + line f = open(applicationSmaliPath, 'w') f.write(result) f.close() return 0 |
这样, 在有application需求的渠道SDK或者插件SDK中,我们不再需要像上面一样写ProxyApplication也无需写一个类去继承XXXApplication了, 直接在渠道或者插件的自定义脚本中, 这样调用即可完成application类的继承:
1 2 |
application_helper.modifyRootApplicationExtends(decompileDir, 'com.xxx.sdk.XXXApplication') |
好了,有兴趣的同学可以试验一下, 目前不排除上面代码有BUG或者没有想到的点, 欢迎指正并加入U8SDK技术交流群:207609068
本文出自 U8SDK技术博客,转载时请注明出处及相应链接。
本文永久链接: http://www.uustory.com/?p=2195