Xposed 热更新的踩坑记录

本文是我踩坑的记录,文末给出最后方案,简书没有目录,欢迎访问我的博客,阅读体验会更好哦。

写过 Xposed 模块的同学应该知道,每一次修改模块逻辑之后都得重启手机才会生效,而重启手机,即使是使用 Hot reboot 也还是最快要用2-3 分钟,这是很难受的。

所以本文的目的就是解决这个问题。

初见,修改逻辑不重启

在github上有这么一个项目:githubwing/HotXposed ,它是用两个 Android 项目来实现的,项目A是Xposed模块,项目B 是我们更新代码的地方,里面有供模块(项目A)调用的方法,在我们更新完代码之后,通过 gradle 生成 项目B dex文件,在通过 adb push 命令吧 dex 文件放到手机的sd卡上,然后在项目A中,加载这个dex 文件,通过反射调用 dex 也就是项目B里面的方法,修改逻辑后,只需要替换更新后的dex 文件即可达到热更新的目的。

代码如下:

public class HookUtil implements IXposedHookLoadPackage {
@Override public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam loadPackageParam)
throws Throwable {
if (!loadPackageParam.packageName.equals("com.wingsofts.zoomimageheader")) {
return;
}
XposedHelpers.findAndHookMethod("com.wingsofts.zoomimageheader.HomeActivity", loadPackageParam.classLoader,
"onCreate", Bundle.class, new XC_MethodHook() {
@Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
}
@Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
//Toast.makeText((Context) param.thisObject, "哈哈", Toast.LENGTH_SHORT).show();
DexFile dexFile = new DexFile(Environment.getExternalStorageDirectory()+"/classes.dex");
Class clazz = dexFile.loadClass("net.androidwing.hotfix.HotFix",loadPackageParam.classLoader);
clazz.getDeclaredMethod("invoke", Activity.class).invoke(null,(Activity)param.thisObject);
}
});
}
}

可以看到,加载的是classes.dex,怎么来的呢?通过 gradle 脚本:

def DEX_DIR = "/sdcard/"
def cmd = "${getAdbPath()}adb push ${project.getBuildDir().getPath()}/intermediates/transforms/dex/debug/folders/1000/1f/main/classes.dex $DEX_DIR"
cmd.execute()

他到项目的intermediates/transforms/dex/debug/folders/1000/1f/main/路径下拿 classes.dex ,最后 pus 到 sd卡。

这里要注意,我不知道作者是怎么成功的,我这边是会报这个错的:

open failed: EACCES (Permission denied)

原因是 没有读sd卡的权限,而且你给模块本身添加上权限是没有用的,那只是改变了应用本身的权限。这就尴尬了,经过我苦苦需找,找到这个Issues,稍微有点坑。

好吧,rovo89好帅。

wing思路很好,我们沿着wing的思路想想其他办法。我们可以这样完善一下,既然重点是dex 里面的方法,那能不能不要两个项目,一个项目,然后跑一下,不重启直接反射本项目的方法呢?毕竟新建项目挺烦的,而且这样就不用push到sd卡,因为我重新跑程序的过程中就把方法的代码更新了。说干就干:

DexFile dexFile = new DexFile("/data/app/com.example.wuht.hothot-1/base.apk");
Class clazz = dexFile.loadClass("com.example.wuht.hothot.HotFix", param.classLoader);
clazz.getDeclaredMethod("invoke", Activity.class).invoke(null, (Activity) lparam.thisObject);

其他都一样,就是dex 的获取不太一样,直接通过apk 来拿dex,重点是apk没有放在sd卡上。

看一下HotFix的内容:

public class HotFix {
public static void toa(Context context) {
Toast.makeText(context, "test1", Toast.LENGTH_SHORT).show();
}
}

还要多说一点 /data/app/com.example.wuht.hothot-1/base.apk这个地址在不同的版本的Android 上是不一样的,我见过两种情况

android 6.0 —> /data/app/com.example.wuht.hothot-1/base.apk

android 4.4.2 —> /data/app/com.example.wuht.hothot-1.apk

然后路径里面的-1有时会变成-2,具体是这样的但没有安装过的时候,安装之后是 -1;当已经安装过之后,如果原来是 -1,安装之后就是-2,如果原来是-2,安装之后就是-1

只要正确的拿到dex,基本就可以了。期间自然是报了很多错了,见过的没见过的都有,但记得是正确的拿到dex,想要的dex。

错过

上面这个还有局限,就是只能更新逻辑,如果我要更换hook的方法,那还得重新写 findAndHookMethod然后重启手机,也是挺烦的。

然后又找到了这个项目 liuyufei/hotposed ,然后一样的发现用不了。。。。发现了个小问题,跟作者说了一下,已经改了,但是这个我还是用不了。。。他这个比上面的那种方法要高端一点,我在使用的过程中是卡在了,比如有实现了接口A 的类A,我们通过 dex 找到类A,调用类A 的newInstance()方法 之后强转接口时候报错,有兴趣的可以试试。是个好项目。

还有就是可能以上两位作者在他们写项目的时候应该是,都可以读sd卡的,具体是哪一个版本的Xposedd,我也不知道,我这边三台不同平台的手机都不行。

