Hook技术 —— 加载完整的Apk

2018-09-11  本文已影响141人  设计失

通过该demo,我们能了解到如下内容:

1、 融合不同的 apk dex 文件,
2、 了解到Element对象以及DexFile 对象
3、 插件中APK资源的合并

本文切入点

1、 融合Element数组
2、 获取资源文件

一、 融合 Element 数组

首先了解一下什么是ClassLoader,以及java 中 ClassLoader与Android 中ClassLoader的区别

这里很详细的介绍了DexClassLoader

假设你已经看完了上面的文章~ 下面我们来分析一下java中加载class文件的流程:

看下下面的代码:
// 我们反射到系统的api
Class.forName("android.app.ActivityThread");

这个forName方法最终调用了 Class.java 中的native方法:

static native Class<?> classForName(String className, boolean shouldInitialize,
            ClassLoader classLoader) throws ClassNotFoundException;

从native层又到了java层中 PathClassLoader ,PathClassLoader 最终又调用了父类BaseDexClassLoader 中的findClass方法:


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        ... 省略
        return c;
    }

我们看 pathList.findClass(); 这个pathList对象是什么?

private final DexPathList pathList;

DexPathList对象中有一个findClass 方法:


  public Class findClass(String name, List<Throwable> suppressed) {
          // Element数组存放Dex文件
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

最终是从Dexfile类中获取到class文件并返回,所以DexFile文件是APK包中dex文件在java中的一个映射~

以上就是系统加载class文件的过程,我们可以在根源入手,把apk中的dex和原app中的dex合并到一起转换成Element

融合两个apk中的dex文件

在我们看到PathClassLoader调用到父类中findClass的时候,可以看到BaseDexClassLoader中的构造方法初始化了DexPathList对象:

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        //ClassLoader 类加载器
        //String dexPath, dex文件的路径
        //String libraryPath 库路径
        // File optimizedDirectory 缓存路径
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

在DexPathList构造方法中又初始化了Element对象:

  public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
... 省略

        // 在这里初始化
        this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null,
                                                          suppressedExceptions);

        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions =
                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
        }
    }

看看 makePathElements() 方法:

 /**
     * Makes an array of dex/resource path elements, one per element of
     * the given array.
     */
    private static Element[] makePathElements(List<File> files, File optimizedDirectory,
                                              List<IOException> suppressedExceptions) {
        List<Element> elements = new ArrayList<>();
        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        // 打开dex中所有的文件,来加载
        for (File file : files) {
            File zip = null;
            File dir = new File("");
            DexFile dex = null;
            String path = file.getPath();
            String name = file.getName();

            if (path.contains(zipSeparator)) {
                String split[] = path.split(zipSeparator, 2);
                zip = new File(split[0]);
                dir = new File(split[1]);
            } else if (file.isDirectory()) {
                // We support directories for looking up resources and native libraries.
                // Looking up resources in directories is useful for running libcore tests.
                // 将文件夹添加进数组
                elements.add(new Element(file, true, null, null));
            } else if (file.isFile()) {
                // 如果文件是以 dex 结尾
                if (name.endsWith(DEX_SUFFIX)) {
                    // Raw dex file (not inside a zip/jar).
                    try {
                        // 获取到一个DexFile文件,不包含 zip/jar
                        dex = loadDexFile(file, optimizedDirectory);
                    } catch (IOException ex) {
                        System.logE("Unable to load dex file: " + file, ex);
                    }
                } else {
                    // 如果不是以 dex 结尾,则以一个压缩包的形式来解决
                    zip = file;

                    try {
                        dex = loadDexFile(file, optimizedDirectory);
                    } catch (IOException suppressed) {
                        /*
                         * IOException might get thrown "legitimately" by the DexFile constructor if
                         * the zip file turns out to be resource-only (that is, no classes.dex file
                         * in it).
                         * Let dex == null and hang on to the exception to add to the tea-leaves for
                         * when findClass returns null.
                         */
                        suppressedExceptions.add(suppressed);
                    }
                }
            } else {
                System.logW("ClassLoader referenced unknown path: " + file);
            }

            if ((zip != null) || (dex != null)) {
                // 将文件添加到Element 数组中
                elements.add(new Element(dir, false, zip, dex));
            }
        }

        return elements.toArray(new Element[elements.size()]);
    }

