Android高级之十三讲-组件化、插件化、热修复

   本文来自刘兆贤的博客_CSDN博客-Java高级,Android旅行,Android基础领域博主 ,引用必须注明出处!

组件化:

对功能进行拆分,独立开发,打成一个包发布。

静态架构模式:

通过公用依赖项目,使用广播或公共接口,进行子项目联动。

动态架构模式:

在主项目AndroidManifest.xml中,注册一个占位StubActivity;在子项目里,注册PluginActivity,将其传入StubActivity里进行加载;子项目通常打成apk包放入asset文件夹方便调用,解析后的dex文件使用自定义ClassLoader加载,其他资源通过反射获得AssetManager来构建的Resource(new Resources(mAstMgr,displayMetrics,configuration)对象来加载(如果直接使用StubActivty里的Resources对象,容易造成内存泄露)。

怎么避免独立的功能,依赖公共包造成重复的问题,使用implementation而非api,从而防止包传递。

怎么把StubActivity和PluginActivity联系起来?

方法一:回调生命周期,后者跟随前者的生命周期。优势在于无需根据Android版本进行适配,灵活性高,劣势在于需要多出大量代码。

方法二:通过反射,使用自定义ClassLoader加载类中的方法,调用activity生命周期。优势是无需回调,劣势是加载效率偏低,且需要适配不同系统API。而且存在一个问题,系统启动activity和回调activity的时候,怎么识别你是APluginActivity还是BPluginActivity?需要hook系统Instrumentation,实现execStartActivity(启动实例-注册在AndroidManifext.xml中的StubActivity替换Component里的PluginActivity,并将旧的Component当成extra塞进intent,使系统可以正常启动),newActivtiy(创建实例,根据intent里的extra旧的Component,生成即A或B的实例)、通过反射让使用PluginActivity即可。资源均通过上一段静态架构里介绍的反射方式调用。

class AppInstrumentation(var realContext: Context, var base: Instrumentation, var pluginContext: PluginContext) :
    Instrumentation() {
    private val KEY_COMPONENT = "commontec_component"

    companion object {
        fun inject(activity: Activity, pluginContext: PluginContext) {
            // hook 系统,替换 Instrumentation 为我们自己的 AppInstrumentation,Reflect 是从 VirtualApp 里拷贝的反射工具类,使用很流畅~
            var reflect = Reflect.on(activity)
            var activityThread = reflect.get<Any>("mMainThread")
            var base = Reflect.on(activityThread).get<Instrumentation>("mInstrumentation")
            var appInstrumentation = AppInstrumentation(activity, base, pluginContext)
            Reflect.on(activityThread).set("mInstrumentation", appInstrumentation)
            Reflect.on(activity).set("mInstrumentation", appInstrumentation)
        }
    }

    override fun newActivity(cl: ClassLoader, className: String, intent: Intent): Activity? {
        // 创建 Activity 的时候会调用这个方法,在这里需要返回插件 Activity 的实例
        val componentName = intent.getParcelableExtra<ComponentName>(KEY_COMPONENT)
        var clazz = pluginContext.classLoader.loadClass(componentName.className)
        intent.component = componentName
        return clazz.newInstance() as Activity?
    }

    private fun injectIntent(intent: Intent?) {
        var component: ComponentName? = null
        var oldComponent = intent?.component
        if (component == null || component.packageName == realContext.packageName) {
            // 替换 intent 中的类名为占位 Activity 的类名,这样系统在 Manifest 中查找的时候就可以找到 Activity
            component = ComponentName("com.zy.commontec", "com.zy.commontec.activity.hook.HookStubActivity")
            intent?.component = component
            intent?.putExtra(KEY_COMPONENT, oldComponent)
        }
    }

    fun execStartActivity(
        who: Context,
        contextThread: IBinder,
        token: IBinder,
        target: Activity,
        intent: Intent,
        requestCode: Int
    ): Instrumentation.ActivityResult? {
        // 启动 activity 的时候会调用这个方法,在这个方法里替换 Intent 中的 ClassName 为已经注册的宿主 Activity
        injectIntent(intent)
        return Reflect.on(base)
            .call("execStartActivity", who, contextThread, token, target, intent, requestCode).get()
    }
    // ...
}

其他Service、BroadCastReceiver、ContentProvider均通过代理、反射、重载的方式实现其API调用即可。

插件化:

对功能拆分,使用主包+分包,分别独立发布。当用户用到此功能,再去下载对应patch包的一种实现方式。

         热部署:无需要重启Application和Activity(修改指针更新方式)

         温部署:需要重启Activity(修改指针更新方式)

         冷部署:需要重启Application(multidex更新方式)

Android插件化出现的原因在于:第一、Android5.1出现之前没有好的办法解决App方法数超过65536的问题,第二、由于加载的模块比较多,导致启动特别慢。本质上还是虚拟机支持JIT的加载机制。

 AndroidDynamicLoader 是最早分析的动态加载框架,主要使用activity当壳fragment当内容的方式,来使用空壳activity的应用,将满是fragment的apk加载进来,使用activity的生命周期来控制fragment的加载,其中原来自己做的一个自动化测试框架也是同样原理。

small 目前最新的插件化框架,支持切割dex实现动态加载,方便业务模块、公共的升级。

multiDex使用时,会判断系统是否支持multiDex,然后判断有无二级dex要安装,将二级dex解压到secondary-dexes目录,通过反射注入ClassLoader的pathList中,完成完整安装。

热更新:

即热修复,两种方式,一种是multidex,一种是修改C层指针;前者需要重新启动,后者无需重新启动。指下载patch包,修复错误的一种方式。

热修复出现的原因在于:从出现Bug,解决Bug,再发版,再升级,这个过程过于漫长,而且有的用户不愿意升级,影响功能的使用以及产品的体验。

        AndFix 采用差分包的方式,将要修复的文件打成dex包,通过注解的方式定位到要修改的文件,最终用jni在c层替换掉原文件的指针达到热修复的目的(这是一种So库的Hook方式,其他Hook方式多采用反射方式实现)。

         nuwa QQ空间使用类似办法,通过往classloader的pathlist里加入一个dex,采用覆盖的方式来替换到原来的模块。

增量更新是谷歌提出的一种App更新方式,Instant Run主要应对更新包过大,耗费流量的问题。

老版本2.0,新版本2.1,使用bsdiff工具分解出两者的差异包patch.jar,将patch发布,在客户端使用native的patch方法将data/app目录下的拿出的apk与patch.jar合并,之后再重新安装即可;此时的apk与原2.1的apk的md5一致。

优点:下载包小,流量耗费少

缺点:patch时间有点长,需要异步处理

Tinker,原理跟增量更新一样

相比而言类似Qzone的Nuwa是最好的

关于热修复,目前比较流行的有HotFix、Nuwa、DroidFix、AndFix等,这些框架均可以在github或其他地方找到,原理如上,方法多样,有覆盖的、有重定向的等等,通过配置、设置action等方式;而作为插件需要满足以下条件:

1、可以独立安装,但不可独立运行

2、具有向下兼容性,即可拓展性

3、只能运行在宿主程序中,而且可以被禁用、替换

使用场景包括修复线上bug、做手机皮肤、开发应用商店等系统提供的接口

JNI操作中,c中.h是开放的接口,.c是具体实现。
Tinker插桩,需要重新加载,因此需要重新启动。
Robust在每个类方法处,加一个跳转代码,如有bug则跳到补丁代码。

Android 中有三个 ClassLoader, 分别为BootClassLoaderPathClassLoaderDexClassLoader。其中

  • BootClassLoader 加载JVM运行核心库。
  • PathClassLoader 它只能加载已经安装的apk。因为 PathClassLoader 只会去读取 /data/dalvik-cache 目录下的 dex 文件。例如我们安装一个包名为com.hujiang.xxx的 apk,那么当 apk 安装过程中,就会在/data/dalvik-cache目录下生产一个名为data@app@com.hujiang.xxx-1.apk@classes.dex的 ODEX 文件。在使用 PathClassLoader 加载 apk 时,它就会去这个文件夹中找相应的 ODEX 文件,如果 apk 没有安装,自然会报ClassNotFoundException
  • DexClassLoader 是最理想的加载器。它的构造函数包含四个参数,分别为:
    1. dexPath,指目标类所在的APK或jar文件的路径.类装载器将从该路径中寻找指定的目标类,该类必须是APK或jar的全路径.如果要包含多个路径,路径之间必须使用特定的分割符分隔,特定的分割符可以使用System.getProperty(“path.separtor”)获得.
    2. dexOutputDir,由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或Jar文件中解压出dex文件,该参数就是制定解压出的dex 文件存放的路径.在Android系统中,一个应用程序一般对应一个Linux用户id,应用程序仅对属于自己的数据目录路径有写的权限,因此,该参数可以使用该程序的数据路径。
    3. libPath,指目标类中所使用的C/C++库存放的路径,主要指so库。
    4. classloader,是指该装载器的父装载器,一般为当前执行类的装载器。

Java中的双亲委派机制:判断class是否已经加载,如无且父ClassLoader不为空,则先从parent里查找,如无且父为空,则从系统ClassLoader里查找,最后从自己的ClassLoader里查找并返回。

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        // 先从父类加载器中进行加载
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 没有找到,再自己加载
                    c = findClass(name);
                }
            }
            return c;
    }

