开篇
动态加载技术,也叫插件化技术;在技术驱动型公司中扮演着相当重要的角色,当项目越来越庞大的时候,需要通过插件化来减轻应用的内存和 CPU 占用,还可以实现热插拔,即不在发布新版本的情况下更新某些模块;动态加载是一项很复杂的技术
我们很早开始就在 Android 项目中采用了动态加载技术,主要目的是为了达到让用户不用重新安装 APK 就能升级应用的功能(特别是 SDK项目),这样一来不但可以大大提高应用新版本的覆盖率,也减少了服务器对旧版本接口兼容的压力,同时如果也可以快速修复一些线上的 BUG
这种技术并不是常规的 Android 开发方式,早期并没有完善的解决方案
动态加载技术的原理与实现
动态加载技术原理
Android 动态加载技术基本原理:在程序运行时加载一些外部的可执行的文件,然后调用这些文件的某个方法执行业务逻辑;因为文件是可执行的,出于安全问题,Android 并不允许直接加载手机外部存储这类 noexec(不可执行)存储路径上的可执行文件
在 Android 应用中调用它们前:要把这些可执行文件拷贝到 data/packagename/ 内部储存文件路径,确保库不会被第三方应用恶意修改成拦截,然后再将这些可执行文件加载到当前的运行环境并调用需要的方法执行相应的逻辑,从而实现动态调用
动态加载技术如何实现?
首先需要获得想要动态加载的可执行文件:通过 JDK 的编译命令 javac 把 Java 代码编译成 .class 文件,再使用 jar 命令把 .class 文件封装成 .jar 文件,再用 Android SDK 的 DX 工具把 .jar 文件优化成 .dex 文件,即需要的可执行文件
与 JVM不 同,Android 的虚拟机不能用 ClassLoader 类直接加载 .dex,而是需要用 DexClassLoader 类;DexClassLoader 类是 ClassLoader 类的子类,可以加载 jar/apk/dex,可以从 SD 卡中加载未安装的 apk;但 DexClassLoader 并不能直接加载外部存储的 .dex 文件,而是要先拷贝到内部存储里
调用成功后,就可以成功从外部路径动态加载一个 .dex 文件,并执行里面的代码逻辑;但是还不能直接启动插件(指经过处理的 dex 或者 apk)的 Activity
- Activity 等组件需要在 Manifest 中注册后才能以标准 Intent 的方式启动,通过 ClassLoader 加载并实例化的 Activity 实例只是一个普通的 Java 对象,能调用对象的方法,但是它没有生命周期,而且 Activity 等系统组件是需要 Android 的上下文环境的(Context 等资源)
没有这些东西 Activity 根本无法工作想要使用插件里的 Activity 需要解决两个问题:如何使插件 APK 里的 Activity 具有生命周期;如何使插件 APK 里的 Activity 具有上下文环境(使用 R 资源)
- 首先要处理插件 Activity 的生命周期,因为一个 Activity 的启动,如果不采用标准的 Intent 方式,没有经历过 Android 系统 Framework 层级的一系列初始化和注册过程,它的生命周期方法是不会被系统调用的
- 可以通过在主项目里创建一个 ProxyActivity,再由它去代理调用插件 Activity 的生命周期方法;用 ProxyActivity(一个标准的 Activity 实例)的生命周期同步控制插件 Activity 的生命周期
同步的方式既可以在 ProxyActivity 生命周期里用反射调用插件 Activity 相应生命周期的方法,又可以把插件 Activity 的生命周期抽象成接口
- 在 ProxyActivity 的生命周期里调用,然后在插件 Activity 里使用 R 资源;因为 res 里的每一个资源都会在 R.java 里生成一个对应的 Integer 类型的 id,APP 启动时会先把 R.java注册到当前的上下文环境,在代码里以 R 文件的方式使用资源时正是通过使用这些 id 访问 res 资源,然而插件的 R.java 并没有注册到当前的上下文环境,所以插件的 res 资源也就无法通过 id 使用
- 想要解决此问题,可以通过获取一个 AssetManager 实例,使用其 “addAssetPath” 方法加载 APK 里的资源,再使用 DisplayMetrics、Configuration、CompatibilityInfo 实例一起创建所需要的 Resources 实例
动态加载的实现方式
简单动态加载模式
通过 JDK 的编译命令 javac 把 jiava 代码编译成 .class 文件,再使用 jar 命令把 .class 文件封装成 .jar 文件;最后使用 Android SDK 的 DX 工具把 .jar 文件优化成 .dex 文件
通过 DexClassLoader 加载后使用反射或者接口方式调用里面的方法:
private void getOutData(){
File optimizadDexOutputPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"test_dexloafer.jar");
// 无法直接从外部路径加载.dex文件,需要指定APP内部路径作为缓存目录(.dex文件会被解压到此目录)
File dexOutputDir = this.getDir("dex",0);
//构造类加载器
DexClassLoader dexClassLoader = new DexClassLoader(optimizadDexOutputPath.getAbsolutePath(),dexOutputDir.getAbsolutePath(),null,ClassLoader.getSystemClassLoader());
}
使用反射的方式
使用 DexClassLoader 加载进来的类无法直接调用,可以通过反射的方式调用:
private void getOutData(){
File optimizadDexOutputPath = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"test_dexloafer.jar");
File dexOutputDir = this.getDir("dex",0);
DexClassLoader dexClassLoader = new DexClassLoader(optimizadDexOutputPath.getAbsolutePath(),dexOutputDir.getAbsolutePath(),null,ClassLoader.getSystemClassLoader());
Class libProviderClazz = null;
try {
libProviderClazz = dexClassLoader.loadClass("包名.类名");
//遍历所有的方法
Method[] methods = libProviderClazz.getDeclaredMethods();
for (int i = 0;i<methods.length;i++){
Log.e("test",methods[i].toString());
}
//通过方法名获取func方法
Method func= libProviderClazz.getDeclaredMethod("func");
//外部可以调用
func.setAccessible(true);
//调用该方法获得值
String string = (String) func.invoke(libProviderClazz.newInstance());
Toast.makeText(this, string, Toast.LENGTH_SHORT).show();
}catch (Exception e){
e.printStackTrace();
}
}
使用接口的方式
把 .dex 文件的方法抽象成公共接口,把这些接口复制到主项目中,通过接口的调用实现动态加载:
pulic interface IFunc{
public String func();
}
// 调用
IFunc ifunc = (IFunc)libProviderClazz;
String string = ifunc.func();
Toast.makeText(this, string, Toast.LENGTH_LONG).show();
使用代理 Activity 模式
使用插件 apk 里的 Activity 需要解决两个问题:
- 如何使插件apk里面的Activity具有生命周期
主项目 apk 注册一个空壳的 ProxyActivity,通过在 ProxyActivity 的生命周期里面同步调用插件中的 Activity 的生命周期方法,从而实现执行插件 apk 的业务逻辑
- 如何使插件apk里的Activity具有上下文环境:
插件里需要用到的新资源都是通过 java 代码的方式创建(xml 布局,动画,点九图);可以通过 AsserManager 实例创建 Resource 实例:
public int getAppNameResId(String apkPath){
PackageManager packageManager = this.getPackageManager();
PackageInfo info = packageManager.getPackageArchiveInfo(apkPath,0);
return info.applicationInfo.labelRes;
}
private Resources getExtResource(Context context,String apkPath){
int mDexPath = getAppNameResId(apkPath);
Resources res = context.getResources();
try {
Class<?> assetClass = Class.forName("android.content.res.AssetManager");
Object assetManager = assetClass.getConstructor(null).newInstance(null);
assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
addAssetPath.invoke(assetManager,mDexPath);
Class<?> resClass = Class.forName("android.content.res.Resources");
res = (Resources) resClass.getConstructor(assetClass,res.getDisplayMetrics().getClass(),res.getConfiguration().getClass())
.newInstance(assetManager,res.getDisplayMetrics(),res.getConfiguration());
}catch (Exception e){
e.printStackTrace();
}
return res;
}
结语
本文分析了 Android 动态加载的原理、过程以及实现方式;解决了 Android 动态加载中遇到的如何使插件 APK 里的 Activity 具有生命周期、如何使插件 APK 里的 Activity 具有上下文环境(使用 R 资源)等关键问题
如若再次遇到 Android APK 动态加载相关的系列问题怎么做?
四个字:防微杜渐;很多 Android 性能方面的问题都不是一朝一夕产生的,因为在开发的过程中,是需要不断的提升代码的质量,所以开发人员就必须要提高自己的开发水平,发现了问题,就要及时的解决
综上所述,针对 Android APK 动态加载相关的系列的问题,特向大家分享一份 Android 性能调优手册
手册内容围绕:启动优化、渲染优化、内存优化、网络优化、卡顿检测与优化、耗电优化、安装包体积优化、安全问题等方面进行展开并都融入进实践项目中来进行详细讲解,有需要这份手册的朋友:可以点击这里查看获取方式 或者简信发送 "进阶" 即可 直达获取,觉得手册内容有用的话,可以帮忙转发分享一下,点个赞
内容展示如下:
Android 性能优化思维导图

高清版 Android 性能优化 思维导图 获取方式:简信发送 “架构图” 即可 直达获取
储存优化
- Android 存储方式
- SharedPrefence
- 多进程问题 -> mmkv
- 已经有 xml/json,为什么要用 protobuf
- 项目中使用 protobuf
- 语言规范
- 字段约束
- 编码协议

APK 瘦身方案
- 瘦身原因
- APK 组成
- 代码瘦身
- 代码混淆
- 三方库处理
- 移除无用代码
- 资源瘦身
- 冗余资源
- 图片处理
- 资源混淆
- SO 瘦身
- SO 移除
- 动态加载 SO

由于篇幅原因,手册的部分内容就展示到这里了,有需要这份 Android 性能调优手册 的朋友:可以简信发送 ”进阶“ 即可 直达获取
改变人生,没有什么捷径可言,这条路需要自己亲自去走一走,只有深入思考,不断反思总结,保持学习的热情,一步一步构建自己完整的知识体系,才是最终的制胜之道,也是程序员应该承担的使命
Android 架构师之路还很漫长,与大家一同共勉
网友评论