Xposed的新打開方式--Xpatch工做流程分析

1. Xpatch概述

Xpatch是一款利用重打包的方式,使得被處理的Apk啓動時自動加載Xposed模塊,來實現應用內Hook的工具。android

項目地址:https://github.com/WindySha/Xpatchgit

2. Xpatch處理apk分析

Xpatch修改apk,主要有三個步驟,代碼在MainCommand類的doCommandLine方法:github

protected void doCommandLine() {
    //...
    if (!disableCrackSignature) {
        // save the apk original signature info, to support crach signature.
        new SaveApkSignatureTask(apkPath, unzipApkFilePath).run();
    }
    FileUtils.decompressZip(apkPath, unzipApkFilePath);
    //...
    // 1. modify the apk dex file to make xposed can run in it
    mXpatchTasks.add(new ApkModifyTask(showAllLogs, keepBuildFiles, unzipApkFilePath, applicationName,
            dexFileCount));
    
    // 2. copy xposed so and dex files into the unzipped apk
    mXpatchTasks.add(new SoAndDexCopyTask(dexFileCount, unzipApkFilePath, getXposedModules(xposedModules)));
    
    // 3. compress all files into an apk and then sign it.
    mXpatchTasks.add(new BuildAndSignApkTask(keepBuildFiles, unzipApkFilePath, output));
    //...
    for (Runnable executor : mXpatchTasks) {
        executor.run();
    }
    //...
}

(1) 第一步web

在Xpatch的源碼中,第一步對應的是ApkModifyTask類,實現的是Runnable接口,它的任務是修改Dex文件,使得被處理的apk在啓動時可以執行指定的代碼。數組

若是反編譯被Xpatch處理過的Apk,查看App中Application的子類,會發現其中多瞭如下的代碼:架構

static {
    XposedModuleEntry.init();
}

咱們大膽的猜想,這就是Xpatch給注入進去的入口代碼。咱們回到Xpatch的源碼,來看看它是如何注入的。查看ApkModifyTask類,一步步進行跟蹤:app

ApkModifyTask類的run方法,在任務被啓動時調用,它的代碼:框架

public void run() {
    //...
    String targetDexFileName = dumpJarFile(dexFileCount, unzipApkFilePath, jarOutputPath, applicationName);
    //...
}

dumpJarFile方法:ide

private String dumpJarFile(int dexFileCount, String dexFilePath, String jarOutputPath, String applicationName) {
    //...
    boolean isApplicationClassFound = dex2JarCmd(filePath, jarOutputPath, applicationName);
    //...
}

繼續跟蹤到dex2JarCmd方法:函數

private boolean dex2JarCmd(String dexPath, String jarOutputPath, String applicationName) {
    Dex2jarCmd cmd = new Dex2jarCmd();
    String[] args = new String[]{
            dexPath,
            "-o",
            jarOutputPath,
            "-app",
            applicationName,
            "--force"
    };
    cmd.doMain(args);

    boolean isApplicationClassFounded = cmd.isApplicationClassFounded();
    if (showAllLogs) {
        System.out.println("isApplicationClassFounded ->  " + isApplicationClassFounded + "the dexPath is  " +
                dexPath);
    }
    return isApplicationClassFounded;
}

看到了它建立了一個com.googlecode.dex2jar.tools.Dex2jarCmd類實例,這個類在名爲dex-tools的外部庫裏,並調用了Dex2jarCmd的doMain方法,給他傳進去一些相似於命令行參數的東西,令咱們比較提得起精神的是-app參數,它傳進去一個applicationName,這個applicationName的值來自MainCommand類的doCommandLine方法,邏輯是從解壓的apk中讀取AndroidManifest.xml,並讀取application節點下的name屬性的值,最後將值賦予applicatioName

