最近插件化,热修复又火了一阵,插件化和热修复基本实现原理都是靠ClassLoader,自己在业余之下也凑了一下热闹,呵呵,造一造自行车的轮子。
首先实现插件化,肯定就是要动态的访问里面的方法和资源了,其实对于已经安装的APK可以通过Context.createPackageContext创建Contxt,然后通过反射调用方法和获取资源,但是没有安装的APK,Context 就没有那么容易创建了。
创建一个没安装的APK的Context,其实只要把一个旧的Context包裹一下覆写一些方法就行了,但有一些细节需要注意。
1)ClassLoader创建
a.首先创建一个ClassLoader,方法如下
/**
*
* @param context 上下文
* @param src apk路径
* @param internalPath 解压classes.dex的位置(不会重复解压)
* @return ClassLoader
*/
private static ClassLoader buildClassLoader(Context context, String src,String internalPath) {
return new DexClassLoader(src, internalPath, internalPath + "/" + "lib", context.getClassLoader());
}
b. 以上代码中,如果有jni的so文件,需要拷贝对应ABI的so文件到internalPath/lib/目录下。
so 从APK对应ABI下解压文件的解压方法如下
/**
* copy suitable so files to destination dir
*
* @param apkPath apk 's path
* @param cpuAbi cpu abi
* @param dstDir destination dir
*/
private static void unZipSpecialJniLib(String apkPath, String cpuAbi, String dstDir) {
FileInputStream fileInputStream = null;
ZipInputStream zipInputStream = null;
try {
fileInputStream = new FileInputStream(apkPath);
zipInputStream = new ZipInputStream(fileInputStream);
ZipEntry zipEntry = null;
String mark = "lib/" + cpuAbi + File.separatorChar;
byte[] buffer = new byte[ * ];
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
String zipEntryName = zipEntry.getName();
if (zipEntryName.startsWith(mark) && !zipEntry.isDirectory()) {
String entryName = zipEntry.getName();
String zipFileName = entryName.substring(entryName.indexOf(mark) + mark.length());
String dstFiePath = dstDir + "/lib/" + zipFileName;
unZipItem(zipInputStream, buffer, dstFiePath);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fileInputStream != null) fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
try {
if (zipInputStream != null) zipInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static void unZipItem(ZipInputStream zipInputStream, byte[] buffer, String dstFiePath) {
File dstFile = new File(dstFiePath);
if (!dstFile.getParentFile().exists()) {
boolean r = dstFile.getParentFile().mkdirs();
if (!r) throw new RuntimeException(" create folder failed,during unzip "+dstFiePath);
}
if (dstFile.exists()) {
boolean r = dstFile.delete();
if (!r) throw new RuntimeException(" create folder failed,during unzip"+dstFiePath);
}
int count;
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(dstFile);
while ((count = zipInputStream.read(buffer)) > ) {
fileOutputStream.write(buffer, , count);
}
fileOutputStream.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fileOutputStream != null)
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
以上的cpu ABI 可以通过 android.os.Build.CPU_ABI,得到。
至此就Classloader创建完毕。现在可以通过反射去调用外部APK中的方法了 。值得注意的是 Classloader 是优先从系统以及当前应用的dex中找类。
2)AssetManager的创建
通过反射创建
代码如下
private static AssetManager buildAssetManager(String apkPluginPath) {
Class<?> assetManagerClass = AssetManager.class;
AssetManager assetManager = null;
try {
assetManager = (AssetManager) assetManagerClass.newInstance();
Method addPathMethod = assetManagerClass.getMethod("addAssetPath", String.class);
addPathMethod.setAccessible(true);
addPathMethod.invoke(assetManager, apkPluginPath);
} catch (Exception e) {
e.printStackTrace();
}
return assetManager;
}
3)ContextWrapper资源替换(重点)
看过开源dynamic-load-apk 源码的应该知道前两步,但是dynamic-load-apk的加载资源的话,一次只能加载一个,原理是每次切换到某一个插件的时候,就把相应的资源替换掉。如果每个插件new一个ContextWrapper 会怎样呢。实际上在加载资源还是会有问题,而问题的根源就是LayoutInflate中的它:
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
和ContextImp中的它
registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() {
public Object createService(ContextImpl ctx) {
return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext());
}});
解释一下,建立ContextWrapper替换资源后,虽然资源已经被替换了,但是
LayoutInflater却是用的旧的Context创建的LayoutInflater,因此需要创建一个新的LayoutInflater;创建方法如下
Class<?> clz = classLoader.loadClass("com.android.internal.policy.PolicyManager");
Method createMethod = clz.getDeclaredMethod("makeNewLayoutInflater", Context.class);
mLayoutInflater = createMethod.invoke(null, this);
4)最终ContextWrapper
package jx.cy.csh.PluginContextLoader;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.AssetManager;
import android.content.res.Resources;
import java.lang.reflect.Method;
/**
* Created by biluo on 2016/8/25.
*/
public class PluginContext extends ContextWrapper {
private AssetManager mAssetManager;
private ClassLoader mClassLoader;
private Resources mResources;
private Object mLayoutInflater;
public PluginContext(Context base, AssetManager assetManager , ClassLoader classLoader) {
super(base);
mClassLoader = classLoader;
mAssetManager = assetManager;
mResources = new Resources(assetManager, base.getResources().getDisplayMetrics(), base.getResources().getConfiguration());
createNewLayoutInflater(classLoader);
}
private void createNewLayoutInflater(ClassLoader classLoader) {
try {
Class<?> clz = classLoader.loadClass("com.android.internal.policy.PolicyManager");
Method createMethod = clz.getDeclaredMethod("makeNewLayoutInflater", Context.class);
mLayoutInflater = createMethod.invoke(null, this);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Resources getResources() {
return mResources;
}
@Override
public ClassLoader getClassLoader() {
return mClassLoader;
}
@Override
public AssetManager getAssets() {
return mAssetManager;
}
@Override
public Object getSystemService(String name) {
if (name != null && name.equals(LAYOUT_INFLATER_SERVICE)) {
return mLayoutInflater;
}
return super.getSystemService(name);
}
}
5)其他
a 插件入口实例
package jx.cy.csh.pluginsample;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
/**
* Created by biluo on 2016/8/30.
*/
public class Main {
private View mView;
static {
System.loadLibrary("jnitest");
}
public void main(Context context) {
mView = LayoutInflater.from(context).inflate(R.layout.main, null);
}
public native int plus(int a,int b);
public View getView() {
return mView;
}
}
说明: layout里面可以使用自定义控件,因为解析xml是用的新的Context。父控件和子控件如果有相同包名的自定义控件(包括jar)并且写在XML里面,可能会出现强制转换异常(之前使用Context.createPackage遇见过,这里尚未验证).