githubwing / HotXposed

xposed hotfix/dynamic load/no reboot library
231 stars 39 forks source link

关于更加通用的热加载方案探讨 #4

Closed virjar closed 6 years ago

virjar commented 6 years ago

hi,关于xposed插件更新后需要重启手机的原因我做了调查。大概跟了跟源码,发现是因为xposed在开机的时候注册钩子函数,同时加载了插件apk,然后使用的是pathclassloader。pathclassloader内部有一个机制,就会遇到加载的apk的时候,会将dex复制到/data/dalvik-cache,所以我们重装了软件,这个钩子函数对应的dex仍然在缓存里面。 image 另外一个问题是,同一个软件,如果是覆盖安装,她的apk路径会发生变化,其规则如下:

        /data/app/com.virjar.xposedhooktool-1/base.apk
        /data/app/com.virjar.xposedhooktool-2/base.apk
        /data/app/com.virjar.xposedhooktool/base.apk

也就是说,及时不走缓存,因为新安装的apk的路径发生了改变,也会导致加载不到apk。这个原因是xposed是在系统启动的时候加载的插件apk,他就是使用这个路径注册到pathclassloader里面的,见代码

/**
     * Load a module from an APK by calling the init(String) method for all classes defined
     * in <code>assets/xposed_init</code>.摘抄自xposed源码,其中apk为插件apk路径,如 /data/app/com.virjar.xposedhooktool-1/base.apk
     */
    private static void loadModule(String apk, ClassLoader topClassLoader) {
        Log.i(TAG, "Loading modules from " + apk);
        DexFile dexFile;
        try {
            dexFile = new DexFile(apk);
        } catch (IOException e) {
            Log.e(TAG, "  Cannot load module", e);
            return;
        }

    //....省略代码
        ClassLoader mcl = new PathClassLoader(apk, XposedBridge.BOOTCLASSLOADER);
//....省略代码
                        if (moduleInstance instanceof IXposedHookLoadPackage)
                            XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));
//....省略代码
    }

当插件代码重装,apk路径由 /data/app/com.virjar.xposedhooktool-1/base.apk变化为/data/app/com.virjar.xposedhooktool-2/base.apk,由于这个 PathClassLoader仍然持有对 /data/app/com.virjar.xposedhooktool-1/base.apk的引用,同时 /data/app/com.virjar.xposedhooktool-1/base.apk的副本存在于/data/dalvik-cahe,所以并不会加载新代码,同时旧代码也能够正常的被执行(没有真正删除)

为了实现热加载,我们需要做到的是在插件触发的时候,不去加载缓存,而走我们自己的类加载器加载。看你也有一个类似的实现,但是这个还可以做得更加通用,因为只要插件运行,证明手机里面存在一份最新代码,所以我们可以找到自己的位置,然后使用新的classloader去加载自己。如下:

package com.virjar.xposedhooktool.hotload;

import android.app.Application;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;

import org.apache.commons.lang3.StringUtils;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dalvik.system.PathClassLoader;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

/**
 * XposedInit
 * <br/>
 * 请注意,该类是热加载入口,不允许直接访问工程其他代码,只要访问过的类,都不能实现热加载
 *
 * @author virjar@virjar.com
 */
public class XposedInit implements IXposedHookLoadPackage {

    private static final Pattern dexPath4ClassLoaderPattern = Pattern.compile("\\[zip file \"(.+)\"");