protected void doCommandLine() {
    //...
    ManifestParser.Pair pair = ManifestParser.parseManifestFile(manifestFilePath);
    String applicationName;
    if (pair != null && pair.applicationName != null) {
        applicationName = pair.applicationName;
    } else {
        System.out.println(" Application name not found error !!!!!! ");
        applicationName = DEFAULT_APPLICATION_NAME;
    }
    //...
}

exm?就這樣?並無發現任何注入的代碼啊,不急,繼續跟蹤,看到applicationName傳進去了,必定能跟蹤到有用的信息。接下來就是進入dex-tools外部庫了,代碼都是反編譯出來的

com.googlecode.dex2jar.tools.BaseCmd的doMain方法:

public void doMain(String... args) {
    try {
        this.initOptions();
        this.parseSetArgs(args);
        this.doCommandLine();
    } catch (BaseCmd.HelpException var4) {
        String msg = var4.getMessage();
        if (msg != null && msg.length() > 0) {
            System.err.println("ERROR: " + msg);
        }
        this.usage();
    } catch (Exception var5) {
        var5.printStackTrace(System.err);
    }
}

主要看doCommandLine方法,doCommandLine是個抽象方法,它的真正實現是在Dex2jarCmd類裏

protected void doCommandLine() throws Exception {
    //...
    for(var4 = 0; var4 < var3; ++var4) {
        //...
        BaseDexFileReader reader = MultiDexFileReader.open(Files.readAllBytes((new File(fileName)).toPath()));
        BaksmaliBaseDexExceptionHandler handler = this.notHandleException ? null : new BaksmaliBaseDexExceptionHandler();
        this.dex2jar = Dex2jar.from(reader);
        this.dex2jar.withExceptionHandler(handler)
        .reUseReg(this.reuseReg)
        .topoLogicalSort()
        .skipDebug(!this.debugInfo)
        .optimizeSynchronized(this.optmizeSynchronized)
        .printIR(this.printIR)
        .noCode(this.noCode)
        .skipExceptions(this.skipExceptions)
        .setApplicationName(this.applicationName)
        .to(file);
        //...
    }
}

跳轉到com.googlecode.d2j.dex.Dex2jar類的to方法

public void to(Path file) throws IOException {
    if (Files.exists(file, new LinkOption[0]) && Files.isDirectory(file, new LinkOption[0])) {
        this.doTranslate(file);
    } else {
        FileSystem fs = createZip(file);
        Throwable var3 = null;

        try {
            this.doTranslate(fs.getPath("/"));
        } catch (Throwable var12) {
            var3 = var12;
            throw var12;
        } finally {
            if (fs != null) {
                if (var3 != null) {
                    try {
                        fs.close();
                    } catch (Throwable var11) {
                        var3.addSuppressed(var11);
                    }
                } else {
                    fs.close();
                }
            }

        }
    }

}

to方法調用doTranslate方法

private void doTranslate(final Path dist) throws IOException {

        //...
        (new ExDex2Asm(this.exceptionHandler) {
            public void convertCode(DexMethodNode methodNode, MethodVisitor mv) {
                if (methodNode.method.getOwner().equals(Dex2jar.this.applicationName) && methodNode.method.getName().equals("<clinit>")) {
                    Dex2jar.this.isApplicationClassFounded = true;
                    mv.visitMethodInsn(184, "com/wind/xposed/entry/XposedModuleEntry", "init", "()V", false);
                }

                if ((Dex2jar.this.readerConfig & 4) == 0 || !methodNode.method.getName().equals("<clinit>")) {
                    super.convertCode(methodNode, mv);
                }
            }

            public void addMethod(DexClassNode classNode, ClassVisitor cv) {
                if (classNode.className.equals(Dex2jar.this.applicationName)) {
                    Dex2jar.this.isApplicationClassFounded = true;
                    boolean hasFoundClinitMethod = false;
                    if (classNode.methods != null) {
                        Iterator var4 = classNode.methods.iterator();

                        while(var4.hasNext()) {
                            DexMethodNode methodNode = (DexMethodNode)var4.next();
                            if (methodNode.method.getName().equals("<clinit>")) {
                                hasFoundClinitMethod = true;
                                break;
                            }
                        }
                    }

                    if (!hasFoundClinitMethod) {
                        MethodVisitor mv = cv.visitMethod(8, "<clinit>", "()V", (String)null, (String[])null);
                        mv.visitCode();
                        mv.visitMethodInsn(184, "com/wind/xposed/entry/XposedModuleEntry", "init", "()V", false);
                        mv.visitInsn(177);
                        mv.visitMaxs(0, 0);
                        mv.visitEnd();
                    }
                }

            }

            //...
        }).convertDex(fileNode, cvf);
    }