/**
     * Constructs a {@code DexFile} instance, as appropriate depending
     * on whether {@code optimizedDirectory} is {@code null}.
     */
    private static DexFile loadDexFile(File file, File optimizedDirectory)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0);
        }
    }

到这里,我们可以看到BaseDexClassLoader初始化的时候就给我们实例化了Element[] 数组,所以我们要融合多个dex,只需要实例化一个BaseDexClassLoader!

首先获取到系统中的Element数组
//1、  找到系统中的 Elements数组,   private final Element[] dexElements;
Class myDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
Field myPathListField = myDexClassLoader.getDeclaredField("pathList");
myPathListField.setAccessible(true);
// 获取到 DexPathList
Object myPathList = myPathListField.get(myDexClassLoader);
// 获取到dexElements   private final Element[] dexElements;
Field dexElements = myPathList.getClass().getDeclaredField("dexElements");
dexElements.setAccessible(true);
// 获取到系统的apk的Element数组
Object myElements = dexElements.get(myPathList);
然后获取到插件中的Element数组
//2、  找到插件的 Element数组,
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Class pluginDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
Field pluginPathList = pluginDexClassLoader.getDeclaredField("pathList");
pluginPathList.setAccessible(true);
// 获取到 DexPathList
Object pluginDexPathList = pluginPathList.get(pathClassLoader);
// 获取到dexElements   private final Element[] dexElements;
Field pluginDexElements = pluginDexPathList.getClass().getDeclaredField("dexElements");
dexElements.setAccessible(true);
// 获取到系统的apk的Element数组
Object pluginElements = pluginDexElements.get(myPathList);
融合两个数组对象
//3、 融合两个 Elements 长度为插件和系统两个数组的长, 反射注入到系统中
// 3_1 获取到两个Elements的长度,得到最新的长度
int myLength = Array.getLength(myElements);
int pluginLength = Array.getLength(pluginElements);
int newLength = myLength + pluginLength;
// 3_2 每个数组的类型,已经新生成的数组的长度
// 找到Element的类型
Class componentType = myElements.getClass().getComponentType();
Object newElements = Array.newInstance(componentType, newLength);
// 3_3 融合
for (int i = 0; i < newLength; i++) {
    if (i < myLength) {
        Array.set(newElements, i, Array.get(myElements, i));
    } else {
        Array.set(newElements, i, Array.get(pluginElements, i - myLength));
    }
}
// 3_4 获取到dexElements   private final Element[] dexElements;
Field elementsField = myPathList.getClass().getDeclaredField("dexElements");
elementsField.setAccessible(true);
elementsField.set(myPathList, newElements);

上面就融合了安装APK与插件APK中的Dex文件


二 、获取到资源文件

首先我们看下平时我们获取到资源:

 @NonNull public CharSequence getText(@StringRes int id) throws NotFoundException {
        CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
        if (res != null) {
            return res;
        }
        throw new NotFoundException("String resource ID #0x"
                + Integer.toHexString(id));
    }

实际上我们获取到资源是通过AssetManager对象获取,所以我们需要获取到外部存储卡的Resource对象和AssetManager对象:

 // apk路径
String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "plugin.apk";
// 初始化一个AssetManager
assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath",String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, apkPath);
// 初始化Resources 对象,加载资源
resources = new Resources(
                   assetManager, // AssetManager
                    getApplicationContext().getResources().getDisplayMetrics(), // DisplayMetrics
                    getApplicationContext().getResources().getConfiguration()  // Configruation
            );

