Android 插件化整理

插件化技术整理

前言

读<<Android插件化开发指南>>总结。
这篇文章不分析系统源码,只整理对加载插件apk中的一些问题的解决方案。插件化就是加载未再Android系统中注册的apk
  1. 加载插件中的类 有两种方案
  • 合并dex

    加载类的ClassLoader中有一个DexPathList对象,里面有一个dexElements数组,专门用来存放apk中dex集合的,通过DexFile加载插件中的dex文件,将它与宿主中的dex合并形成一个新的数组,通过反射修改系统中dexElements的值,这样就可以直接加载在插件apk中的类了。该方案也可用于热修复。

  • 为插件apk构建一个单独classLoader

     val mBaseClassLoader = context.classLoader
     val dexOutputPath = context.getDir("dex", Context.MODE_PRIVATE).absolutePath
     // 第一个参数 apk文件路径 第二个:dex文件存放路径 第三个 lib(so)文件存放路径
     val dexLoader = DexClassLoader(path,dexOutputPath,null,mBaseClassLoader)
    

自定义实现一个classloader 用来代理 所有插件和宿主的classloader,再用这个classloader替换系统中的。

val packageInfo = RefInvoke.getFieldObject(context, "mPackageInfo")
RefInvoke.setFieldObject(packageInfo,"mClassLoader",pluginClassLoader)
Thread.currentThread().contextClassLoader = pluginClassLoader

此方案,用Class.forName加载不到插件中的类
2. 一个apk除了代码,还有的就是资源文件了,指放在res和assets目录下的文件,构建能够加载插件apk中资源的Resources和AssetManager

 assets = RefInvoke.createObject(AssetManager::class.java) as AssetManager
 RefInvoke.invokeInstanceMethod(assets,"addAssetPath",String::class.java,path)
 mResources = Resources(assets,context.resources.displayMetrics,
         context.resources.configuration)

通过addAssetPath方法可以将所有插件以及宿主的资源都放在一个AssetManager中,然后再反射替换系统原来的Resources.
此时需要注意资源文件id冲突的问题,解决方案:

  • apk中的资源文件的打包工作都是通过aapt完成的,可以通过修改系统源码中aapt设置的packageid,即资源id的0x7f前缀,重新生成一个新的aapt命令替换原生的。
  • 通过hook Android的打包流程,修改R文件以及最终生成的arsc文件的id完成。(gradle-small 框架)
  1. 一个apk除了普通的类以外,还包含需要与AMS等系统服务打交道的类。如果apk中不包含这些类,比如插件中全是fragment,那么就无需做特殊的处理了,早期的一个插件化框架就是基于fragment完成的。

而这些需要系统进行创建的特殊类,比如四大组件以及application。加载这些特殊类有两种方案:动态替换以及静态代理

  • 动态替换

    1. activity以及service的动态替换

    activity和service的创建,系统会检测是否在manifest中注册,动态替换是在开发中通过hook系统类,欺骗AMS的校验,再校验通过后再启动为插件中的类。

    校验前:
    修改Activity的startAcivityForResult方法,先替换为占坑的activity,同时吧真实要启动的信息保存在intent中。
    或者通过 hook AMS 完成替换

    校验后:
    为 ActivityThread 的H 类加一个callback,再启动activity以及创建service 时替换为真实插件中的类。同时需要替换原来的Instrumentation类,因为activity创建是通过这个类的newActivity方法,service创建是通过ActivityThread的handleCreateService方法。

    若需要对activity的launchMode支持,需要提前在宿主中定义不同的launchMode占坑activity。

    service的启动因为多次start只会创建一次,所以此方案需要再宿主中提前定义好多个占坑service

    1. receiver的处理

    动态广播可以当成一个普通的观察者类看待,无需做特殊处理。

    静态广播:通过PackageParser解析插件apk的manifest中的receiver信息,然后再动态注册这些广播。

    若想支持静态广播的特性,需要提前在宿主中定义一个占坑的静态receiver,再想办法将插件receiver与这个占坑receiver的action一一对应起来(比如插件receiver定义一个metedata值,用来定义占坑receiver对应的action值)
    然后再收到占坑receiver的广播后再分发给插件的receiver。
    此时占坑receiver需要定义很多的action针对不同的插件receiver

    1. contentprovider的处理

    先解析manifest中的provider信息,然后反射调用ActivityThread 的 installContentProviders 方法 把插件 中的 provide 安装到 宿主中

    也可以通过 在宿主中定义一个contentProvider作为中转,再间接调用插件的provider。

  • 静态代理

    静态代理是将插件中的所有类都当成一个普通类来对待,这样就不需要hook系统类来完成替换工作了。

  1. activity的应用