doTranslate方法很長,可是咱們很容易就能看到了很敏感的字符串:com/wind/xposed/entry/XposedModuleEntry,這就是Xpatch插入本身初始化的代碼的地方。visitMethodInsn方法用於在函數內插入一條指令,看到兩處調用visitMethodInsn來插入調用 com.wind.xposed.entry.XposedModuleEntry類的init方法的指令。

  • convertCode函數中的visitMethodInsn,邏輯是若是要處理的Application類中存在clinit方法,即存在靜態代碼段,就直接插入調用 com.wind.xposed.entry.XposedModuleEntry類的init方法的指令
  • addMethod函數中的visitMethodInsn,若是要處理的Application類中不存在clinit方法,即不存在靜態代碼段,就建立一個靜態代碼段,並在其中插入調用 com.wind.xposed.entry.XposedModuleEntry類的init方法的指令,最後返回void

注:上面的操做碼,184表明invoke-static,177表明return-void。這些操做碼定義在org.objectweb.asm.Opcodes類中。

到這裏,第一步咱們已經搞清楚了。

(2) 第二步

對應的是SoAndDexCopyTask類,從名字能夠看出它的任務是複製so和dex的,具體是怎樣的,咱們看代碼。

SoAndDexCopyTask類,它也實現了Runnable接口,run方法在任務被啓動時調用:

@Override
public void run() {
    copySoFile();
    copyDexFile(dexFileCount);
    deleteMetaInfo();
}

這個類主要就作這三個動做:複製so文件,複製dex文件,刪除Meta信息。

咱們先看copySoFile代碼:

private void copySoFile() {
    for (String libPath : APK_LIB_PATH_ARRAY) {
        String apkSoFullPath = fullLibPath(libPath);
        if(new File(apkSoFullPath).exists()) {
            copyLibFile(apkSoFullPath, SO_FILE_PATH_MAP.get(libPath));
        }
    }
    // copy xposed modules into the lib path
    if (xposedModuleArray != null && xposedModuleArray.length > 0) {
        int index = 0;
        for (String modulePath : xposedModuleArray) {
            modulePath = modulePath.trim();
            if (modulePath == null || modulePath.length() == 0) {
                continue;
            }
            File moduleFile = new File(modulePath);
            if (!moduleFile.exists()) {
                continue;
            }
            for (String libPath : APK_LIB_PATH_ARRAY) {
                String apkSoFullPath = fullLibPath(libPath);
                String outputModuleName= XPOSED_MODULE_FILE_NAME_PREFIX + index + SO_FILE_SUFFIX;
                if(new File(apkSoFullPath).exists()) {
                    File outputModuleSoFile = new File(apkSoFullPath, outputModuleName);
                    FileUtils.copyFile(moduleFile, outputModuleSoFile);
                }

            }
            index++;
        }
    }
}

看代碼能夠知道它的任務是把Xpatch.jar中assets目錄下的libxpatch_wl.so複製到apk解壓目錄的lib/<架構文件夾>下。這個libxpatch_wl.so是whale框架提供so文件,爲Hook提供可能。

