From:Android插件化开发指南
目录
- 预备知识
1.1 简介
插件化的用途
插件化的发展史
1.2 Binder原理
1.3 Activity工作原理
App启动流程 / App内部页面跳转
1.4 PMS
1.5 ClassLoader
1.6 反射
1.7 代理模式 - 插件化知识
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.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.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/目录下。
在gradle1.3版本之前是默认支持public.xml的,但之后不再支持了,所以要在build.gradle中添加相应任务。
应用:插件如何使用宿主中的固定资源?把宿主打包成jar包被各插件compileOnly,在插件中使用StringConstant.house_name
。StringConstant类是根据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都会对应一个插件中的静态广播。
宿主中占位的静态广播:
......
插件中定义的静态广播:
注意:同样需要把插件中的静态广播作为动态广播手注册到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 {
@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 {
@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 {
@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的能力。