Xposed 热更新的踩坑记录

作者: wuhtt | 来源:发表于2017-09-05 10:34 被阅读87次

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

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

    好累啊,欢迎赞赏。

    相关文章

      网友评论

      • bfa757213d57:loadPackageParam.packageName是什么原因啊
        wuhtt:包名,可以用来过滤hook与否

      本文标题:Xposed 热更新的踩坑记录

      本文链接:https://www.haomeiwen.com/subject/urkujxtx.html