除了複製so,若是咱們在用Xpatch時使用-xm參數來將Xposed模塊集成到apk中,那麼模塊會被就會被重命名成:以libxpatch_xp_module_爲前綴,後面接着模塊序號,最後再以so爲後綴。最終這個模塊被複制到apk的lib目錄下。

copyDexFile方法:

private void copyDexFile(int dexFileCount) {
    String copiedDexFileName = "classes" + (dexFileCount + 1) + ".dex";
    FileUtils.copyFileFromJar("assets/classes.dex", unzipApkFilePath + copiedDexFileName);
}

邏輯也很明瞭,把assets下的classes.dex複製到apk解壓目錄下,根據原來apk中的dex個數來給複製進去的dex重命名。

deleteMetaInfo方法:

private void deleteMetaInfo() {
    String metaInfoFilePath = "META-INF";
    File metaInfoFileRoot = new File(unzipApkFilePath + metaInfoFilePath);
    if (!metaInfoFileRoot.exists()) {
        return;
    }
    File[] childFileList = metaInfoFileRoot.listFiles();
    if (childFileList == null || childFileList.length == 0) {
        return;
    }
    for (File file : childFileList) {
        String fileName = file.getName().toUpperCase();
        if (fileName.endsWith(".MF") || fileName.endsWith(".RAS") || fileName.endsWith(".SF")) {
            file.delete();
        }
    }
}

沒什麼好說的,就是刪除<apk解壓目錄>/META-INF下的指定文件。

(3) 第三步

對應的是BuildAndSignApkTask類,從名字能夠看出它的任務是構建和對apk簽名的。

這個BuildAndSignApkTask類也是實現Runnable接口,咱們來看run方法:

public void run() {
    //...
    FileUtils.compressToZip(unzipApkFilePath, unsignedApkPath);
    //...
    signApk(unsignedApkPath, keyStoreFilePath, signedApkPath, false);
    //...
}

這個方法作了兩件重要的事,把apk解壓目錄給壓縮成zip,並給壓縮成的文件簽名,這裏就不細講了。

3. 被集成進apk中的dex分析

咱們在上面提到過,Xpatch把assets目錄下的classes.dex文件複製進了目標apk裏,這個dex是不開源的,那麼這個dex裏面究竟有什麼呢,咱們把dex解壓出來,拖進jadx中反編譯。

既然Xpatch將初始化代碼注入到應用的Application類,初始化代碼調用com.wind.xposed.entry.XposedModuleEntry類的init方法,那麼咱們從init方法開始看起。

public static void init() {
    if (b.compareAndSet(false, true)) {
        Context createAppContext = XpatchUtils.createAppContext();//1
        if (createAppContext == null) {
            Log.e(a, "try to init XposedModuleEntry, but create app context failed !!!!");
            return;
        }
        d = createAppContext;
        if (VERSION.SDK_INT > 21 && !FileUtils.isFilePermissionGranted(createAppContext)) {
            Log.e(a, "File permission is not granted, can not control xposed module by file ->xposed_config/modules.list");
        }
        XposedHelper.initSeLinux(createAppContext.getApplicationInfo().processName);
        SharedPrefUtils.init(createAppContext);
        ClassLoader classLoader = createAppContext.getClassLoader();
        b.a(createAppContext.getApplicationInfo(), classLoader);//2
        List<String> arrayList = new ArrayList();
        List<String> a = a(createAppContext);//3
        a(createAppContext, (List) arrayList);//4
        if (a.size() > 0) {
            String a2;
            String a3;
            List list = null;
            for (String a32 : arrayList) {
                if (list == null) {
                    list = new ArrayList();
                }
                a2 = a(createAppContext, a32);
                String str = a;
                StringBuilder stringBuilder = new StringBuilder("Current packed module path ----> ");
                stringBuilder.append(a32);
                stringBuilder.append(" packageName = ");
                stringBuilder.append(a2);
                XLog.d(str, stringBuilder.toString());
                list.add(a2);
            }
            if (list == null || list.size() == 0) {
                arrayList.addAll(a);
            } else {
                for (String str2 : a) {
                    a32 = a(createAppContext, str2);
                    a2 = a;
                    StringBuilder stringBuilder2 = new StringBuilder("Current installed module path ----> ");
                    stringBuilder2.append(str2);
                    stringBuilder2.append(" packageName = ");
                    stringBuilder2.append(a32);
                    XLog.d(a2, stringBuilder2.toString());
                    if (!list.contains(a32)) {
                        arrayList.add(str2);
                    }
                }
            }
        }
        for (String str3 : arrayList) {
            String absolutePath = createAppContext.getDir("xposed_plugin_dex", 0).getAbsolutePath();
            if (!TextUtils.isEmpty(str3)) {
                Log.d(a, "Current truely loaded module path ----> ".concat(String.valueOf(str3)));
                b.a(str3, absolutePath, createAppContext.getApplicationInfo(), classLoader);//5
            }
        }
    }
}

