/* * Copyright (C) 2016 Baidu, Inc. All Rights Reserved. */ package com.dodola.rocoofix; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetManager; import android.os.Build; import android.util.Log; import dalvik.system.DexFile; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.ListIterator; import java.util.Set; import java.util.zip.ZipFile; import static com.dodola.rocoofix.RocooUtils.expandFieldArray; import static com.dodola.rocoofix.RocooUtils.findField; import static com.dodola.rocoofix.RocooUtils.findMethod; /** * modify from MultiDex source code * * https://github.com/dodola/RocooFix/blob/master/rocoo/src/main/java/com/dodola/rocoofix/RocooFix.java * * https://android.googlesource.com/platform/frameworks/multidex/+/master/library/src/android/support/multidex/MultiDex.java */ public final class RocooFix { static final String TAG = "Rocoo"; private static final String CODE_CACHE_NAME = "code_cache"; private static final String CODE_CACHE_SECONDARY_FOLDER_NAME = "rocoo-dexes"; private static final Set<String> installedApk = new HashSet<String>(); private RocooFix() { } public static void init(Context context) { initPathFromAssets(context, "hack.jar"); } /** * 从 Assets 里取出补丁 * * @param context context * @param assetName 补丁名 */ public static void initPathFromAssets(Context context, String assetName) { File dexDir = new File(context.getFilesDir(), "hotfix"); // 创建 hotfix 文件夹 dexDir.mkdir(); String dexPath = null; try { // 复制补丁到 hotfix 文件夹内,并记录复制后补丁的路径 dexPath = copyAsset(context, assetName, dexDir); } catch (IOException e) { } finally { // 如果复制成功 if (dexPath != null && new File(dexPath).exists()) { // 加载补丁 applyPatch(context, dexPath); } } } /** * 加载补丁 * * @param context context * @param dexPath 补丁地址 */ public static void applyPatch(Context context, String dexPath) { // if (IS_VM_CAPABLE) { // //art虚拟机走另外一套fix // return; // } try { ApplicationInfo applicationInfo = getApplicationInfo(context); if (applicationInfo == null) { return; } synchronized (installedApk) { if (installedApk.contains(dexPath)) { return; } installedApk.add(dexPath); /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */ ClassLoader loader; try { // 获取 PathClassLoader loader = context.getClassLoader(); } catch (RuntimeException e) { /* Ignore those exceptions so that we don't break tests relying on Context like * a android.test.mock.MockContext or a android.content.ContextWrapper with a * null base Context. */ Log.w(TAG, "Failure while trying to obtain Context class loader. " + "Must be running in test mode. Skip patching.", e); return; } if (loader == null) { // Note, the context class loader is null when running Robolectric tests. Log.e(TAG, "Context class loader is null. Must be running in test mode. " + "Skip patching."); return; } // 包含补丁 File 的 List List<File> files = new ArrayList<File>(); files.add(new File(dexPath)); // 获取 缓存 dex File 的文件夹 File dexDir = getDexDir(context, applicationInfo); installDexes(loader, dexDir, files); } } catch (Exception e) { e.printStackTrace(); } catch (Throwable e) { e.printStackTrace(); } } /** * 获取 ApplicationInfo 信息 * * @param context context * @return ApplicationInfo * @throws NameNotFoundException */ private static ApplicationInfo getApplicationInfo(Context context) throws NameNotFoundException { PackageManager pm; String packageName; try { pm = context.getPackageManager(); packageName = context.getPackageName(); } catch (RuntimeException e) { /* Ignore those exceptions so that we don't break tests relying on Context like * a android.test.mock.MockContext or a android.content.ContextWrapper with a null * base Context. */ Log.w(TAG, "Failure while trying to obtain ApplicationInfo from Context. " + "Must be running in test mode. Skip patching.", e); return null; } if (pm == null || packageName == null) { // This is most likely a mock context, so just return without patching. return null; } return pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); } /** * 开始 根据不同版本的策略 插入补丁 dex * * @param loader classloader * @param dexDir 缓存 dex File 的文件夹 * @param files 包含补丁 File 的 List * @throws IllegalArgumentException * @throws IllegalAccessException * @throws NoSuchFieldException * @throws InvocationTargetException * @throws NoSuchMethodException * @throws IOException * @throws InstantiationException * @throws ClassNotFoundException */ private static void installDexes(ClassLoader loader, File dexDir, List<File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException, InstantiationException, ClassNotFoundException { if (!files.isEmpty()) { if (Build.VERSION.SDK_INT >= 24) { // Android N or 7 的策略 V24.install(loader, files, dexDir); } else if (Build.VERSION.SDK_INT >= 23) { // Android 6.0 的策略 V23.install(loader, files, dexDir); } else if (Build.VERSION.SDK_INT >= 19) { // Android 4.4 以上的策略 V19.install(loader, files, dexDir); } else if (Build.VERSION.SDK_INT >= 14) { // Android 4.0 以上的策略 V14.install(loader, files, dexDir); } else { // Android 4.0 以下的策略 V4.install(loader, files); } } } /** * 获取 缓存 dex File 的文件夹 * * @param context context * @param applicationInfo applicationInfo * @return 文件夹 File * @throws IOException */ private static File getDexDir(Context context, ApplicationInfo applicationInfo) throws IOException { // /data/data/code_cache File cache = new File(applicationInfo.dataDir, CODE_CACHE_NAME); try { // 检查 /data/data/code_cache mkdirChecked(cache); } catch (IOException e) { /* If we can't emulate code_cache, then store to filesDir. This means abandoning useless * files on disk if the device ever updates to android 5+. But since this seems to * happen only on some devices running android 2, this should cause no pollution. */ // /code_cache cache = new File(context.getFilesDir(), CODE_CACHE_NAME); // 检查 /code_cache mkdirChecked(cache); } // /data/data/code_cache/rocoo-dexes File dexDir = new File(cache, CODE_CACHE_SECONDARY_FOLDER_NAME); // 检查 /data/data/code_cache/rocoo-dexes mkdirChecked(dexDir); return dexDir; } /** * 检查 文件夹 是否存在 * * @param dir file * @throws IOException */ private static void mkdirChecked(File dir) throws IOException { dir.mkdir(); if (!dir.isDirectory()) { File parent = dir.getParentFile(); if (parent == null) { Log.e(TAG, "Failed to create dir " + dir.getPath() + ". Parent file is null."); } else { Log.e(TAG, "Failed to create dir " + dir.getPath() + ". parent file is a dir " + parent.isDirectory() + ", a file " + parent.isFile() + ", exists " + parent.exists() + ", readable " + parent.canRead() + ", writable " + parent.canWrite()); } throw new IOException("Failed to create directory " + dir.getPath()); } } private static final class V24 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, InstantiationException, ClassNotFoundException { // 获取 BaseDexClassLoader 中的 DexPathList pathList 属性 Field pathListField = findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); // 获取 DexPathList 中的 Element[] dexElements 属性 Field dexElement = findField(dexPathList, "dexElements"); Class<?> elementType = dexElement.getType().getComponentType(); /** * 获取 DexPathList 中的 loadDexFile 方法 * private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader, * Element[] elements) */ Method loadDex = findMethod(dexPathList, "loadDexFile", File.class, File.class, ClassLoader.class, dexElement.getType()); loadDex.setAccessible(true); // 调用 loadDexFile 方法 Object dex = loadDex.invoke(null, additionalClassPathEntries.get(0), optimizedDirectory, loader, dexElement.get(dexPathList)); // 反射 Element 的构造方法 Constructor<?> constructor = elementType.getConstructor(File.class, boolean.class, File.class, DexFile.class); constructor.setAccessible(true); // 反射构造一个 Element Object element = constructor.newInstance(new File(""), false, additionalClassPathEntries.get(0), dex); // 将 刚才构造的 Element 放入数组中 Object[] newEles = new Object[1]; newEles[0] = element; /** * 合并 dexElements 数组 和 构造的 Element 数组 * 构造的 Element 数组 会在合并数组的头部 * 达到插桩效果 */ expandFieldArray(dexPathList, "dexElements", newEles); } } private static final class V23 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, InstantiationException { // 获取 BaseDexClassLoader 中的 DexPathList pathList 属性 Field pathListField = findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); // 获取 DexPathList 中的 Element[] dexElements 属性 Field dexElement = findField(dexPathList, "dexElements"); // 拿到 Element 的 class Class<?> elementType = dexElement.getType().getComponentType(); /** * 获取 DexPathList 中的 loadDexFile 方法 * private static DexFile loadDexFile(File file, File optimizedDirectory) */ Method loadDex = findMethod(dexPathList, "loadDexFile", File.class, File.class); loadDex.setAccessible(true); // 调用 loadDexFile 方法 Object dex = loadDex.invoke(null, additionalClassPathEntries.get(0), optimizedDirectory); // 反射 Element 的构造方法 Constructor<?> constructor = elementType.getConstructor(File.class, boolean.class, File.class, DexFile.class); constructor.setAccessible(true); // 反射构造一个 Element Object element = constructor.newInstance(new File(""), false, additionalClassPathEntries.get(0), dex); // 将 刚才构造的 Element 放入数组中 Object[] newEles = new Object[1]; newEles[0] = element; /** * 合并 dexElements 数组 和 构造的 Element 数组 * 构造的 Element 数组 会在合并数组的头部 * 达到插桩效果 */ expandFieldArray(dexPathList, "dexElements", newEles); } } /** * Installer for platform versions 19. */ private static final class V19 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException { /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */ // 获取 BaseDexClassLoader 中的 DexPathList pathList 属性 Field pathListField = findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); /** * 调用 DexPathList#makeDexElements 方法 补丁 >> Element[] * makeDexElements 方法 产生的异常会收集到 suppressedExceptions 内 * * 然后 合并 DexPathList#dexElements 和 上面得到的 Element[] * Element[] 会在头部 >> 插桩 */ expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList<File>(additionalClassPathEntries), optimizedDirectory, suppressedExceptions)); if (suppressedExceptions.size() > 0) { for (IOException e : suppressedExceptions) { Log.w(TAG, "Exception in makeDexElement", e); } // 获取 IOException[] dexElementsSuppressedExceptions Field suppressedExceptionsField = findField(loader, "dexElementsSuppressedExceptions"); IOException[] dexElementsSuppressedExceptions = (IOException[]) suppressedExceptionsField.get(loader); /** * 如果 dexElementsSuppressedExceptions == null * 创建一个 makeDexElements 方法 产生的异常集合 suppressedExceptions * 相同长度的 IOException[] * * * 如果 dexElementsSuppressedExceptions != null * 与 suppressedExceptions 进行集合合并 * 成一个 数组 */ if (dexElementsSuppressedExceptions == null) { dexElementsSuppressedExceptions = suppressedExceptions.toArray( new IOException[suppressedExceptions.size()]); } else { IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions.length]; suppressedExceptions.toArray(combined); System.arraycopy(dexElementsSuppressedExceptions, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions.length); dexElementsSuppressedExceptions = combined; } // 重新设置 IOException[] dexElementsSuppressedExceptions 属性的值 suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions); } } /** * 反射获取 并 调用 dalvik.system.DexPathList#makeDexElements 方法 * A wrapper around * {@code private static final dalvik.system.DexPathList#makeDexElements}. */ private static Object[] makeDexElements( Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { Method makeDexElements = findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class); return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions); } } /** * Installer for platform versions 14, 15, 16, 17 and 18. */ private static final class V14 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException { /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */ // 获取 BaseDexClassLoader 中的 DexPathList pathList 属性 Field pathListField = findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); /** * 调用 DexPathList#makeDexElements 方法 补丁 >> Element[] * makeDexElements 方法 产生的异常会收集到 suppressedExceptions 内 * * 然后 合并 DexPathList#dexElements 和 上面得到的 Element[] * Element[] 会在头部 >> 插桩 */ expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList<File>(additionalClassPathEntries), optimizedDirectory)); } /** * 反射获取 并 调用 dalvik.system.DexPathList#makeDexElements 方法 * A wrapper around * {@code private static final dalvik.system.DexPathList#makeDexElements}. */ private static Object[] makeDexElements( Object dexPathList, ArrayList<File> files, File optimizedDirectory) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { Method makeDexElements = findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class); return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory); } } /** * Installer for platform versions 4 to 13. */ private static final class V4 { private static void install(ClassLoader loader, List<File> additionalClassPathEntries) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, IOException { /* The patched class loader is expected to be a descendant of * dalvik.system.DexClassLoader. We modify its * fields mPaths, mFiles, mZips and mDexs to append additional DEX * file entries. */ // 拿到 补丁 List 的长度 (一般只有一个) int extraSize = additionalClassPathEntries.size(); // 获取 PathClassLoader 中的 String path Field pathField = findField(loader, "path"); // 创建一个 path 的 StringBuilder StringBuilder path = new StringBuilder((String) pathField.get(loader)); String[] extraPaths = new String[extraSize]; File[] extraFiles = new File[extraSize]; ZipFile[] extraZips = new ZipFile[extraSize]; DexFile[] extraDexs = new DexFile[extraSize]; for (ListIterator<File> iterator = additionalClassPathEntries.listIterator(); iterator.hasNext(); ) { // 拿到每个补丁 File File additionalEntry = iterator.next(); // 拿到补丁 File 的虚拟路径 String entryPath = additionalEntry.getAbsolutePath(); // 将补丁 File 的虚拟路径 附加在 path 上 path.append(':').append(entryPath); int index = iterator.previousIndex(); /** * 补丁 path 数据 * 补丁 file 数据 * 补丁 zip 数据 * 补丁 dex 数据 */ extraPaths[index] = entryPath; extraFiles[index] = additionalEntry; extraZips[index] = new ZipFile(additionalEntry); extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0); } // 重新 将 Builder 内的 String 赋值给 path pathField.set(loader, path.toString()); // 合并 mPaths 和 补丁 path 数据 expandFieldArray(loader, "mPaths", extraPaths); // 合并 mFiles 和 补丁 file 数据 expandFieldArray(loader, "mFiles", extraFiles); // 合并 mZips 和 补丁 zip 数据 expandFieldArray(loader, "mZips", extraZips); // 合并 mDexs 和 补丁 dex 数据 expandFieldArray(loader, "mDexs", extraDexs); } } /** * 复制 Asset 的文件 * * @param context context * @param assetName Asset 内的文件名 * @param dir 指定目录 * @return 返回输出路径 * @throws IOException */ public static String copyAsset(Context context, String assetName, File dir) throws IOException { File outFile = new File(dir, assetName); if (!outFile.exists()) { AssetManager assetManager = context.getAssets(); InputStream in = assetManager.open(assetName); OutputStream out = new FileOutputStream(outFile); copyFile(in, out); in.close(); out.close(); } return outFile.getAbsolutePath(); } /** * 复制文件 * * @param in InputStream * @param out OutputStream * @throws IOException */ private static void copyFile(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[1024]; int read; while ((read = in.read(buffer)) != -1) { out.write(buffer, 0, read); } } }