而要使普通类具备activity的生命周期,需要先在宿主中定义一个代理的activity,再代理的activity中反射创建出插件的activity,生命周期方法中 调用插件activity的方法。

@SuppressLint("MissingSuperCall")
class ProxyActivity : Activity() {

    protected var pluginActivity: IRemoteActivity? = null
    get() {
        if (field != null) return field
        else {
            field = loadPluginActivity()
        }
        return field
    }

    override fun onCreate(savedInstanceState: Bundle?) {
//        super.onCreate(savedInstanceState)  // 插件activity 决定调用时机
        pluginActivity?.onCreate(savedInstanceState)
    }

    fun superOnCreate(savedInstanceState: Bundle?) {  // 需要插件activity调用 所以改为public
        super.onCreate(savedInstanceState)
    }

    private fun loadPluginActivity() : IRemoteActivity {
        val pluginName = intent?.getStringExtra(PluginConstant.PLUGIN_NAME) ?: ""
        val activityName = intent?.getStringExtra("activity_name")
        val a = PluginManager.getPluginClassLoader(pluginName)?.loadClass(activityName)?.newInstance() as IRemoteActivity
        a.setProxy(this,pluginName)
        return a
    }
}
  1. receiver 以及 provider 的应用

    receiver: 在宿主的ProxyReceiver中 根据intent的信息 反射创建 插件 receiver 对象 调用其的onReceiver方法

    provider: 在宿主ProxyProvider中 通过uri 解析出插件对应provider的信息,反射调用对应的方法。

  2. service 的 应用

    重复启动一个service的时候,是不会重复创建的,所以一个代理service只能对应一个插件service。而若想做到一对多的关系,可以自己管理插件service的创建销毁,同时因为service 能够跨进程间通信,所以需要一个ProxyService来代理,将真实要启动的service信息通过intent传递给ProxyService

    startService: 在宿主ProxyService中的onStartCommand方法创建插件service 并调用onCreate 再调用onStartCommand方法(重复启动service时,onStartCommand此方法会重复调用)

    bindService: 此方法会返回一个IBinder对象,同时会传入一个ServiceConnection接口做回调,而unBinder时直接通过connection停止的,所以再处理时 需要将intent 与 connection 做一个对应关系存储。(问题: 重复bindService,onBind()方法只会走一次。)

  3. so库加载

    so的加载有两种方案:

  • System.loadLibrary()
    此方法可以加载放在libs目录下的so文件。在创建DexClassLoader时,第三个参数用来指定解压后的so文件存放目录,即可以用这种方式加载。
  • System.load()
    此方法 , 可以加载任意路径下的so文件。
    注意,如果要用此方法在64位设备上加载32位的so库,需要现在libs目录下放一个占位用的32位(armeabi-v7a),先用loadLibrary加载一次。
  1. 相关技术

    • 降级开发 为奔溃的界面(activity),下发一个H5页面,用来暂时替代。
    • 混淆 插件宿主同时用到的公共库必须用同一套混淆规则。需要在宿主中通过反射创建的类,不能被混淆。打包插件apk,可以分dex打包,打包后,最终可以去掉与宿主共用到的公共库dex。
    • 增量更新 用bsdiff 生成两个apk之间的差异文件,然后再用ApkPatchLibrary 将这个差异包与以前的apk合并成一个新的apk。

简单demo地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值