init方法代碼比較多,上面標註釋的地方是比較值得關注的,根據這些地方展開

註釋1: 這裏主要經過反射來建立Context,做爲這麼早執行的代碼,做者也經過很巧妙的方式建立了Context,有了Context後,不少事就好辦多了,XpatchUtils.createAppContext()的代碼以下:

public static Context createAppContext() {
    try {
        Class cls = Class.forName("android.app.ActivityThread");
        Method declaredMethod = cls.getDeclaredMethod("currentActivityThread", new Class[0]);
        declaredMethod.setAccessible(true);
        Object invoke = declaredMethod.invoke(null, new Object[0]);
        Field declaredField = cls.getDeclaredField("mBoundApplication");
        declaredField.setAccessible(true);
        Object obj = declaredField.get(invoke);
        Field declaredField2 = obj.getClass().getDeclaredField("info");
        declaredField2.setAccessible(true);
        obj = declaredField2.get(obj);
        Method declaredMethod2 = Class.forName("android.app.ContextImpl").getDeclaredMethod("createAppContext", new Class[]{cls, obj.getClass()});
        declaredMethod2.setAccessible(true);
        Object invoke2 = declaredMethod2.invoke(null, new Object[]{invoke, obj});
        if (invoke2 instanceof Context) {
            return (Context) invoke2;
        }
    } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException | NoSuchMethodException | InvocationTargetException e) {
        e.printStackTrace();
    }
    return null;
}

註釋2:調用com.wind.xposed.entry.b類的a方法,並將當前App的ApplicationInfo和ClassLoader傳過去,從這裏開始就開始碰到XposedBridge的代碼了

public static void a(ApplicationInfo applicationInfo, ClassLoader classLoader) {
    Wrapper wrapper = new Wrapper(a.a());
    CopyOnWriteSortedSet copyOnWriteSortedSet = new CopyOnWriteSortedSet();
    copyOnWriteSortedSet.add(wrapper);
    LoadPackageParam loadPackageParam = new LoadPackageParam(copyOnWriteSortedSet);
    loadPackageParam.packageName = applicationInfo.packageName;
    loadPackageParam.processName = applicationInfo.processName;
    loadPackageParam.classLoader = classLoader;
    loadPackageParam.appInfo = applicationInfo;
    loadPackageParam.isFirstApplication = true;
    XCallback.callAll(loadPackageParam);
}

方法第一行把a.a()傳給了Wrapper的構造函數,a類完整類名是com.wind.xposed.entry.a,該類實現IXposedHookLoadPackage接口,a靜態方法返回a類實例,那麼Wrapper的構造函數獲得的就是IXposedHookLoadPackage接口的類實例。接着Wrapper類實例被添加到一個CopyOnWriteSortedSet中,這個CopyOnWriteSortedSet類是一個操做Object數組的類,CopyOnWriteSortedSet被傳到LoadPackageParam類的構造函數中,調用這個構造函數就是在給它父類(Param類)中的callbacks字段賦值。

