Android APP修改dex文件实现插件加载
前言
首先,你可能看到的是一篇假的插件化方案。因为Android开发发展到现在出现了很多插件化的方案,但是目前还没发现哪个方案是通过修改插件的dex文件来实现的,估计很大可能性是别人在研究这个方向的时候发现是条死路吧。本人研究这个完全是出于好奇心,想看看这条路上到底有多少坑。
原理
其实原理很简单,插件化要迈的第一道坎,就是解决插件内四大组件的启动问题(主要是activity,以下就用activity代替四大组件)。因为Activity要通过AMS来启动,AMS要去PMS那边查询Activity的相关信息,PMS的信息来自manifest。我们可以先在宿主的manifest内预埋一些Activity声明,比如com.abc.pluginActivity。然后加载插件时读取插件的manifest获知插件的Activity,比如com.xxx.xxxActivity。然后我们修改插件的classes.dex,将里面的com.xxx.xxxActivity改成com.abc.pluginActivity。这样插件的Activiy就在宿主的manifest内有声明了。
当然,这么简单粗暴肯定是不行的,还要解决context问题,包名访问权限问题等。但是我们有了修改dex的能力,这些还是有办法解决的,这个在后面细说。
优势和劣势
据我所知,现在主流的插件化方案,一种是用hook的方式将activity偷梁换柱(例如DroidPlugin),另一种是在编译阶段将插件的manifest声明都merge到宿主工程。早期还有用代理方式实现等方式。这里还要特别提一下非主流的黑科技,动态字节码实现插件化android-pluginmgr。
用hook的方式兼容性要稍微差些,可能在某些厂商修改过的ROM会运行出错。Merge插件manifest的方式则对于插件新增的四大组件可能就无法处理了。
本文的方案不需要merge插件的manifest,也不需要hook ActiviyThread。当然劣势也很多,一是修改Activiy类名包名可能会导致插件一些代码出问题,比如反射、getClassName之类。二是插件安装效率很低,这种方式需要重新生成整个dex文件,时间很长,不过可以在插件编译期进行修改来避免。还有就是,不知道有多少坑要填……
既然这样,为什么还要研究? 因为之前的方案,对于Android N混合编译的支持有些问题,参考Android N混合编译与对热补丁影响解析(这是热补丁的,插件化也同理)。简单来说,Android N在运行时会将部分“热代码”缓存起来,然后编译成Native代码,提升下次运行效率。程序使用loaddex动态加载的代码是无法享受混合编译带来的好处的,但是如果采用类似MultiDex的方案,缓存的代码跟其依赖和被依赖的代码无法同步更新,就会引发问题。本文的方案理论上是支持Android N的(现在并没有实现……),相当于对插件进行新的一次混淆,让缓存的代码无法命中(需要在宿主manifest预埋足够多的Activiy)。
代码实现
说了这么多,还是“Show me the code”吧。这里只贴一些关键部分代码。详细请到github, Junhua102/Chameleon
dex文件操作库采用dexlib2。
第1步:插件加载器初始化
调用PluginLoader.init
初始化就是创建内部存储路径,然后用FrameworkClassLoader将Application conetext的ClassLoader替换掉。FrameworkClassLoader用于将来加载插件的类。
/**
* 初始化
* @param context 宿主application context
*/
public void init (Context context) {
mContext = context;
pluginParentClassLoader = ClassLoader.getSystemClassLoader().getParent();
File optimizedDexPath = context.getDir(Globals.PRIVATE_PLUGIN_ODEX_OUTPUT_DIR_NAME, Context.MODE_PRIVATE);
odexOutputPath = optimizedDexPath.getAbsolutePath();
pluginInternalStoragePath = context.getDir(
Globals.PRIVATE_PLUGIN_OUTPUT_DIR_NAME, Context.MODE_PRIVATE
);
// 替换ClassLoader
try {
Object mPackageInfo = ReflectionUtils.getFieldValue(context,
"mBase.mPackageInfo", true);
mFrameworkClassLoader = new FrameworkClassLoader(
context.getClassLoader());
// set Application's classLoader to FrameworkClassLoader
ReflectionUtils.setFieldValue(mPackageInfo, "mClassLoader",
mFrameworkClassLoader, true);
} catch (Exception e) {
e.printStackTrace();
}
}
第2步:插件安装
调用PluginLoader.install
2.1读取插件manifest信息
先调用PluginManifestUtil.setManifestInfo获取插件apk的manifest信息,保存到PlugInfo。
PlugInfo info = new PlugInfo();
info.setId(pluginApk.getName());
String pluginApkPath = pluginApk.getAbsolutePath();
//Load Plugin Manifest
try {
PluginManifestUtil.setManifestInfo(context, pluginApkPath, info, getPluginLibPath(info, pluginInternalStoragePath));
} catch (Exception e) {
throw new RuntimeException("Unable to create ManifestInfo for "
+ pluginApkPath + " : " + e.getMessage());
}
2.2对插件dex进行重构
先将PlugInfo中保存的插件Activity读取出来,给它们各自分配一个预设的Activity名称,保存在RefactorItem。然后将RefactorItem列表refactorItems传递给ApkRefactor.refactoring进行重构,重构完成后生成新的apk,保存到newApkFile。
// 搜索所有activity,并设置对应的替换名称
int activityIndex = 0;
List<ApkRefactor.RefactorItem> refactorItems = new ArrayList<>();
for (ResolveInfo resolveInfo : info.getActivities()) {
String pluginActivityName = "me.dreamheart.demo.PluginActivity" + activityIndex;
refactorItems.add(new ApkRefactor.RefactorItem(resolveInfo.activityInfo.name, pluginActivityName));
resolveInfo.activityInfo.name = pluginActivityName;
activityIndex ++;
}
ApkRefactor.ApkInfo apkInfo = new ApkRefactor.ApkInfo(info.getPackageInfo().packageName);
// application可能会受到重构的影响
info.getPackageInfo().applicationInfo.className = ApkRefactor.getNewApplicationName(info.getPackageInfo().applicationInfo.className, refactorItems);
// 重构apk的保存路径
File newApk = new File(pluginInternalStoragePath, info.getPackageName() + ".apk");
// 删除以前生成的旧apk
newApk.delete();
String newApkFile = newApk.getAbsolutePath();
Trace.store("generate " + newApkFile);
// 开始重构
ApkRefactor.refactoring(pluginApkPath, apkInfo, refactorItems, newApkFile, pluginInternalStoragePath.getAbsolutePath());
2.3 dex文件重构
上一步调用ApkRefactor.refactoring将apk和其他信息传递过来,ApkRefactor.refactoring方法内将classes.dex解压出来,调用另一个版本的ApkRefactor.refactoring对dex数据进行重构。然后用新的dex文件跟原apk的其他文件一起打包成新的apk。
2.3.1 将同包名其他类的Field和Method由default改成public(private 和 protected的可以不用管),解决Activity改包名后无法访问的问题。
DexRewriter accessFlagRewriter = new DexRewriter(new RewriterModule() {
/**
* 将同包名其他类的Field由default改成public
* @param rewriters
* @return
*/
@Nonnull
@Override
public Rewriter<Field> getFieldRewriter(@Nonnull Rewriters rewriters) {
return new Rewriter<Field>() {
@Nonnull
@Override
public Field rewrite(@Nonnull Field value) {
// System.out.print(value.getName() + "\n");
for (RefactorItem refactorItem : refactorItems) {
if (refactorItem.isSamePackage(value.getDefiningClass()) &&
AccessFlagUtils.isDefault(value.getAccessFlags())) {
int accessFlag = AccessFlagUtils.changeToPublic(value.getAccessFlags());
return new ImmutableField(value.getDefiningClass(), value.getName(),
value.getType(), accessFlag, value.getInitialValue(), value.getAnnotations());
}
}
return value;
}
};
}
/**
* 将同包名其他类的Method由default改成public
* @param rewriters
* @return
*/
@Nonnull
@Override
public Rewriter<Method> getMethodRewriter(@Nonnull Rewriters rewriters) {
return new Rewriter<Method>() {
@Nonnull
@Override
public Method rewrite(@Nonnull Method value) {
// System.out.print(value.getName() + "\n");
for (RefactorItem refactorItem : refactorItems) {
if (refactorItem.isSamePackage(value.getDefiningClass()) &&
AccessFlagUtils.isDefault(value.getAccessFlags())) {
int accessFlag = AccessFlagUtils.changeToPublic(value.getAccessFlags());
return new ImmutableMethod(value.getDefiningClass(), value.getName(),
value.getParameters(), value.getReturnType(),accessFlag,
value.getAnnotations(), value.getImplementation());
}
}
return value;
}
};
}
});
DexFile rewriteDexFile = accessFlagRewriter.rewriteDexFile(dexBackedDexFile);
2.3.2将Activity改名,同时也将Activity的内部类改名。
DexRewriter typeRewriter = new DexRewriter(new RewriterModule() {
@Nonnull
@Override
public Rewriter<String> getTypeRewriter(@Nonnull Rewriters rewriters) {
return new Rewriter<String>() {
@Nonnull
@Override
public String rewrite(@Nonnull String value) {
// System.out.print(value + "\n");
for (RefactorItem refactorItem : refactorItems) {
if (value.equals(refactorItem.srcClassType)) {
return refactorItem.desClassType;
} else if (value.indexOf(refactorItem.srcSubClassTypePrefix) == 0) {
return value.replace(refactorItem.srcSubClassTypePrefix, refactorItem.desSubClassTypePrefix);
}
}
return value;
}
};
}
});
rewriteDexFile = typeRewriter.rewriteDexFile(rewriteDexFile);
2.3.3 这个循环有两个操作,一是发现同包名的类,将访问权限由default改成public。二是在Activity中注入attachBaseContext方法,这个方法中带了个hook,可以将带有插件资源的context放到Activity的mBase中,这个后面会讲到。
for (ClassDef classDef: fileWrapper.getClasses()) {
for (RefactorItem refactorItem : refactorItems) {
// 同包名的类,将访问权限由default改成public
if (refactorItem.isSamePackage(classDef.getType()) &&
AccessFlagUtils.isDefault(classDef.getAccessFlags())) {
ClassDefWrapper classDefWrapper = new ClassDefWrapper(classDef);
classDefWrapper.setAccessFlags(AccessFlagUtils.changeToPublic(classDef.getAccessFlags()));
classDef = classDefWrapper;
}
if (!refactorItem.isActivity)
continue;
String activityType = refactorItem.desClassType;
// 向Activity注入attachBaseContext的钩子代码
if (classDef.getType().equals(activityType)) {
ClassDefWrapper classDefWrapper;
if (classDef instanceof ClassDefWrapper) {
classDefWrapper = (ClassDefWrapper) classDef;
} else {
classDefWrapper = new ClassDefWrapper(classDef);
}
Method attachBaseContextMethod = InjectMethodBuilder.buildAttachBaseContextMethod(activityType);
classDefWrapper.addMethod(attachBaseContextMethod);
classDef = classDefWrapper;
}
}
dexPool.internClass(classDef);
}
这是注入代码的smali代码
# virtual methods
.method protected attachBaseContext(Landroid/content/Context;)V
.locals 1
.param p1, "newBase" # Landroid/content/Context;
invoke-static {p0, p1}, Lme/dreamheart/chameleon/Hook;->attachBaseContext(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v0
check-cast v0, Landroid/content/Context;
invoke-super {p0, v0}, Landroid/app/Activity;->attachBaseContext(Landroid/content/Context;)V
return-void
.end method
生成attachBaseContext方法的dex字节码的代码
2.4 创建资源、Classloader和Application
创建对应的AssetManager、PluginClassLoader和Application,将宿主context通过反射赋值给插件Application的mBase。这些都保存到PlugInfo中。
//Load Plugin Res
try {
AssetManager am = AssetManager.class.newInstance();
am.getClass().getMethod("addAssetPath", String.class)
.invoke(am, newApkFile);
info.setAssetManager(am);
Resources hotRes = context.getResources();
Resources res = new Resources(am, hotRes.getDisplayMetrics(),
hotRes.getConfiguration());
info.setResources(res);
} catch (Exception e) {
throw new RuntimeException("Unable to create Resources&Assets for "
+ info.getPackageName() + " : " + e.getMessage());
}
//Load classLoader for Plugin
PluginClassLoader pluginClassLoader = new PluginClassLoader(info, newApkFile, odexOutputPath
, getPluginLibPath(info, pluginInternalStoragePath).getAbsolutePath(), pluginParentClassLoader);
info.setClassLoader(pluginClassLoader);
ApplicationInfo appInfo = info.getPackageInfo().applicationInfo;
// 创建插件的Application
Application app = makeApplication(info, appInfo);
attachBaseContext(context, info, app);
info.setApplication(app);
第3步:运行插件
到目前为止,PlugInfo保存着这些信息:插件manifest,插件AssetManager,插件classloader,插件Application。运行的时候,先取出插件的初始Activity,然后告诉mFrameworkClassLoader从插件的classloader中加载类,接着创建一个PluginHook用于往插件的Activity中注入内容。再调用plugInfo.ensureApplicationCreated确保插件Application的onCreated函数得到调用,最后就可以startActivity了。
/**
* 启动插件
* start插件的默认activiy
* @param plugInfo 安装插件时生成的插件信息
*/
public void startPlugin (Context context, PlugInfo plugInfo) {
if (null == plugInfo)
return;
ActivityInfo activityInfo = plugInfo.getMainActivity().activityInfo;
if (activityInfo == null) {
throw new ActivityNotFoundException("Cannot find Main Activity from plugin.");
}
mFrameworkClassLoader.setPlugin(plugInfo);
Hook.sHookListener = new PluginHook(mContext, plugInfo);
plugInfo.ensureApplicationCreated();
Intent intent = new Intent();
try {
intent.setClass(context, plugInfo.getClassLoader().loadClass(activityInfo.name));
context.startActivity(intent);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
插件Activity钩子注入过程
前面我们在修改dex的时候,在插件Activity注入了attachBaseContext方法,方法内部会调用Hook.onAttachBaseContext,Hook会返回一个PluginContext给Activity,PluginContext内包含了插件的AssetManager和Classloader,Activity的super.attachBaseContext将这个PluginContext赋值给mBase,这样,插件就可以使用自身的资源和类了。
public class PluginHook implements Hook.HookListener {
private PlugInfo plugInfo;
private Context context;
public PluginHook(Context appContext, PlugInfo plugInfo) {
this.plugInfo = plugInfo;
context = appContext;
}
/**
* 插件activity调用onAttachBaseContext时,调用此函数
* @param activity 插件activity
* @param orgContext 原context
* @return PluginContext
*/
@Override
public Object onAttachBaseContext(Object activity, Object orgContext) {
return attachBaseContext((Activity)activity, (Context)orgContext);
}
/**
* 将插件的baseContext替换成PluginContext
* @param activity 插件activity
* @param orgContext 原context
* @return PluginContext
*/
private Context attachBaseContext(Activity activity, Context orgContext) {
return new PluginContext(context, plugInfo);
}
}
总结
现在对插件安装效率没有进行优化,安装速度极慢,请耐心等待……
目前只是实现了Activity的简单替换,做了一些简单测试。希望大家多多提意见,让这个项目将来能做的更加完善。
Github
本项目很多代码来自houkx/android-pluginmgr,感谢作者的奉献。