上面是初始化了AssetManager对象,以及Resources对象,实际上资源的加载都是通过AssetManager中的addAssetPath方法来注入,所以我们反射获取到AssetManager对象之后,需要invoke到addAssetPath对象,把插件的路径加载~

通过以上就让插件的资源融合了,但是还有一点,就是获取到资源的时候,没有初始化需要的Block对象,而Block对象是什么呢?

初始化 block 系列对象

通过getResouces().getText() 可以获取到一个文字资源,查看源码,我们可以看到 (这里以安卓6.0 api为主)

    /**
     * Retrieve the string value associated with a particular resource
     * identifier for the current configuration / skin.
     */
    /*package*/ final CharSequence getResourceText(int ident) {
        synchronized (this) {
            TypedValue tmpValue = mValue;
            int block = loadResourceValue(ident, (short) 0, tmpValue, true);
            if (block >= 0) {
                if (tmpValue.type == TypedValue.TYPE_STRING) {
                    // 获取到StringBlock
                    return mStringBlocks[block].get(tmpValue.data);
                }
                return tmpValue.coerceToString();
            }
        }
        return null;
    }

而在Resouces构造方法中,并没有初始化StringBlock数组的方法,通过层层追查,我们看到如下方法:

/*package*/ final boolean getThemeValue(long theme, int ident,
            TypedValue outValue, boolean resolveRefs) {
        int block = loadThemeAttributeValue(theme, ident, outValue, resolveRefs);
        if (block >= 0) {
            if (outValue.type != TypedValue.TYPE_STRING) {
                return true;
            }
            StringBlock[] blocks = mStringBlocks;
            if (blocks == null) {
                // 为空时 则调用 ensureStringBlocks() 方法
                ensureStringBlocks();
                blocks = mStringBlocks;
            }
            outValue.string = blocks[block].get(outValue.data);
            return true;
        }
        return false;
    }

接下来调用了ensureStringBlocks() 方法:

   /*package*/ final void ensureStringBlocks() {
       // 这里使用了双重校验
        if (mStringBlocks == null) {
            synchronized (this) {
                if (mStringBlocks == null) {
                    makeStringBlocks(sSystem.mStringBlocks);
                }
            }
        }
    }

再调用了makeStringBlocks() :


    /*package*/ final void makeStringBlocks(StringBlock[] seed) {
        final int seedNum = (seed != null) ? seed.length : 0;
        final int num = getStringBlockCount();
        mStringBlocks = new StringBlock[num];
        if (localLOGV) Log.v(TAG, "Making string blocks for " + this
                + ": " + num);
        for (int i=0; i<num; i++) {
            if (i < seedNum) {
                mStringBlocks[i] = seed[i];
            } else {
                // 通过native 方法来获取到StringBlock数组
                mStringBlocks[i] = new StringBlock(getNativeStringBlock(i), true);
            }
        }
    }

到此,就初始化了StringBlock ,所以我们需要反射调用 ensureStringBlocks 方法,来初始化:

// 初始化StringBlock对象
Method ensureStringBlocks = assetManager.getClass().getDeclaredMethod("ensureStringBlocks");
ensureStringBlocks.setAccessible(true);
ensureStringBlocks.invoke(assetManager);

将插件中的添加BaseActivity 重写getResouces() 方法和 getAssets 方法 :


public class BaseActivity extends Activity {

    @Override
    public Resources getResources() {
        if(getApplication()!=null && getApplication().getResources()!=null) {
            return getApplication().getResources();
        }
        return super.getResources();
    }

    @Override
    public AssetManager getAssets() {
        
        if(getApplication()!=null && getApplication().getAssets()!=null) {
            return getApplication().getAssets();
        }
        return super.getAssets();
    }
}
最后一步,跳转界面:
Intent i = new Intent();
i.setComponent(new ComponentName(pkg, cls));
startActivity(i);

完~~

上一篇下一篇

猜你喜欢

热点阅读