public static abstract class Param {
    public final Object[] callbacks;
    //...
    protected Param(CopyOnWriteSortedSet<? extends XCallback> copyOnWriteSortedSet) {
        this.callbacks = copyOnWriteSortedSet.getSnapshot();
    }
    //...
}

接下來就是給LoadPackageParam的字段賦值,這些字段存儲着當前應用包名,進程名,ApplicationInfo,ClassLoader等等信息。

com.wind.xposed.entry.b.a(ApplicationInfo applicationInfo, ClassLoader classLoader)方法的最後,調用XCallback類的callAll方法

public static void callAll(Param param) {
    if (param.callbacks != null) {
        int i = 0;
        while (true) {
            Object[] objArr = param.callbacks;
            if (i < objArr.length) {
                try {
                    ((XCallback) objArr[i]).call(param);
                } catch (Throwable th) {
                    XposedBridge.log(th);
                }
                i++;
            } else {
                return;
            }
        }
    }
    //...
}

callAll方法遍歷Param類中的全部callback,調用它們的call方法

public void call(Param param) {
    if (param instanceof LoadPackageParam) {
        handleLoadPackage((LoadPackageParam) param);
    }
}

饒了半天,就是調用傳進Wrapper類構造函數的類的handleLoadPackage方法,那就是調用com.wind.xposed.entry.a類的handleLoadPackage方法,而com.wind.xposed.entry.a類的handleLoadPackage方法又去調用com.wind.xposed.entry.a.a類的handleLoadPackage方法,那咱們去看com.wind.xposed.entry.a.a類的handleLoadPackage的實現

public final void handleLoadPackage(LoadPackageParam loadPackageParam) {
    Context a = XposedModuleEntry.a();
    String readTextFromAssets = FileUtils.readTextFromAssets(a, "xpatch_asset/original_signature_info.ini");
    Log.d("PackageSignatureHooker", "Get the original signature --> ".concat(String.valueOf(readTextFromAssets)));
    if (!(readTextFromAssets == null || readTextFromAssets.isEmpty())) {
        try {
            WhaleRuntime.reserved2();
            Class cls = Class.forName("android.app.ActivityThread");
            Object invoke = cls.getDeclaredMethod("currentActivityThread", new Class[0]).invoke(null, new Object[0]);
            Method declaredMethod = cls.getDeclaredMethod("getPackageManager", new Class[0]);
            declaredMethod.setAccessible(true);
            Object invoke2 = declaredMethod.invoke(invoke, new Object[0]);
            Object newProxyInstance = Proxy.newProxyInstance(Class.forName("android.content.pm.IPackageManager").getClassLoader(), new Class[]{r7}, new a(invoke2, loadPackageParam.packageName, readTextFromAssets));
            Field declaredField = cls.getDeclaredField("sPackageManager");
            declaredField.setAccessible(true);
            declaredField.set(invoke, newProxyInstance);
            PackageManager packageManager = a.getPackageManager();
            declaredField = packageManager.getClass().getDeclaredField("mPM");
            declaredField.setAccessible(true);
            declaredField.set(packageManager, newProxyInstance);
        } catch (Exception e) {
            Log.e("PackageSignatureHooker", " hookSignatureByProxy failed !!", e);
        }
    }
}

這個方法的做用是Hook相關的函數,將被處理的apk的簽名替換成原來的,防止某些App檢測到本身的Apk被修改。apk在被Xpatch處理以前,簽名的信息的被保存了下來,對應的任務類是SaveApkSignatureTask,上文沒有講到,感興趣能夠去看一下。

註釋3:調用本類中的a方法,這個方法的參數只有一個參數Context

