Android中实现Dex和apk动态加载
一. 动态加载原理
android中可实现dex动态加载功能, 通过反射机制,替换系统内部的类加载器,使用自定义的类加载器,可实现动态加载最新的class, 此方法主要用途:
(1). 用于替换调系统内部实现内实现class;
(2). 实现bug热修复,服务器下发一个dex文件,客户端重启进程事加载新的class,从而声不知鬼不觉的"偷偷"修复bug.
二. Dex类动态加载
下面一使用自定义加载assets中的jar文件加以说明.
把org.apache.tools.zip.jar放到 android项目的 assets目录下;
(1) 加载自定义的dex jar,生成自定义的DexClassLoader
step1: 先把jar文件从assets中拷贝到data/data中方便使用, 生成mDexPath目录
step2: 预先建立优化后的dex输出目录optimizedDexOutputPath, 必要字段,必须是绝对路径,目录存在即可.
step3: 生成dex类加载器
@Param mDexPath: dex文件源目录
@Param librarySearchPath: 表示一类库的目录,通常在dex加载中传null即可, 在apk加载中传: mContext.getApplicationInfo().dataDir + "/lib";
@Param parent 表示父类加载器, 通常为mContext.getClassLoader(), apk加载时为 mContext.getClassLoader().getParent()
stDexClassLoader cl = new DexClassLoader(mDexPath, optimizedDexOutputPath, null, mContext.getClassLoader());
(2) 使用反射机制替换mContext.mPackageInfo中的mClassLoader字段,把他换成自定义的StubClassloader,在其中重写loadClass(String className) throws ClassNotFoundException:
@Override
public Class<?> loadClass(String className) throws ClassNotFoundException {
String realClassName = className;
ClassLoader stubloader = AssetsDexClassLoader.getClassLoader();
if (AssetsDexClassLoader.needCustomLoader(realClassName)&& null != stubloader) {
//加载自定义的ClassLoader中的类,从而实现hook或者劫持系统内部的class
Log.d(mTAG, "StubClassloader loading own ClassName: " + className + " from stubloader: " + stubloader);
return stubloader.loadClass(realClassName);
} else {
return super.loadClass(RealClassName);
}
}
(3). 使用动态加载方式加载jar中的类
(1) 把java目录中的类拷贝到 package com.hulk.android.util 中, 可以随意定义其他把哦哦名.
(2) 在自定义Application.attachBaseContext(Context context)中调用初始化:
@Override
protected void attachBaseContext(Context context) {
//动态加载Assets中的jar包
AssetsDexClassLoader.init(context);
}
(4). 直接使用ZipUtils中的线程函数, 或者自己写相关压缩解压功能函数.
dex加载的详细源代码请参考详细代码: https://download.csdn.net/download/zhanghao_Hulk/16294655
三. apk动态加载
manifest中需要提前声明apk中的四大组件, 而且必须运行在子进程;
eg: com.hulk.android.util:browser
1. 把asstes中的apk拷贝到data/data的file目录下 (下面核心代码中的mApkDirPath为子apk的apk文件路径(通常在/data/data/.....目录中));
2. DexClassLoader类加载器的基础实现与dex加载差不多,只是需要注意DexClassLoader的partent为 mContext.getClassLoader().getParent()
DexClassLoader cl = new DexClassLoader(mDexPath,
optimizedDexOutputPath.getAbsolutePath(), mContext.getApplicationInfo().dataDir + "/lib",
mContext.getClassLoader().getParent());
3. apk中的资源替换
启动子进程的activity等组件时, 子进程中的资源默认加载主应用的, 必须把apk中的相关资源和class替换掉里面进程中, 核心函数代码:
private void setCustomFields() {
Object mPackageInfo = null;
Application application = null;
try {
// 使用反射获取mContext的mPackageInfo对象
mPackageInfo = getField(mContext, "mPackageInfo");
Log.d(TAG, "reflect mPackageInfo Value: " + mPackageInfo);
// 使用反射替换mPackageInfo中的mClassLoader字段
setField(mPackageInfo, "mClassLoader", mClassLoader);
Log.i(TAG, "setCustomFields: Reflected to set mClassLoader: " + mClassLoader);
if (mPluginInfo == null) {
return;
}
//资源替换
//mApkDirPath为子apk的apk文件路径(通常在/data/data/.....目录中)
String apkPath = mApkDirPath.getAbsolutePath() + "/" + mPluginInfo.mApkFileName;
// 使用反射替换mPackageInfo的mResDir
setField(mPackageInfo, "mResDir", apkPath);
// 使用反射替换mPackageInfo的mResources:
//先置空,然后再通过getResources拿到解析后的资源
//原因:进程中牧人的mResources为主应用的资源, 有缓存机制,
//先设置为null,才能重新解析mResDir目录下的资源文件
setField(mPackageInfo, "mResources", null);
Object mActivityThread = getField(mPackageInfo, "mActivityThread");
Resources resources = null;
if (Build.VERSION.SDK_INT < 26) {
Method getResources = mPackageInfo.getClass().getDeclaredMethod(
"getResources", mActivityThread.getClass());
resources = (Resources) getResources.invoke( mPackageInfo, mActivityThread);
} else {
Method getResources = mPackageInfo.getClass().getDeclaredMethod("getResources");
resources = (Resources) getResources.invoke(mPackageInfo);
}
// resources为子apk的资源, 通过反射在设置为进程的资源字段
setField(mApplication.getBaseContext(), "mResources", resources);
Log.i(TAG, "setCustomFields: Reflected to set mResources: " + resources);
// application:
//生成目标application,替换进程中原来的application,执行目标application周期函数
try {
//mPluginInfo.mAppliactionName为Application的全路径, new出一个对象
application = (Application)Class.forName(mPluginInfo.mAppliactionName, true, mContext.getClassLoader()).newInstance();
//反射替换mPackageInfo中的mApplication字段为实际的application对象
setField(mPackageInfo, "mApplication", application);
setField(mApplication.getBaseContext(), "mOuterContext", application);
setField(mActivityThread, "mInitialApplication", application);
//把mActivityThread中的"mAllApplications"全部替换为目标application
ArrayList<Application> mAllApplications = (ArrayList<Application>) getField(
mActivityThread, "mAllApplications");
for (int i = 0; i < mAllApplications.size(); i++) {
//mApplication为主应用的application
if (mAllApplications.get(i) == mApplication) {
mAllApplications.set(i, application);
}
}
Log.i(TAG, "Finished to init application: " + mPluginInfo.mAppliactionName);
} catch (Exception e) {
e.printStackTrace();
Log.w(TAG, "setCustomFields: Init application error: " + e, e);
} finally {
//执行目标application的生命周期函数
Method attach = Application.class.getDeclaredMethod("attach",
Context.class);
attach.setAccessible(true);
if(application!=null){
Log.w(TAG, "Finally, Reflect to call attach() and onCreate() for " + application );
attach.invoke(application, mApplication.getBaseContext());
application.onCreate();
} else {
Log.e(TAG, "Finally, the application is null" );
}
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "setCustomFields error: " + e, e);
}
}
以上为apk动态加载的核心代码, 感兴趣的码友请留言. apk加载相对麻烦一下,详细代码
https://download.csdn.net/download/zhanghao_Hulk/20272210
4. 备注说明
(1) 缺陷:上述apk动态加载为简单的动态加载, 所有子apk的四大组件必须在宿主中声明,也就是把子应用的manifest的四大组件拷贝到宿主manifest中, 使用不是很方便,子apk的manifest有修改,就得拷贝变化部分.
优点: 但是不存在兼容问题. 现在的gradle打包可实现manifest自动合并,具体实现就该各位大神自行脑补喽.
(2) 其他动态加载框架(360的replugin), 那样在宿主中对四大组件进行"打桩", 先声明坑位, 也需要打包时进行代码注入; 这种实现相对比较复杂,存在兼容新问题, 需要考虑的东西比较多.