美文网首页
BiBi - Android 插件化

BiBi - Android 插件化

作者: 奋飞的蜗牛ing | 来源:发表于2019-11-11 15:18 被阅读0次

    From:Android插件化开发指南

    目录

    1. 预备知识
      1.1 简介
       插件化的用途
       插件化的发展史
      1.2 Binder原理
      1.3 Activity工作原理
       App启动流程 / App内部页面跳转
      1.4 PMS
      1.5 ClassLoader
      1.6 反射
      1.7 代理模式
    2. 插件化知识
      2.1 加载外部类
      2.2 插件的Application
      2.3 访问插件中的类
       方案1:把插件dex合并到宿主dex
       方案2:为每个插件创建ClassLoader
       方案3:Hook App原生的ClassLoader
      2.4 访问插件中的资源
       2.4.1 资源简介
        AssetManager
        Resources
       2.4.2 资源访问
        方案1:在宿主Activity中创建插件的AssetManager
        方案2:宿主插件共用AssetManager
       2.4.3 资源id冲突
        方案1:修改AAPT构建工具
        方案2:修改R.java & resources.arsc文件
      2.5 最简单的实现一个插件化
    3. 插件化四大组件
      3.1 Activity
       3.1.1 动态框架
        上半场:用Stub欺骗AMS
        下半场:启动真实Activity
        解决LaunchMode问题
       3.1.2 静态代理that框架
        解决LaunchMode问题
      3.2 Service
       3.2.1 动态框架
        startService的解决方案
        bindService的解决方案
       3.2.2 静态方案
      3.3 BroadcastReceiver
       3.3.1 动态方案
        动态广播的解决方案
        静态广播的解决方案
         方案1:把静态广播转换为动态广播
         方案2:占位StubReceiver
       3.3.2 静态框架
    4. 插件化相关知识
      4.1 基于Fragment的插件化
      4.2 插件的混淆
       方案1:不混淆公共库midlib
       方案2:混淆公共库midlib
      4.3 增量更新
      4.4 so的插件化
       4.4.1 so知识的简介
       4.4.2 so的加载流程
       4.4.3 so的加载方法
       4.4.4 基于System.loadLibrary的so插件化
       4.4.5 基于System.load的so插件化
      4.5 自定义Gradle
       Extension动态设置
       afterEvaluate应用

    1. 预备知识

    1.1 简介

    插件化的用途

    游戏平台,按需下载。【体积 & 更新】PC是,而Android采用服务器动态下发脚本。
    动态更新:增加新功能或完整的模块,80%用于修复线上bug。
    换肤:用于游戏领域,王者荣耀的换肤,上线新英雄,调整数据。
    ABTest,数据驱动产品。
    独立性 & 开发效率【组件化???】
    插件化的未来:虚拟机技术 — — 应用双开。

    国内对RN等相关技术的需求远大于插件化;GooglePlay不允许插件化App的存在。

    插件化的发展史

    2012.07 —— 大众点评,基于Fragment。
    2013.03 —— 淘宝Atlas,未开源。
    2014.03 —— 任玉刚,that框架静态代理,非Hook。
    2014.11 —— 提出StubActivity欺骗AMS。
    2014.12 —— Android Studio1.0,可以借助Gradle。
    2015.08 —— 360手机助手张勇,DroidPlugin。
    2015.12 —— 林光亮Small框架。
    此时,插件化中遇到的技术难题都已解决。【开始关注:热修复技术和RN】
    2017.06 —— 360手机助手RePlugin。
    可见,一项技术5年时间内由雏形到成熟。

    1.2 Binder原理

    • Client、Service、ServiceManager三者关系。
    • AIDL
    • Binder、IBinder、IInterface、Stub.asInterface()、asBinder()、onTransact()
    • 问题:
      1)类结构层级设计原理?
      2)跨进程与同进程是如何区分?
      3)onTransact在同一进程如何被调用?
      4)一次通信的过程,数据如何传递和解析?

    1.3 Activity工作原理

    App启动流程 / App内部页面跳转
    • Launcher在一个不同的进程。
    • App安装时,Android系统中的PackageManagerService从apk包中的AndroidManifest文件中读取信息。
    • Launcher、App、AMS关系。
    • 启动流程。【ActivityThread中有main入口,即主线程】

    mMainThread的类型为ActivityThread

    ActivityThread中持有Instrumentation【仪表盘】的引用。在performLaunchActivity()中activity.attach(...)将其传递给Activity类。
    ApplicationThread extends IApplicationThread.Stub
    ActivityManagerService extends IActivityManager.Stub
    App端通过获取IActivityManager调用AMS中的方法。
    AMS端通过获取IApplicationThread调用App端的方法,如:bindApplication。
    ApplicationThread调用ActivityThread的sendMessage,通过H类调用分发。
    最终:Instrumentation newActivity > callActivityOnCreate > onCreate。

    Activity中哪个类扮演Stub,哪个类扮演Proxy?
    ServiceManager.getService("xxx") // 连接池?

    一个应用中Context的个数 = Service个数+Activity个数+1(Application的)

    getApplicationContext在ContextImpl中实现,返回的就是在ActivityThread main()方法中初始化的Application对象。

    1.4 PMS

    PMS加载包的信息,将其封装在LoadedApk这个类对象中,然后可以从中取出AndroidManifest中的信息。

    结束安装的时候,都会把安装信息保存在xml文件中,当Android系统再次启动时,会重新安装所有的apk,就可以直接读取之前保存的xml文件。
    Android的5个安装目录:data/app-private、data/app、system/app、vender/app、system/framework。

    Android系统重启后,会重新安装所有的App,这是由PMS类完成,并且App首次安装到手机上也是由PMS完成。

    PMS中的一个类PackageParse,用来解析AndroidManifest文件,通过反射调用generatePackageInfo()来获取插件中的四大组件。

    涉及到AIDL:IPackageManager

    1.5 ClassLoader

    DexClassLoader可以根据optimizedDirectory加载需要的【dex、apk、jar】文件,并创建一个DexFile对象,也可以从外部SD卡加载。

    对于App而言,Apk文件中有一个classes.dex,他是Apk的主dex,通过PathClassLoader加载,它的父类是BaseDexClassLoader。MultiDex把一个dex文件拆分成多个dex文件,每个dex的方法数量不超过65536个,classes.des主dex由PathClassLoader加载,其它classes2.dex等会在App启动后使用DexClassLoader加载。

    可以让classes.dex中只保留App启动时所需要的类以及首页的代码,从而确保App进入首页时间最少。

    如何手动指定classes2.dex中包含哪些类的代码?

    gradle配置:

    dexOptions{
      additionalParameters += '--main-dex-list=maindexlist.txt'
    }
    

    增加maindexlist.txt文件,里面包括要将哪些文件保留在主dex中,注意是class文件。如:

    ljg/aaa/a.class
    ljg/aaa/b.class
    

    后面有一个详细的例子。

    1.6 反射

    • 如何反射一个泛型类
    • 网络数据解析 & Json2Bean是如何利用反射实现的?
    • setAccessible(true)的本质?【跳过校验】
    • jOOR库,在Android中不支持反射final类型的字段,因为:Android的Field类中没有定义final字段。

    1.7 代理模式

    • 动态代理的原理?
    • 生成的代理类是什么样子的?
    • PMS是系统服务,为什么没有办法Hook?
      只能Hook App自己进程的东西,Hook永远只在Client端,若在Service端那就是病毒了。所以,App只能对App所在的进程进程Hook,所影响的范围也仅限于App本身。

    Java动态代理只能代理接口,不能代理类, 为什么?如何破?
    Java动态代理是由Java内部的反射机制来实现的,而cglib动态代理底层则是借助asm来实现的。https://blog.csdn.net/u010111422/article/details/69062338

    在Hook过程中,什么时候用静态代理【暴露类】,什么时候用动态代理【暴露接口】

    Hook AMS => ActivityManagerNative :: gDefault :: Singleton :: mInstance
    Hook ActivityThread => sCurrentActivityThread :: mH :: mCallBack

    2. 插件化知识

    2.1 加载外部类

    ClassLoader classLoader = new DexClassLoader("assets/aaa.apk", getAbsolutePath(), null, getClassLoader());
    Class mLoadClassBean = classLoader.loadClass("plugin.test.AaaBean");
    Object beanObject = mLoadClassBean.newInstance();
    Method method = mLoadClassBean.getMethod("getName");
    method.setAccessible(true);
    String name = (String)method.invoke(beanObject);
    

    利用反射可以不用引起其对象,调用其类中的方法时也要通过反射。如果使用接口编程,在反射出对象后,可以直接类型转换为该接口对象,从而可以直接调用类中的方法,不再通过反射。

    2.2 插件的Application

    插件Application的onCreate是没有机会调用的,除非我们在宿主自定义的Application的onCreate方法中利用反射来执行插件们的onCreate方法。因此,插件Application没有生命周期,它就是一个普通的类。

    2.3 访问插件中的类

    方案1:把插件dex合并到宿主dex

    BaseDexClassLoader :: pathList :: dexElements[ ]

    方案2:为每个插件创建ClassLoader

    为每个插件创建一个ClassLoader,把LoadedApk类中mClassLoader替换为插件的ClassLoader。
    ActivityThread :: currentActivityThread :: mPackages
    mPackages中缓存dex文件。
    为插件创建loadedApk,然后mPackages.put(packageName, loadedApk)
    loadedApk :: mClassLoader 赋值为插件的ClassLoader。
    缺陷:Hook的点太多

    方案3:Hook App原生的ClassLoader

    修改App原生的ClassLoader【mPackageInfo :: mClassLoader】。构建一个SuperClassLoader类,它内部有一个mClassLoaderList变量,即持有所有插件ClassLoader的集合。于是SuperClassLoader的loadClass()方法,会先尝试使用宿主的ClassLoader【即系统的】加载类,如果不能加载,就遍历插件的ClassLoader。

    注意:使用该方案加载插件中的类时,不能再使用Class.forName()方法来反射插件中的类了,因为Class.forName会使用BootClassLoader来加载类,这个类并没有被Hook。应该使用:getClassLoader().loadClass()来反射类。

    2.4 访问插件中的资源

    2.4.1 资源简介

    将插件放在宿主的assets目录中,App启动时会把assets目录中的东西加载到内存中。【assets目录不编译】

    AssetManager

    AssetManager的addAssetPath方法可以解决资源的插件化。由于apk下载后不会解压到本地,所以无法直接获取到assets的绝对路径。只能通过AssetManager类的open方法来获取assets目录下的文件资源。AssetManager中的addAssetPath方法,App启动时会把当前apk的路径传递进去,从而能够访问当前apk的所有资源。传插件的路径时,就能访问插件中的资源了。

    Resources

    Resources是外暴露的类 => 调用AssetManager中的方法 => 访问resources.arsc文件。resources.arsc在打包时生成。

    2.4.2 资源访问

    方案1:在宿主Activity中创建插件的AssetManager

    宿主中读取插件里的资源:
    1)反射创建AssetManager对象,调用addAssetPath方法,把插件的路径添加到这个AssetManager对象中,这个对象只为该插件服务。并根据该AssetManager对象创建相应的Resources和Theme对象。
    2)重写Activity的getAsset()、getResources()和getTheme()方法,返回新创建的插件对象。【如果没有则默认读取宿主中的资源】
    3)宿主中加载外部插件,生成该插件的ClassLoader。通过反射获取插件中的类,从而读取插件中的资源。

    // 插件中被调用的方法
    public String getStringF(Context context){
      return context.getResources().getString(R.string.hello);
    }
    

    注意:反射调用插件中的getStringF方法时,传入的context是宿主中的MainActivity.this,因为宿主Activity的getResources已经被覆写,此时返回的是该插件的AssetManager所创建的Resources对象。

    当宿主需要某个插件中的资源时,才会loadResource,即利用反射为某插件生成AssetManager对象和与其相关的Resources、Theme,再反射调用addAssetPath方法。宿主默认是加载自己的资源。

    将插件中的getStringF()移到宿主中去定义了,插件不做任何事

    R.java中的内部类:

    R.java中的string类:

    该R.java会存在apk包的classes.dex文件中,宿主可以直接访问插件中R.java的内部类如:string、id、color等。

    Class stringClass = pluginClassLoader.loadClass("com.ljg.plugin.R$string");
    int resId = stringClass.getDeclaredField("a_plus");
    tv.setText(getResources().getString(resId));
    

    其中,getResources()方法返回插件的Resources。

    插件如何访问插件中的资源呢?插件不能自动加载自身的资源,因为该插件中的资源并没有addAssetPath到资源池中。所以,跟宿主访问一样,一样需要反射AssetManager并调用addAssetPath,同时还要覆写getAsset()、getResources()和getTheme()方法。

    总结:该方案不会合并宿主和插件的资源,进入到哪个插件,就为这个插件创建AssetManager和Resource对象,AssetManager通过反射调用addAssetPath方法,把插件自己的资源添加进去,当宿主进入到一个插件的时候,就把AssetManager切换为该插件的AssetManager,所以插件就只能加载到插件中的资源了。

    方案2:宿主插件共用AssetManager

    构建一个超级AssetManager对象,在addAssetPath时,添加宿主和所有插件的资源。该Resources为全局变量。【宿主和插件如何共享数据???】

    注意:插件Activity中必须覆写getResources()方法,返回超级Resources全局变量。

    public Resources getResources() {
      return PluginManager.mSuperResources;
    }
    

    方案2会存在资源id冲突问题,如何解决呢?在下一节介绍。

    2.4.3 资源id冲突

    背景:把宿主和插件的资源合并到一起,通过AssetManager的addAssetPath来实现,此方案会产生资源id冲突。
    原因:宿主App和各插件App都是各自打包。
    思路:Hook App打包过程中的aapt阶段。

    Android打包流程:

    1、aapt。为res目录的资源生成R.java文件,同时为AndroidManifest.xml生成Manifest.java文件。
    2、aidl。把项目中定义的aidl文件生成相应的Java代码。
    3、javac。自己编写的代码+aapt生成的Java文件+aidl生成的Java文件,编译成class文件。
    4、proguard。混淆的同时生成proguardMapping.txt文件。
    5、dex。自己项目中生成的class文件+第三方库的class文件,转换为dex文件。
    6、aapt。打包,把res目录下的资源、assets目录下的文件,打包成一个.ap_文件。
    7、apkbuilder。将所有的dex文件+.ap_文件+AndroidManifest.xml打包为.apk文件。
    8、jarsigner。对apk进行签名。
    9、zipalign。对要发布的apk文件进行对齐操作,以便运行时节省内存。
    
    方案1:修改AAPT构建工具

    资源id的定义格式:public static final int fade_in=0x7f050023;该十六进制由三部分组成:PackageId【7f】+ TypeId 【05】+ EntryId【0023】
    PackageId:apk包的id,默认为0x7f。
    TypeId:资源类型值,如:layout、id、string、drawable。

    具体过程:
    1)修改AAPT这个Android SDK工具,在AAPT的命令行参数中指定插件资源id的前缀。一般选用0x71~0xff这个区间内的值作为前缀。
    2)把修改后的AAPT工具命名为aapt_mac,放在项目根目录下。
    3)修改gradle,通过脚本反射,把AAPT的路径修改为该App根路径下的aapt_mac。

    public.xml固定id值

    场景:多个插件都需要一个自定义控件,把它放在宿主中,插件调用宿主的Java代码和使用宿主的资源。
    问题:App每次打包后,会随着资源的增加,同一个资源的id值也会发生变化。
    方案:如果宿主App的某个资源id被插件使用,那么为了避免下次因资源值变化而导致资源找不到,需要把这个资源id值写死,这个固定的值要保存在public.xml文件中,放在res/values/目录下。

    <resources>
      <public type="string" name="house_name" id="0x7f092234">
    </resources>
    

    在gradle1.3版本之前是默认支持public.xml的,但之后不再支持了,所以要在build.gradle中添加相应任务。

    应用:插件如何使用宿主中的固定资源?把宿主打包成jar包被各插件compileOnly,在插件中使用StringConstant.house_nameStringConstant类是根据public.xml自动生成的。

    方案2:修改R.java & resources.arsc文件

    Android中的两类资源AssetManager和Resources,其中AssetManager直接通过文件名称就可以获取到具体资源,而Resources先在resources.arsc文件中通过id查找到资源文件名称,然后再通过AssetManager来获取资源。

    优化:resources.arsc中存放了很多冗余的资源。因为我们开发时引入的AppCompat包、Design包,这些包也要生成资源id。对插件而言每个插件包的resources.arsc文件中都会有一份相同的资源,这样就冗余了。所以对于插件中AppCompat包、Design包资源会在resources.arsc中删除,只会在宿主的resources.arsc中存在。

    具体过程:
    1)aapt会生成R.java文件,Hook processReleaseResources这个task,在它之后将R.java文件中的0x7f修改为0x71。【注:R.java文件不能修改,只能重新建一份保存】
    2)aapt还会生成一个后缀为ap_的压缩包,里面有AndroidManifest.xml、res、asset、resources.arsc文件,解压取出resources.arsc,把里面的0x7f修改为0x71。
    3)删除resources.arsc文件中的冗余的资源Id,如AppCompat库。
    4)Hook compileReleaseJavaWithJavac,把所有class中的R$drawable.class、R$layout.class这样的class删除,因为它们中保存的资源Id值还是以0x7f为前缀。
    5)将步骤1中新生成的R.java文件,执行javac,生成R.class文件。

    疑惑:步骤4、5有必要吗?在步骤1中,不能将新生成的R.java替换旧的吗?

    2.5 最简单的实现一个插件化

    1)合并所有插件的dex,来解决插件的类加载问题。
    BaseDexClassLoader :: pathList :: dexElements。dexElements类型是Element[ ]数组,即利用反射把宿主和插件中的Element[ ]合并到一起,替换dexElements的值。
    2)把插件中所有的资源统一性地合并到宿主的资源中。【可能导致资源id冲突】
    3)预先在宿主的AndroidManifest文件中声明插件的四大组件。

    提示:AndroidManifest文件中可以声明不存在的Activity类。AndroidManifest文件只做格式校验,不会进行编译。

    3. 插件化四大组件

    3.1 Activity

    3.1.1 动态框架
    上半场:用Stub欺骗AMS

    ActivityManagerNative :: gDefault :: mInstance :: Singleton :: IActivityManager

    下半场:启动真实Activity

    ActivityThread :: sCurrentActivityThread :: mH :: mCallback

    解决LaunchMode问题

    问题:AMS会认为每次要打开都是StubActivity,在AMS端有个栈,会存放每次要打开的Activity,那么现在这个栈上就都是StubActivity了。插件中设置的singleTask、singleTop和singleInstance都无效。
    解决:占位思想。事先为SingleTop、SingleTask、SingleInstance这三种LaunchMode创建多个StubActivity,指定插件Activity与哪个StubActivity对应关系。

    在插件AndroidManifest中设置的许多属性都是无效的。

    3.1.2 静态代理that框架

    每次都是启动宿主中的ProxyActivity,携带参数:要打开页面所在插件的路径dexPath和要打开Activity的全路径名。在宿主ProxyActivity中反射插件中的要启动的Activity类,但反射出来的Activity是一个普通的类,不具有Activity的生命周期。所以要在ProxyActivity的声明周期方法中调用插件Activity的相应方法,以此来同步Activity的声明周期。同时ProxyActivity中通过反射调用setProxy(this)与PluginActivity建立双向通信,在PluginActivity中持有ProxyActivity的引用命名为that。由于插件中定义的Activity都是一个木偶,而非真正的Activity,所以this.setContentView();this.findViewById();就会运行时报错误,而改为that.setContentView();that.findViewById();

    问题:为什么Hook之后会有生命周期呢???

    消灭that关键字

    基类中实现,但Activity的final方法不能覆写只能使用that调用。

    @Override
    public View findViewById(int id){
      return that.findViewById(id);
    }
    
    跳转

    宿主跳插件;宿主跳宿主;插件跳宿主;插件跳插件。

    只有在跳插件时,才会使用ProxyActivity。

    接口简化

    在静态代理中使用面向接口的编程思想来减少反射的使用。

    解决LaunchMode问题

    维护一个atyStack集合,它持有所有打开的插件Activity。

    switch(launchMode){
      case Standard:
        正常存入集合atyStack中;
        break;
      case SingleTop:
        判断atyStack倒数第二个元素是否即将打开的插件Activity,如果是则移除,并调用其finish()方法;
        break;
      case SingleTask:
        移除这个元素以及在它之上的元素,并调用finish()方法;
        break;
      case SingleInstance:
        只把这个元素移除,并调用finish()方法;
        break;
    }
    

    注意:与原生不同,这种方法是重新创建一个Activity,再finish掉之前的Activity,而不是复用。并且,如果所有的Activity都是插件Activity那这种方案是OK的,如果宿主中也有Activity,并且不受ProxyActivity的管理,那宿主中的Activity不会遵守该种方案。

    3.2 Service

    3.2.1 动态框架

    问题:可以使用一个StubActivity来“欺骗AMS”【不考虑LaunchMode】,而对于同一个Service调用多次startService并不会启动多个Service实例。所以只用一个StubService是应付不了多个插件Service的。
    解决方案:预先占位。考虑到一个App中Service的数量不会超过10个,所以在宿主中创建StubService1、StubService2等,并且它们与插件中的Service一一对应。

    startService的解决方案

    首先,把插件和宿主的dex合并,这样可以加载插件中的类;其次,“欺骗AMS”。
    Hook上半场:
    ActivityManagerNative :: gDefault :: mInstance :: Singleton
    Hook IActivityManager【将PluginService切换回StubService】
    Hook下半场:
    ActivityThread :: sCurrentActivityThread :: mH :: mCallBack
    需要截获handleMessage方法中的case CREATE_SERVICE【将StubService切换回PluginService】

    bindService的解决方案

    与startService类似,但有两点需要注意:
    1)在Hook上半场时,对于unbindService不需要“欺骗AMS”,因为unbindService(_)需要一个ServiceConnection类型的参数,跟intent没有关系,所以不需要“欺骗AMS”。AMS会根据ServiceConnection参数找到对应的Service。
    2)在Hook下半场时,不再需要将StubService切换回PluginService。因为在startService下半场Hook中,在CREATE_SERVICE时已做了切换处理,handleCreateService方法会把启动的PluginService放在mServices集合中。当handleBindService和handleUnbindService时会从mService集合中找到PluginService进行绑定和解绑。

    3.2.2 静态方案

    与Activity静态方案类似。注意:要在ProxyService的onStartCommand和onBind方法中需要先反射实例化RemoteService对象,调用其mRemoteService.onCreate方法,然后再调用其mRemoteService.onStartCommand和mRemoteService.onBind。

    单纯的静态方案也不能实现用一个StubService就能对应多个插件的Service。可以通过Hook一部分代码 + 静态代理来实现。【纯Hook当然也可以,只不过使用静态代码会少Hook一些】

    思路:将所有启动的Service放到一个集合中,每次从intent中取出真正要启动的Service,在该集合中查找,如果不存在则create service,存在则返回。当service结束时,要从该集合中删除。

    3.3 BroadcastReceiver

    3.3.1 动态方案

    动态广播的解决方案
    不需要跟AMS打交道,只要合并插件的dex,保证宿主能加载插件中的广播类,反射调用其onReceive方法即可。

    静态广播的解决方案
    问题:不能使用插桩方案,因为广播必须指定IntentFilter,而IntentFilter中的action参数是随意设置的。

    方案1:把静态广播转换为动态广播

    将插件中声明的静态广播【安装App时会注册在PMS中】转换为动态广播注册到AMS中。
    具体措施:
    1)反射PMS读取插件AndroidManifest文件中声明的静态广播。
    2)使用插件的ClassLoader加载静态广播,实例化为一个对象,然后作为动态广播注册到AMS中。

    注意:该方案丧失了静态广播不需要启动App就可以被启动的特性。

    方案2:占位StubReceiver

    占位StubReceiver,该静态广播会预定义多个Action,每个Action都会对应一个插件中的静态广播。
    宿主中占位的静态广播:

        <receiver
          android:name=".HostReceiver"
          android:enabled="true"
          android:exported="true">
          <intent-filter><action android:name="stub1" /></intent-filter>
          <intent-filter><action android:name="stub2" /></intent-filter>
          <intent-filter><action android:name="stub3" /></intent-filter>
          ......
        </receiver>
    

    插件中定义的静态广播:

        <receiver
          android:name=".PluginReceiver"
          android:enabled="true"
          android:exported="true">
          <intent-filter><action android:name="realReceiver1" /></intent-filter>
          <meta-data
            android:name="oldAction"
            android:value="stub1" />
        </receiver>
    

    注意:同样需要把插件中的静态广播作为动态广播手注册到AMS中。

    使用流程:
    1)启动HostReceiver,携带action=stub1。
    2)在HostReceiver的onReceiver()方法中,得到action=stub1。
    3)解析插件AndroidManifest中receiver的action和meta-data信息,将其保存在map中,如:map.put("stub1","realReceiver1")。
    4)根据action=stub1,从map中获取到真正的realReceiver1,发射实例化并sendBroadcast()。

    3.3.2 静态框架

    最简单,可以实现一个StubReceiver对应多个插件的Receiver。但that框架只能支持动态广播,不支持静态广播。

    4. 插件化相关知识

    4.1 基于Fragment的插件化

    原理:一个App中只有一个Activity来承载所有的Fragment。Fragment不同于四大组件,它就是一个简单的类,不需要与AMS进行交互。在这个唯一的Activity中需要管理所有插件的ClassLoader来加载相应插件中的Fragment,并且还要将宿主和插件资源合并在一起。
    缺陷:对四大组件未能实现插件化。
    三种跳转场景:
    1)宿主跳出插件的Fragment
    2)从插件的Fragment跳本插件的Fragment【Fragment进出栈】
    3)从插件的Fragment跳宿主或其它插件的Fragment

    4.2 插件的混淆

    proguard工具不仅做混淆,还会把项目中用不到的方法删除掉。【???】
    插件不支持加固,宿主可以加固,但插件支持签名。
    混淆的规则:

    1、四大组件和Application要在AndroidManifest中声明,不能混淆。
    2、R文件不能混淆,因为有时会通过反射获取资源。
    3、support的v4、v7包中的类不能混淆,系统的东西,不能随意动。
    4、实现了Serializable的类不能混淆,否则反序列化会出错。
    5、泛型不能混淆。
    6、自定义View不能混淆,否则Layout布局中使用自定义View时会找不到。
    7、反射的类不能混淆。 
    

    宿主和插件都会引用midlib基础库,那么混淆时如何对midlib进行处理呢?

    方案1:不混淆公共库midlib

    插件中compileOnly midlib库,compileOnly不会混淆。并在宿主中keep midlib中的所有类。

    方案2:混淆公共库midlib

    具体过程:
    1)插件中compile midlib库。
    2)multidex手动拆包,把插件拆分成两个包,插件中的代码都放在主dex中,而其他代码放在classes2.dex中【包括midlib和其他compile的库,这些库都会在宿主中同时存在一份】。
    3)gradle配置

    dexOptions{
      additionalParameters += '--main-dex-list=maindexlist.txt'
    }
    

    4)在插件中增加maindexlist.txt文件,里面包括要将哪些文件保留在主dex中。如:

    ljg/aaa/a.class
    ljg/aaa/b.class
    

    技巧:可以使用脚本生成maindexlist.txt文件,扫描插件项目的src/main/java/目录下的所有Java文件,将文件后缀java替换为class,然后填充到maindexlist.txt。
    问题:使用上述技巧,导致匿名内部类放在classes2.dex中。
    解决:预先为插件中的每个类,生成10个内部类。【因为内部类的命令是有规律的,User$1,User$2,......】

    5)如果midlib中有A,B,C三个类,而宿主中只用到了A,B两个类,插件中用到了C类,那么在宿主混淆时会将C类移除。所以,需要在插件和宿主的proguard-rule.pro中增加-dontshrink这样在混淆过程中即使没有用到的类也会保留。

    6)对插件打一个混淆包,会生成一个mapping.txt文件,里面含有midlib库中类的对应关系。将其中的这部分规则复制保存到mapping_plugin.txt中,并复制到宿主根目录下,与proguard-rule.pro平级。然后对宿主proguard-rule.pro文件中增加-applymapping mapping_plugin.txt

    7)移除插件中冗余的dex,用一个空的classes2.dex替换插件中的classes2.dex。具体操作如下:
    A. 反编译。java -jar apktool.jar d --no-src -f plugin.apk 解压apk,这样才能替换apk里面的classes2.dex。
    B. 重新打包。java -jar apktool.jar b plugin
    C. 重新签名。jarsigner -verbose -keystore keystore.jks ......
    D. 对生成的签名包执行对齐操作。zipalign -v 4 plugin_sign.apk plugin_ok.apk

    可以把混淆公共库midlib这整套流程集成到gradle中。

    4.3 增量更新

    流程如下:
    1)通过bsdiff old.apk和new.apk生成patch.diff文件。
    2)宿主中添加libApkPatchLibrary.so,在加载插件之前, 使用PatchUtils.patch,将下发的patch.diff文件与现有的插件进行合并,生成new.apk,宿主加载该插件。

    问题:在App两个正式版本之间,可能会有多个插件版本,那么就需要维护多个增量包。有的用户插件升级到了3.0.0.2,而有的用户没有升级。
    解决:App根据自己的插件版本号,去服务端请求合适自己的增量包。

    4.4 so的插件化

    4.4.1 so知识的简介

    Android支持的三种CPU类型:x86、arm、mips。现在手机基本上都是arm,而arm又分为32位和64位。armeabi/armeabi-v7a是32位,其中armeabi是相当老的版本,缺少对浮点数计算的硬件支持。arm64-v8a是64位,主要用于Android5.0之后。

    问题:通常我们是生成多种CPU类型的so,然后放到jniLibs不同目录下。其实这是不必要的,因为arm体系是向下兼容的,比如:32位的so,是可以在64位系统上运行的。

    原理:Android启动App时都会创建一个虚拟机,Android64位系统加载32位的so或App时,会在创建一个64位虚拟机的同时还创建一个32位的虚拟机来兼容32位的App应用。

    结论:App中只保留一个armeabi-v7a版本的so就足够了。

    4.4.2 so的加载流程

    手机支持CPU的种类存放在abiList集合中,如有:arm64-v8a、armeabi-v7a、armeabi。按照此顺序变量jniLib目录,如果这个目录下有arm64-v8a子目录,并且里面有so文件,那么接下来将加载arm64-v8a下的所有so文件,就不再加载armeabi-v7a和armeabi中的so了。
    所以,32位的arm手机肯定能加载到armeabi-v7a下的so文件。而64位的arm手机,想要加载armeabi-v7a下的so文件,必须不能在arm64-v8a下方任何so文件,并且armeabi-v7a下必须有so文件。如果所有的so文件都是从服务器下发的,那么需要建一个简单的so文件,放在armeabi-v7a目录下占位。

    4.4.3 so的加载方法

    1)System.loadLibrary("ljg") 只能加载jniLibs目录下的so文件。【src/main/jniLibs与src/main/java平级】
    2)System.load方法,可以加载任意路径下的so文件,需要传入so文件的完整路径。

    ClassLoader与so的关系:
    classLoader = new DexClassLoader(dexpath, fileRelease.getAbsolutePath(), null, getClassLoader());
    其中,第三个参数null,是apk中so文件的路径。如果有多个so路径,用逗号连接成字符串。

    优化:动态加载so,把非及时需要的so由服务器下发来减小apk的体积。

    4.4.4 基于System.loadLibrary的so插件化

    宿主在解析每个插件时,为每个插件创建一个DexClassLoader,先解析出每个插件apk中的so文件,解压到某个位置,将其路径用逗号拼接成字符串,放到DexClassLoader构造函数的第三个参数中。这样宿主和插件中都可以通过System.loadLibrary("xxx")来加载各自src/main/jniLibs中的so文件。

    插件的DexClassLoader中包含so的路径了,所以插件中就可通过loadLibrary("xxx")来加载so。

    4.4.5 基于System.load的so插件化

    插件中的so,可以交给插件自己来处理,不必通过DexClassLoader。插件把自身的jniLibs下的so复制到某个位置,然后通过System.load(libPath + "/" + soFileName)动态加载。

    4.5 自定义Gradle

    1)自定义Gradle插件库的名字必须是buildSrc,还在buildSrc的build.gradle文件中配置:

    apply plugin: 'groovy'
    dependencies {
      compile gradleApi()
      compile localGroovy()
    }
    

    2)定义MyPlugin.groovy类

    public class MyPlugin implements Plugin<Project> {
      @Override
      void apply(Project project) {
        project.task('testXXX') << {
          println "hello gradle plugin"
          }
        }
    }
    

    3)创建自定义Gradle插件的入口,在buildSrc/resources/META-INF.gradle-plugins/下新建文件com.ljg.define.pluginTest.properties文件,在该文件中声明:

    implementation-class=com.ljg.MyPlugin
    

    4)在build.gradle文件中引用【注意引用的名称是入口的文件名】

    apply plugin: 'com.ljg.define.pluginTest'
    
    Extension动态设置

    在buildSrc目录中定义类MyExtension

    class MyExtension {
      String message
    }
    

    在上面2)定义的MyPlugin类中应用

    public class MyPlugin implements Plugin<Project> {
      @Override
      void apply(Project project) {
        project.extensions.create('ljgTestPlugin', MyExtension)
    
        project.task('testXXX') << {
          println project.ljgTestPlugin.message
          }
        }
    }
    

    创建了一个名为ljgTestPlugin的Extension,它的类型是MyExtension。在build.gradle文件中引用。【注意引入的名字是ljgTestPlugin】

    apply plugin: 'com.ljg.define.pluginTest'
    ljgTestPlugin {
      message = 'hello xxx'
    }
    
    afterEvaluate应用
    public class MyPlugin implements Plugin<Project> {
      @Override
      void apply(Project project) {
        project.afterEvaluate() {
          def preBuild = project.tasks['preBuild']
          preBuild.doFirst {
            println 'hook before preReleaseBuild'
          }
          preBuild.doLast {
            println 'hook after preReleaseBuild'
          }
        }
      }
    }
    

    preBuild、preDebugBuild、processReleaseResources、compileReleaseJavaWithJavac等等,这些都是App打包的原生Task。Gradle会先创建project的所有任务的有向图,然后调用project的afterEvaluate方法,所以当我们想获取preBuild这样的task时,就只能在afterEvaluate方法中获取。

    提示:可以学习gradle-small的源码来提升编写Gradle的能力。

    相关文章

      网友评论

          本文标题:BiBi - Android 插件化

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