private static List<String> a(Context context) {
    PackageManager packageManager = context.getPackageManager();
    ArrayList arrayList = new ArrayList();
    List a = a(true);
    final ArrayList arrayList2 = new ArrayList();
    boolean exists = new File(c, "xposed_config/modules.list").exists();
    for (PackageInfo packageInfo : packageManager.getInstalledPackages(128)) {
        ApplicationInfo applicationInfo = packageInfo.applicationInfo;
        if (applicationInfo.enabled) {
            Bundle bundle = applicationInfo.metaData;
            if (bundle != null && bundle.containsKey("xposedmodule")) {
                CharSequence charSequence = packageInfo.applicationInfo.publicSourceDir;
                String charSequence2 = context.getPackageManager().getApplicationLabel(packageInfo.applicationInfo).toString();
                if (TextUtils.isEmpty(charSequence)) {
                    charSequence = packageInfo.applicationInfo.sourceDir;
                }
                if (!TextUtils.isEmpty(charSequence) && (!exists || a == null || a.contains(applicationInfo.packageName))) {
                    XLog.d(a, " query installed module path -> ".concat(String.valueOf(charSequence)));
                    arrayList.add(charSequence);
                }
                arrayList2.add(Pair.create(packageInfo.applicationInfo.packageName, charSequence2));
            }
        }
    }
    new Thread(new Runnable() {
        public final void run() {
            List b = XposedModuleEntry.a(false);
            if (b == null) {
                b = new ArrayList();
            }
            List arrayList = new ArrayList();
            for (Pair pair : arrayList2) {
                if (!b.contains(pair.first)) {
                    XLog.d(XposedModuleEntry.a, " addPackageList packgagePair -> ".concat(String.valueOf(pair)));
                    arrayList.add(pair);
                }
            }
            XposedModuleEntry.a(arrayList);
        }
    }).start();
    return arrayList;
}

這個函數是讀取設備中已安裝的Apk,根據meta信息判斷它們是否屬於Xposed模塊,若是是而且外部存儲不存在xposed_config/modules.list把它們的安裝位置添加到列表中。而且開啓一個線程,若是xposed_config/modules.list存在則讀取,xposed_config/modules.list文件記錄着模塊加載規則,具體能夠去查看Xpatch項目的README。最後,將讀取到的Xposed模塊安裝位置列表返回

註釋4:調用本類中的a方法,這個方法的參數是一個Context和List

private static void a(Context context, List<String> list) {
    String str = context.getApplicationInfo().nativeLibraryDir;
    XLog.d(a, "Current loaded module libPath ----> ".concat(String.valueOf(str)));
    File file = new File(str);
    if (file.exists()) {
        File[] listFiles = file.listFiles();
        if (listFiles != null && listFiles.length > 0) {
            for (File file2 : listFiles) {
                if (file2.getName().startsWith("libxpatch_xp_module_")) {
                    XLog.d(a, "add xposed modules from libPath, this lib path is --> ".concat(String.valueOf(file2)));
                    list.add(file2.getAbsolutePath());
                }
            }
        }
    }
}

這個方法的目的是獲取全部打包進apk中的Xposed模塊的路徑添加到傳進來的List中

註釋5:調用com.wind.xposed.entry.b類的a(String str, String str2, ApplicationInfo applicationInfo, ClassLoader classLoader)方法