    @Override
    public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) {
        XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                hotLoadPlugin(lpparam.classLoader, (Context) param.args[0], lpparam);
            }
        });
    }

    private void hotLoadPlugin(ClassLoader ownerClassLoader, Context context, XC_LoadPackage.LoadPackageParam lpparam) {
        ClassLoader classLoader = XposedInit.class.getClassLoader();
        if (!(classLoader instanceof PathClassLoader)) {
            XposedBridge.log("classloader is not PathClassLoader: " + classLoader.toString());
            return;
        }

        PathClassLoader parentClassLoader = (PathClassLoader) classLoader;
        String classloaderDescription = parentClassLoader.toString();
        Matcher matcher = dexPath4ClassLoaderPattern.matcher(classloaderDescription);
        if (!matcher.find()) {
            XposedBridge.log("can not find plugin apk file location");
            return;
        }
        String pluginApkLocation = matcher.group(1);
        XposedBridge.log("find plugin location " + pluginApkLocation + ", use for new classloader");
        String packageName = findPackageName(pluginApkLocation);
        if (StringUtils.isBlank(packageName)) {
            XposedBridge.log("can not find mirror of apk :" + pluginApkLocation);
            return;
        }

        //find real apk location by package name
        PackageManager packageManager = context.getPackageManager();
        if (packageManager == null) {
            XposedBridge.log("can not find packageManager");
            return;
        }

        PackageInfo packageInfo = null;

        try {
            packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA);
        } catch (PackageManager.NameNotFoundException e) {
            //ignore
        }
        if (packageInfo == null) {
            XposedBridge.log("can not find plugin install location for plugin: " + packageName);
            return;
        }

        //hotClassLoader can load apk class && classLoader.getParent() can load xposed framework and android framework
        PathClassLoader hotClassLoader = new PathClassLoader(packageInfo.applicationInfo.sourceDir, classLoader.getParent());
        try {
            Class<?> aClass = hotClassLoader.loadClass("com.virjar.xposedhooktool.hotload.HotLoadPackageEntry");
            aClass.getMethod("entry").invoke(null, ownerClassLoader, hotClassLoader, context, lpparam, packageInfo.applicationInfo.sourceDir);
        } catch (Exception e) {
            XposedBridge.log(e);
        }
    }

    private static final Pattern apkNamePattern = Pattern.compile("/data/app/([^/]+).*");

    private String findPackageName(String apkLocation) {
        Matcher matcher = apkNamePattern.matcher(apkLocation);
        if (!matcher.matches()) {
            return null;
        }
        String candidatePackageName = matcher.group(1);
        //com.virjar.xposedhooktool-1
        //com.virjar.xposedhooktool-2
        //com.virjar.xposedhooktool
        matcher = Pattern.compile("(.+)-\\d+").matcher(candidatePackageName);
        if (matcher.find()) {
            return matcher.group(1);
        }
        return candidatePackageName;
    }
}

原理就是,插件是有xposed创建的classloader加载的,那么插件的class能够得到对已经能够的classloader,进而得到xposed创建插件class的时候传入的apk路径。进而通过class得到插件自己第一次安装的时候的apk路径 /data/app/com.virjar.xposedhooktool-1/base.apk,基于这个路径,我们能够分析出packagename,然后我们注册context的attached回调,使用context加载插件的package,得到插件最新的安装路径。然后使用pathclassloader加载她。

virjar commented 6 years ago

感谢作者,根据此思路实现,并测试通过,项目地址:https://gitee.com/virjar/xposedhooktool

githubwing commented 6 years ago

这个实现的思路都大同小异,感谢反馈~ 虚拟机缓存可以杀死宿主来解决。

virjar commented 6 years ago
  1. 还是有问题,实践发现,apk安装路径,有些自定义的系统,不在/data/app下面(建议使用ApkManager来获取apk安装路径,这个代码可以参考xposedBridge里面计算apk路径的方案)。
  2. 虚拟机缓存干不掉,因为他已经注入到所有的app下面了,而且是受精卵的时候就注入了。杀死宿主没用,除非杀死受精卵。但是杀死zygote就是软重启的意思了(xposed里面有一个软重启的功能,好像就是通过杀死受精卵的方式来实现的)
githubwing commented 6 years ago

第一点真实不同系统的 你说的对,是需要一种更加通用的方案,但是第二点 我这种做法杀死宿主即可,因为zygote注入的只是我的框架,而在框架内部动态加载的dex, 所以只需要杀死宿主让这次的动态加载类缓存失效即可

virjar commented 6 years ago

哦,那没有歧义。我只说的是xposed插件入口缓存无法通过杀死宿主解决