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的模板,不用每一次都复制粘贴,很烦的。

好累啊,欢迎赞赏。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,932评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,554评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,894评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,442评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,347评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,899评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,325评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,980评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,196评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,163评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,085评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,826评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,389评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,501评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,753评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,171评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,616评论 2 339

推荐阅读更多精彩内容