这个不行,然后我又在看雪论坛看到这么一个帖子 ,这个大神的apk,他故意没有混淆加固,建议反编译看一下代码,它是 hook 了Application 的attach方法,以为每一个应用都会有一个 Application ,也都会走 attach,所以相当与一个总的入口了,然后对比包名,对比方法名。

如果是对应的包名he方法名的时候,就打印输出。其中我觉得他写的找方法参数的方式很值得学习,至少之前我是不知道可以这么玩的,之前都是写死的,给大家看一下:

可以看大图

可能看着是有点复杂,其实道理就是遍历所有参数,如果是我们想要的方法,就中断循环,按特定的的格式打印,代码里可能 Utils.paramLog(paramString, paramAnonymousMethodHookParam, localStringBuffer.toString());这句是写多了吧。

好像有点偏题啊,介绍给大家介绍这个工具,简单查看log是很用的,接着热更新说,能不能直接在我更改hook方法的时候,也不用重启手机?

注意到之前我们反射的方法是写在findAndHookMethod 的回调里面,能不能想办法写在外面,并把XC_LoadPackage.LoadPackageParam 这个类型的param当做参数给我们反射的函数,然后把 findAndHookMethod 的逻辑写在我们反射的方法里,我试过了,不行。如果哪位大哥成功了,可以@我一下,因为我在这里耗了很多的时间了。提示找不到XC_LoadPackage.LoadPackageParam 类。

最后解决方案

后来在一个晚上,我看到了这个项目 asiontang/XposedNoRebootModuleSample ,要是我早点见到这个就好了,省了我很多尝试,原理都差不多,通过反射动态地更新代码,反射的方式稍微有点不同,最重要的是,它的方法就是在模块类里面,真是想不通为啥之前没有想到,然后它本身只是支持4.0,我稍微改了一下,支持了5.0、6.0,其实就是改了一下路径,然后将就着把 handleInitPackageResources也做了相应的人更新处理,应该说关于热更新的问题已经解决了。不用重启、不用写死,这样写xopsed的模块比之前爽太多了。

最后给一下兼容版本:

private static String MODULE_PATH = null;
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam param) throws Throwable {
final String packageName = Module.class.getPackage().getName();
String filePath = String.format("/data/app/%s-%s.apk", packageName, 1);
if (!new File(filePath).exists()) {
filePath = String.format("/data/app/%s-%s.apk", packageName, 2);
if (!new File(filePath).exists()) {
filePath = String.format("/data/app/%s-%s/base.apk", packageName, 1);
if (!new File(filePath).exists()) {
filePath = String.format("/data/app/%s-%s/base.apk", packageName, 2);
if (!new File(filePath).exists()) {
XposedBridge.log("Error:在/data/app找不到APK文件" + packageName);
return;
}
}
}
}
final PathClassLoader pathClassLoader = new PathClassLoader(filePath, ClassLoader.getSystemClassLoader());
final Class<?> aClass = Class.forName(packageName + "." + Module.class.getSimpleName(), true, pathClassLoader);
final Method aClassMethod = aClass.getMethod("handleMyHandleLoadPackage", XC_LoadPackage.LoadPackageParam.class);
aClassMethod.invoke(aClass.newInstance(), param);
}
private void xLog(String content) {
XposedBridge.log("*******************************************************************************************************************************");
XposedBridge.log(content);
XposedBridge.log("----------------------------------------------------------------------------------------------------------------------");
}
@Override
public void handleInitPackageResources(XC_InitPackageResources.InitPackageResourcesParam resparam) throws Throwable {
final String packageName = Module.class.getPackage().getName();
String filePath = String.format("/data/app/%s-%s.apk", packageName, 1);
if (!new File(filePath).exists()) {
filePath = String.format("/data/app/%s-%s.apk", packageName, 2);
if (!new File(filePath).exists()) {
filePath = String.format("/data/app/%s-%s/base.apk", packageName, 1);
if (!new File(filePath).exists()) {
filePath = String.format("/data/app/%s-%s/base.apk", packageName, 2);
if (!new File(filePath).exists()) {
XposedBridge.log("Error:在/data/app找不到APK文件" + packageName);
return;
}
}
}
}
final PathClassLoader pathClassLoader = new PathClassLoader(filePath, ClassLoader.getSystemClassLoader());
final Class<?> aClass = Class.forName(packageName + "." + Module.class.getSimpleName(), true, pathClassLoader);
final Method aClassMethod = aClass.getMethod("handleMyInitPackageResources", XC_InitPackageResources.InitPackageResourcesParam.class);
aClassMethod.invoke(aClass.newInstance(), resparam);
}
@Override
public void initZygote(StartupParam startupParam) throws Throwable {
MODULE_PATH = startupParam.modulePath;
}
public void handleMyHandleLoadPackage(final XC_LoadPackage.LoadPackageParam loadPackageParam) {
}
public void handleMyInitPackageResources(XC_InitPackageResources.InitPackageResourcesParam resparam){
}

最后,建议将一些常用的套路写成Android Studio的模板,不用每一次都复制粘贴,很烦的。

好累啊,欢迎赞赏。