BiBi - Android 插件化

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的能力。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345