Hook技术 —— 加载完整的Apk
通过该demo,我们能了解到如下内容:
1、 融合不同的 apk dex 文件,
2、 了解到Element对象以及DexFile 对象
3、 插件中APK资源的合并
本文切入点
1、 融合Element数组
2、 获取资源文件
一、 融合 Element 数组
首先了解一下什么是ClassLoader,以及java 中 ClassLoader与Android 中ClassLoader的区别
假设你已经看完了上面的文章~ 下面我们来分析一下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);