Tinker后台搭建:GitHub - typ0520/tinker-manager: 微信tinker补丁管理,后端代码+客户端sdk

每次构建时 ProGuard 都会输出下列文件:

dump.txt

说明 APK 中所有类文件的内部结构。

mapping.txt

提供原始与混淆过的类、方法和字段名称之间的转换。

seeds.txt

列出未进行混淆的类和成员。

usage.txt

列出从 APK 移除的代码。

压缩APK:

1、减少ProGuard的keep数量,例:去掉openmobileapi明显26M降到25M

2、官方https://developer.android.com/topic/performance/reduce-apk-size.html

简单Hook方式:https://www.diycode.cc/topics/568

总结起来热修复有两种策略:

1、像腾讯系的Tinker,使用覆盖的方式,打一个修复好的dex插入ClassLoader的findClass中的dexElements数组对象前面(学名插桩,通过反射拿到数组,将补丁插入到数组前面),这样可以优先使用它的方法(需要重启生效)。为避免出现关联(直接互相调用,无递归)的类在不同dex而报错的问题(只在同一dex的类,则会被打上CLASS_ISPREVERIFIED标志{即文件已经被预先验证过,类加载过程二的验证过程}),需要在所有类(Application除外,因为包含AntilazyLoad类的补丁包hack.dex,在Application的onCreate方法里加载,构造器执行时无此类,则报错系统标记无此类)的构造器插入一段代码:

