组件化
拆成多个 module 开发就是组件化
插件化
App 的部分功能模块在打包时并不以传统方式打包进 apk 文件中,而是以另⼀种形式⼆次封装进 apk内部,或者放在网络上适时下载,在需要的时候动态对这些功能模块进行加载,称之为插件化。这些单独⼆次封装的功能模块 apk ,就称作「插件」,初始安装的 apk 称作「宿主」。插件化是组件化的更进⼀步推进。
插件化步骤:
主Apk分析:
- 主App打包完成后,会形成dex,image,xml资源
- Dex靠PathClassLoader加载
- 图片以及xml资源靠Resource加载
代码实现:
- 创建DexClassLoader加载插件代码
- 创建Resource加载资源文件
- 管理插件Activity生命周期
插件化基础:反射
-
使用反射的目的:
Java 既然提供了可见性关键字 public private 等等,用来限制代码之间的可见性。可见性特性的支持不是为了代码不被坏⼈使用,而是是为了程序开发的简洁性。可见性的支持提供的是 Safety 的安全,而不是 Security 的安全。即,可见性的支持让程序更不容易写出 bug,而不是更不容易被人入侵。反射的支持可以让开发者在可见性的例外场景中,可以突破可见性限制来调⽤自己需要的 API。这是基于对开发者「在使⽤反射时已经足够了解和谨慎」的假设的。所以,可见性的支持不是为了防御外来者⼊侵,因此反射功能的⽀持并没有什么不合理。
总结一句话:反射可以让程序员抛开代码的限制,任意使用各种方法。
try {
Class clz = Class.forName("com.king.reflect.Apple");
Constructor constructor = clz.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Object object = constructor.newInstance();
Method method = clz.getDeclaredMethod("setPrice", int.class);
method.setAccessible(true);
method.invoke(object, 4);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
插件化原理:动态加载
通过自定义 ClassLoader 来加载新的 dex 文件,从而让程序员原本没有的类可以被使用,这就是插件化的原理。
-
关于 DEX:
class: java编译后的文件,每个类对应⼀个class文件
dex: Dalvik EXecutable 把class打包在⼀起,⼀个dex可以包含多个class文件
odex: Optimized DEX针对系统的优化,例如某个方法的调用指令,会把虚拟的调用转换为使用具体的 index,这样在执行的时候就不用再查找了
oat: Optimized Android file Type。使用AOT策略对dex预先编译(解释)成本地指令,这样再运行阶段就不需再经历⼀次解释过程,程序的运行可以更快
AOT: Ahead-Of-Time compilation预先编译
File dexfile = new File(getCacheDir() + "/plugin.dex");
if (!dexfile.exists()) {
try {
InputStream is = getAssets().open("apk/dex/plugin.dex");
int size = is.available();
byte[] buffer = new byte[size];
is.read(buffer);
is.close();
FileOutputStream fos = new FileOutputStream(dexfile);
fos.write(buffer);
fos.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
try {
DexClassLoader classLoader = new DexClassLoader(dexfile.getPath(), getCacheDir().getPath(), null, null);
Class pluginUtilsClass = classLoader.loadClass("com.king.reflect.Apple");
Constructor utilsConstructor = pluginUtilsClass.getDeclaredConstructors()[0];
Object utils = utilsConstructor.newInstance();
Method shoutMethod = pluginUtilsClass.getDeclaredMethod("sell");
shoutMethod.invoke(utils);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
DexClassLoader构造方法参数说明:
dexPath:apk/dex/jar文件路径
optimizedDirectory:文件解压路径(这个路径下保存的是.dex文件不是.class)
libraryPath:加载时用到的so库(可能理解有问题)
parent:父加载器(这个比较重要与Android加载class的机制有关)
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
-
需要注册的组件(例如 Activity)如何打开
解决方式⼀:代理 Activity
解决方式⼆:欺骗系统
解决方式三:重写 gradle 打包过程,合并 AndroiManifest.xml
我也只是稍微了解方式一的方法。首先在宿主apk中留一个注册的ProxyActivity,生命周期的方法由RealActivity来实现,RealActivity通过插件化的反射来实现。
public class ProxyActivity extends Activity {
Object realActivity;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
realActivity.onCreate(savedInstanceState);
}
.....
}
-
资源文件如何加载
解决方式:自定义 AssetManager 和 Resources 对象,重写getResources() 和 getAssets() 方法。
public class ProxyActivity extends Activity {
@Override
public AssetManager getAssets() {
try {
Class assetManagerClass = AssetManager.class;
AssetManager assetManager = (AssetManager) assetManagerClass.newInstance();
Method addAssetPath = assetManagerClass.getDeclaredMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, "apkPath");
return assetManager;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return super.getAssets();
}
@Override
public Resources getResources() {
return new Resources(getAssets(), getResources().getDisplayMetrics(), getResources().getConfiguration());
}
}
-
插件化的作用
解决 dex 65535 问题。现在由 multidex 工具来专门解决。
减小安装包大小。
动态部署。
热更新
不安装新版本的软件,直接从网络下载新功能模块来对软件进行局部更新
- 热更新和插件化的区别
- 插件化的内容在原 App 中没有,而热更新是原 App 中的内容做了改动
- 插件化在代码中有固定的入口,而热更新则可能改变任何⼀个位置的代码
-
热更新的原理
ClassLoader 的 dex 文件替换
直接修改字节码 -
loadClass() 的类加载过程(双亲委托):是⼀个带缓存的、从上到下的加载过程(如下图所示):
灰色部分为缓存:
对于具体的⼀个 ClassLoader:
先从自己的缓存中取
自己没有缓存,就找父ClassLoader 要(parent.loadClass())
父ClassLoader也没有,就自己加载(findClass())BaseDexClassLoader 或者它的⼦类(DexClassLoader、 PathClassLoader 等)的findClass():通过 pathList.findClass(),它的 pathList.loadClass() 通过 DexPathList 的 dexElements 的 findClass()。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
//
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
所以热更新的关键在于,把补丁 dex 文件加载放进⼀个 Element,并且插入到 dexElements 这个数组的前面(插入到后面的话会被忽略掉)
-
热更新代码
因为无法在更新之前就指定要更新谁;所以不能定义新的 ClassLoader,而只能选择对ClassLoader 进行修改,让它能够加载补丁里面的类。因为补丁的类在原先的 App 中已经存在,所以应该把补丁的 Element 对象插入到 dexElements 的前面才行,插入到后面会被忽略掉。
具体的代码:
- 自己用补丁创建⼀个 PathClassLoader
- 把补丁 PathClassLoader 里面的 elements 替换到就的里面去, 尽早加载热更新(通用手段是把加载过程放在 Application.attachBaseContext())
try {
ClassLoader classLoader = getClassLoader();
Class loaderClass = BaseDexClassLoader.class;
Field pathListField = loaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathListObject = pathListField.get(classLoader);
Class pathListClass = pathListObject.getClass();
Field dexElementsField = pathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object dexElementsObject = dexElementsField.get(pathListObject);
// classLoader.pathList.dexElements = 热更新内容elements ;
PathClassLoader newClassLoader = new PathClassLoader(apk.getPath(), null);
Object newPathListObject = pathListField.get(newClassLoader);
Object newDexElementsObject = dexElementsField.get(newPathListObject);
//将newElement添加到Element[]的前面
int oldLength = Array.getLength(dexElementsObject);
int newLength = Array.getLength(newDexElementsObject);
Object concatDexElementsObject = Array.newInstance(dexElementsObject.getClass().getComponentType(), oldLength + newLength);
for (int i = 0; i < newLength; i++) {
Array.set(concatDexElementsObject, i, Array.get(newDexElementsObject, i));
}
for (int i = 0; i < oldLength; i++) {
Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObject, i));
}
dexElementsField.set(pathListObject, concatDexElementsObject);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}