美文网首页
手游联运聚合SDK——Application继承关系自动适应

手游联运聚合SDK——Application继承关系自动适应

作者: U8SDK | 来源:发表于2020-09-24 20:04 被阅读0次

    熟悉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的接入完全和游戏工程解耦,我们采用了反编译母包,动态合并资源(包括代码)的方式进行渠道打包。 对于上面Application的业务情形,我们采用了代理的方案【具体方案参考这篇文档或者博客】。

    但是这种方案, 并没有完全解决上面的问题。如果SDK要求继承XXXApplication, 同时SDK代码中有对XXXApplication做强制类型转换的话, 那么显然这种方式是无效的。强制转换的代码类似如下:

    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。

    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的自定义脚本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_ext.png

    通过上图可以看到, 要想让上面三个部分的application变成单继承,我们需要想办法,修改这些Application类的继承关系。 上面说了, 如果能够要求渠道或者SDK提供方修改实现方式可行的话,这里就没有讨论的必要了,所以,我们还是另寻他路。

    了解过U8SDK打包工具原理的同学应该知道, 母包apk、渠道sdk以及插件的jar包,在打包的过程中, 会生成为对应的smali文件(smali文件是啥,可以自行百度google了解)。那么, 如果我们想让这些Application类改祖认宗,我们可能的方式,就是在这里进行操作了。

    所以,接下来,我们的思路很简单: 打包的时候, 合并渠道和插件代码是一步步执行的。 执行顺序是, 先合并插件, 再合并渠道。渠道和插件的自定义脚本执行也是这个顺序。 所以, 我们只需要在各自的自定义脚本类中, 将当前AndroidManifest.xml中第一级Application(就是他直接继承了android系统的Application类)继承的父类改为这个SDK要求的XXXApplication即可。如下图示意:


    application_change.png

    这样打包工具顺序执行下来, 最终每个SDK的Application都会合并到最终的Application继承树里面。 所以,剩下的问题, 就是怎么写逻辑了。 逻辑还是比较简单, 先解析AndroidManifest.xml中当前的Application,找到他的第一级父类, 然后定位该父类的smali文件, 然后将smali文件中该类继承的Application改为我们指定的SDK的Application类。 我们直接定义一个辅助类application_helper.py, 直接上python代码:

    #从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类的继承:

    application_helper.modifyRootApplicationExtends(decompileDir, 'com.xxx.sdk.XXXApplication')
    

    好了,有兴趣的同学可以试验一下。

    相关文章

      网友评论

          本文标题:手游联运聚合SDK——Application继承关系自动适应

          本文链接:https://www.haomeiwen.com/subject/oyytuktx.html