if (ClassVerifier.PREVENT_VERIFY) {

    System.out.println(AntilazyLoad.class);//加载类文件

}

public class ClassVerifier{

    public static boolean PREVENT_VERIFY=false;//防止代码被执行,以提高性能

}

public class AnilazyLoad{

}

表示所有类都有引用其他dex里的文件,故都不会打上CLASS_ISPREVERIFIED标志(损失安全和性能),从而保证patch包的成功率。

2、像阿里系的AndFix,使用指针的方式,修改方法入口,指向新的方法;不需要重启即时生效。

两者均可使用差分包,用来降低升级patch的大小。

其他so库的加载,有补丁使用System.load方法,无补丁使用System.loadLibrary方法;还有一种办法,类似上文的插桩,只不过是插入nativeLibraryPathElements数组对象前面。

其实还有一种是美团的Robust框架,使用InstantRun的方式,每个类里的方法都有个静态变量,无补丁使用原方法,有补丁使用补丁方法,通过下发补丁包的方式,注入补丁。此种方法,对于类方法错误的修改,效果是非常明显的。

原理:Java高级之虚拟机加载机制

参考:

专栏 - 腾讯WeTest,您的质量守护专家

https://github.com/limpoxe/Android-Plugin-Framework

https://github.com/kaedea/android-dynamical-loading

安卓App热补丁动态修复技术介绍

全面解析 Android 热修复原理 - 知乎

【Android 修炼手册】常用技术篇 -- Android 插件化解析 - 掘金

手机迅雷的兄弟接入RePlugin时,总结的,感觉很不错,拿出来重新分享下,当你不能100% Cover RePlugin的时候,只要能遵守以下规范,是肯定不会有问题的:

1:代码绝对要完全隔离;
2:是通信全部走aidl(RePlugin提供了Binder);
3:aidl接口要严格管控,每一个接口都要设计好,不能随便修改,顺序不能乱,发出去就定了;
4:插件和宿主之间的消息传递,一律用Bundle;宿主内传递,插件内传递,随便;
5:当插件和宿主直接有回调时,走本地广播(RePlugin提供了LocalBroadCast的封装,支持跨进程,也支持本进程);
6:在开发插件的时候,当独立进程来设计(即便当前是同一个进程,也有有这种意识);
7:如果你想用的花哨,就先看一下代码,熟悉整体架构,这样,遇到问题的时候,能定位解决。 
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘兆贤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值