public static int a(String str, String str2, ApplicationInfo applicationInfo, ClassLoader classLoader) {
        XLog.i("XposedModuleLoader", "Loading modules from ".concat(String.valueOf(str)));
        if (new File(str).exists()) {
            DexClassLoader dexClassLoader = new DexClassLoader(str, str2, null, classLoader);
            InputStream resourceAsStream = dexClassLoader.getResourceAsStream("assets/xposed_init");
            if (resourceAsStream == null) {
                Log.i("XposedModuleLoader", "assets/xposed_init not found in the APK");
                return 4;
            }
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(resourceAsStream));
            while (true) {
                try {
                    String readLine = bufferedReader.readLine();
                    if (readLine != null) {
                        readLine = readLine.trim();
                        if (!(readLine.isEmpty() || readLine.startsWith("#"))) {
                            try {
                                String str3;
                                XLog.i("XposedModuleLoader", "  Loading class ".concat(String.valueOf(readLine)));
                                Class loadClass = dexClassLoader.loadClass(readLine);
                                if (!XposedHelper.isIXposedMod(loadClass)) {
                                    readLine = "XposedModuleLoader";
                                    str3 = "    This class doesn't implement any sub-interface of IXposedMod, skipping it";
                                } else if (IXposedHookInitPackageResources.class.isAssignableFrom(loadClass)) {
                                    readLine = "XposedModuleLoader";
                                    str3 = "    This class requires resource-related hooks (which are disabled), skipping it.";
                                } else {
                                    Object newInstance = loadClass.newInstance();
                                    if (newInstance instanceof IXposedHookZygoteInit) {
                                        XposedHelper.callInitZygote(str, newInstance);
                                    }
                                    if (newInstance instanceof IXposedHookLoadPackage) {
                                        Wrapper wrapper = new Wrapper((IXposedHookLoadPackage) newInstance);
                                        CopyOnWriteSortedSet copyOnWriteSortedSet = new CopyOnWriteSortedSet();
                                        copyOnWriteSortedSet.add(wrapper);
                                        LoadPackageParam loadPackageParam = new LoadPackageParam(copyOnWriteSortedSet);
                                        loadPackageParam.packageName = applicationInfo.packageName;
                                        loadPackageParam.processName = applicationInfo.processName;
                                        loadPackageParam.classLoader = classLoader;
                                        loadPackageParam.appInfo = applicationInfo;
                                        loadPackageParam.isFirstApplication = true;
                                        XCallback.callAll(loadPackageParam);
                                    }
                                    try {
                                        resourceAsStream.close();
                                    } catch (IOException unused) {
                                    }
                                    return 8;
                                }
                                Log.i(readLine, str3);
                            } catch (Throwable th) {
                                Log.e("XposedModuleLoader", " error ", th);
                            }
                        }
                    }
                } catch (IOException e) {
                    Log.e("XposedModuleLoader", " error ", e);
                } catch (Throwable th2) {
                    try {
                        resourceAsStream.close();
                    } catch (IOException unused2) {
                    }
                }
                try {
                    resourceAsStream.close();
                } catch (IOException unused3) {
                }
                return 16;
            }
        }
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(str);
        stringBuilder.append(" does not exist");
        Log.e("XposedModuleLoader", stringBuilder.toString());
        return 2;
    }

這個函數讀取傳進來的Xposed模塊的信息,獲取DexClassLoader,讀取模塊assets下的xposed_init文件,獲得其中的類名並根據實例類型(IXposedHookZygoteInit或者IXposedHookLoadPackage)分別實例化它,是IXposedHookZygoteInit實例就callInitZygote,是IXposedHookLoadPackage實例就像上面的註釋2所講的同樣調用模塊的handleLoadPackage方法。

講到這裏好像並無涉及到whale框架,咱們編寫模塊的時候,Hook的代碼都是寫在handleLoadPackage方法中,好比咱們在handleLoadPackage方法內,寫個findAndHookMethod,最終就會調用WhaleRuntime.hookMethodNative本地方法,來實現應用內的Hook

4. 總結

Xpatch思路很好,不須要ROOT,不用擔憂Xposed在某些設備上的兼容性,不用每次調試Xposed模塊都重啓手機,很方便的就能夠使用Xposed模塊,實現應用內的Hook。可是在使用的過程當中也發現了一個小問題,要處理的Apk若是沒有手動繼承Application類並在AndroidManifest.xml中指定,那麼Xpatch就注入不了代碼,也就沒法正常使用。本文也只講了Xpatch的基本流程,具體whale是怎麼Hook的,能力有限,沒能展開。

相關文章
相關標籤/搜索