目录:
- 1. 插件化加载简介
- 2. Qigsaw 加载 class、libs
- 3. Qigsaw 加载资源
1. 插件化加载简介
插件化技术最初源于免安装运行 APK 的想法,这个免安装的 APK 可以理解为插件。支持插件化的 APP 可以在运行时加载和运行插件,这样便可以将 APP 中一些不常用的功能模块做成插件,一方面减小了安装包的大小,另一方面可以实现 APP 功能的动态扩展。想要实现插件化,主要是解决下面三个问题:
- 插件中代码的加载和与主工程的互相调用。
- 插件中资源的加载和与主工程的互相访问。
- 四大组件生命周期的管理。
(1) 代码加载
通过给插件 APK 生成相应的 DexClassLoader 便可以访问其中的类,这边又有两种处理方式,有单 DexClassLoader 和多DexClassLoader 两种结构。Android 中常用的有两种类加载器,DexClassLoader 和 PathClassLoader,它们都继承于 BaseDexClassLoader。区别在于调用父类构造器时,DexClassLoader 多传了一个 optimizedDirectory 参数,这个目录必须是内部存储路径,用来缓存系统创建的 Dex 文件。而 PathClassLoader 该参数为 null,只能加载内部存储目录的 Dex 文件。所以我们可以用 DexClassLoader 去加载外部的 APK.
多 DexClassLoader:
对于每个插件都会生成一个 DexClassLoader,当加载该插件中的类时需要通过对应 DexClassLoader 加载。这样不同插件的类是隔离的,当不同插件引用了同一个类库的不同版本时,不会出问题。RePlugin 采用的是该方案。
单 DexClassLoader:
将插件的 DexClassLoader 中的 pathList 合并到主工程的 DexClassLoader 中。这样做的好处时,可以在不同的插件以及主工程间直接互相调用类和方法,并且可以将不同插件的公共模块抽出来放在一个 common 插件中直接供其他插件使用。Small 采用的是这种方式。
插件调用主工程:
在构造插件的 ClassLoader 时会传入主工程的 ClassLoader 作为父加载器,所以插件是可以直接可以通过类名引用主工程的类。
主工程调用插件:
- 若使用多 ClassLoader 机制,主工程引用插件中类需要先通过插件的 ClassLoader 加载该类再通过反射调用其方法。插件化框架一般会通过统一的入口去管理对各个插件中类的访问,并且做一定的限制。
- 若使用单 ClassLoader 机制,主工程则可以直接通过类名去访问插件中的类。该方式有个弊病,若两个不同的插件工程引用了一个库的不同版本,则程序可能会出错,所以要通过一些规范去避免该情况发生。
Qigsaw 支持单、多 classloader 加载。
(2) 资源加载
资源路径的处理:
- 合并式:addAssetPath 时加入所有插件和主工程的路径。
- 独立式:各个插件只添加自己 APK 路径。
合并式由于 AssetManager 中加入了所有插件和主工程的路径,因此生成的 Resource 可以同时访问插件和主工程的资源。但是由于主工程和各个插件都是独立编译的,生成的资源 id 会存在相同的情况,在访问时会产生资源冲突。
独立式时,各个插件的资源是互相隔离的,不过如果想要实现资源的共享,必须拿到对应的 Resource 对象。
Qigsaw 采用合并式资源加载方式。
(3) 四大组件
Android 开发中有一些特殊的类,是由系统创建的,并且由系统管理生命周期。如常用的四大组件,Activity,Service,BroadcastReceiver 和 ContentProvider。 仅仅构造出这些类的实例是没用的,还需要管理组件的生命周期。其中以 Activity 最为复杂,不同框架采用的方法也不尽相同。下面以 Activity 为例详细介绍插件化如何支持组件生命周期的管理,大致分为两种方式:
- ProxyActivity 代理。
- 预埋 StubActivity,hook 系统启动 Activity 的过程。
由于 Qigsaw 是基于 AAB 开发的,AAB 会提前合并 base APK、dynamic feature APK 中所有的四大组件,所以 Qigsaw 的插件加载无需考虑四大组件的支持。
2. Qigsaw 加载 class、libs
SplitDelegateClassloader.java:
final class SplitDelegateClassloader extends PathClassLoader {
private static final String TAG = "SplitDelegateClassloader";
private static BaseDexClassLoader originClassLoader;
private int splitLoadMode;
SplitDelegateClassloader(ClassLoader parent) {
super("", parent);
originClassLoader = (BaseDexClassLoader) parent;
}
private static void reflectPackageInfoClassloader(Context baseContext, ClassLoader reflectClassLoader) throws Exception {
Object packageInfo = HiddenApiReflection.findField(baseContext, "mPackageInfo").get(baseContext);
if (packageInfo != null) {
HiddenApiReflection.findField(packageInfo, "mClassLoader").set(packageInfo, reflectClassLoader);
}
}
static void inject(ClassLoader originalClassloader, Context baseContext) throws Exception {
SplitDelegateClassloader classloader = new SplitDelegateClassloader(originalClassloader);
reflectPackageInfoClassloader(baseContext, classloader);
}
void setSplitLoadMode(int splitLoadMode) {
this.splitLoadMode = splitLoadMode;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
return originClassLoader.loadClass(name);
} catch (ClassNotFoundException error) {
if (SplitLoadManagerService.hasInstance()) {
if (splitLoadMode == SplitLoad.MULTIPLE_CLASSLOADER) {
Class<?> result = onClassNotFound(name);
if (result != null) {
return result;
}
} else if (splitLoadMode == SplitLoad.SINGLE_CLASSLOADER) {
Class<?> result = onClassNotFound2(name);
if (result != null) {
return result;
}
}
}
throw error;
}
}
private Class<?> onClassNotFound(String name) {
Class<?> ret = findClassInSplits(name);
if (ret != null) {
return ret;
}
Class<?> fakeComponent = AABExtension.getInstance().getFakeComponent(name);
if (fakeComponent != null) {
SplitLoadManagerService.getInstance().loadInstalledSplits();
ret = findClassInSplits(name);
if (ret != null) {
return ret;
}
return fakeComponent;
}
return null;
}
private Class<?> onClassNotFound2(String name) {
Class<?> fakeComponent = AABExtension.getInstance().getFakeComponent(name);
if (fakeComponent != null) {
SplitLoadManagerService.getInstance().loadInstalledSplits();
try {
return originClassLoader.loadClass(name);
} catch (ClassNotFoundException e) {
return fakeComponent;
}
}
return null;
}
private Class<?> findClassInSplits(String name) {
Set<SplitDexClassLoader> splitDexClassLoaders = SplitApplicationLoaders.getInstance().getClassLoaders();
for (SplitDexClassLoader classLoader : splitDexClassLoaders) {
try {
Class<?> clazz = classLoader.loadClassItself(name);
return clazz;
} catch (ClassNotFoundException e) {
}
}
return null;
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
return originClassLoader.getResources(name);
}
@Override
public URL getResource(String name) {
return originClassLoader.getResource(name);
}
//...
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return findClass(name);
}
@Override
public String findLibrary(String name) {
String libName = originClassLoader.findLibrary(name);
if (libName == null) {
Set<SplitDexClassLoader> splitDexClassLoaders = SplitApplicationLoaders.getInstance().getClassLoaders();
for (SplitDexClassLoader classLoader : splitDexClassLoaders) {
libName = classLoader.findLibraryItself(name);
if (libName != null) {
break;
}
}
}
return libName;
}
}
findClass 最终找不到时,如果 class 类型是四大组件,则会返回 FakeClass.
(1) 单 classloader 加载 libs
final class SplitCompatLibraryLoader {
private static final String TAG = "SplitCompatLibraryLoader";
static void load(ClassLoader classLoader, File folder)
throws Throwable {
// android o sdk_int 26
// for android o preview sdk_int 25
if ((Build.VERSION.SDK_INT == 25 && Build.VERSION.PREVIEW_SDK_INT != 0)
|| Build.VERSION.SDK_INT > 25) {
try {
V25.load(classLoader, folder);
} catch (Throwable throwable) {
V23.load(classLoader, folder);
}
} else if (Build.VERSION.SDK_INT >= 23) {
try {
V23.load(classLoader, folder);
} catch (Throwable throwable) {
V14.load(classLoader, folder);
}
} else if (Build.VERSION.SDK_INT >= 14) {
V14.load(classLoader, folder);
} else {
throw new UnsupportedOperationException("don\'t support under SDK version 14!");
}
}
private static final class V14 {
private static void load(ClassLoader classLoader, File folder) throws Throwable {
final Field pathListField = HiddenApiReflection.findField(classLoader, "pathList");
final Object dexPathList = pathListField.get(classLoader);
final Field nativeLibDirField = HiddenApiReflection.findField(dexPathList, "nativeLibraryDirectories");
final File[] origNativeLibDirs = (File[]) nativeLibDirField.get(dexPathList);
final List<File> newNativeLibDirList = new ArrayList<>(origNativeLibDirs.length + 1);
newNativeLibDirList.add(folder);
for (File origNativeLibDir : origNativeLibDirs) {
if (!folder.equals(origNativeLibDir)) {
newNativeLibDirList.add(origNativeLibDir);
}
}
nativeLibDirField.set(dexPathList, newNativeLibDirList.toArray(new File[0]));
}
}
private static final class V23 {
private static void load(ClassLoader classLoader, File folder) throws Throwable {
final Field pathListField = HiddenApiReflection.findField(classLoader, "pathList");
final Object dexPathList = pathListField.get(classLoader);
final Field nativeLibraryDirectories = HiddenApiReflection.findField(dexPathList, "nativeLibraryDirectories");
List<File> origLibDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
if (origLibDirs == null) {
origLibDirs = new ArrayList<>(2);
}
final Iterator<File> libDirIt = origLibDirs.iterator();
while (libDirIt.hasNext()) {
final File libDir = libDirIt.next();
if (folder.equals(libDir)) {
libDirIt.remove();
break;
}
}
origLibDirs.add(0, folder);
final Field systemNativeLibraryDirectories = HiddenApiReflection.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> origSystemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
if (origSystemLibDirs == null) {
origSystemLibDirs = new ArrayList<>(2);
}
final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() + origSystemLibDirs.size() + 1);
newLibDirs.addAll(origLibDirs);
newLibDirs.addAll(origSystemLibDirs);
final Method makeElements = HiddenApiReflection.findMethod(dexPathList,
"makePathElements", List.class, File.class, List.class);
final ArrayList<IOException> suppressedExceptions = new ArrayList<>();
final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs, null, suppressedExceptions);
final Field nativeLibraryPathElements = HiddenApiReflection.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.set(dexPathList, elements);
}
}
private static final class V25 {
private static void load(ClassLoader classLoader, File folder) throws Throwable {
final Field pathListField = HiddenApiReflection.findField(classLoader, "pathList");
final Object dexPathList = pathListField.get(classLoader);
final Field nativeLibraryDirectories = HiddenApiReflection.findField(dexPathList, "nativeLibraryDirectories");
List<File> origLibDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
if (origLibDirs == null) {
origLibDirs = new ArrayList<>(2);
}
final Iterator<File> libDirIt = origLibDirs.iterator();
while (libDirIt.hasNext()) {
final File libDir = libDirIt.next();
if (folder.equals(libDir)) {
libDirIt.remove();
break;
}
}
origLibDirs.add(0, folder);
final Field systemNativeLibraryDirectories = HiddenApiReflection.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> origSystemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
if (origSystemLibDirs == null) {
origSystemLibDirs = new ArrayList<>(2);
}
final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() + origSystemLibDirs.size() + 1);
newLibDirs.addAll(origLibDirs);
newLibDirs.addAll(origSystemLibDirs);
final Method makeElements = HiddenApiReflection.findMethod(dexPathList, "makePathElements", List.class);
final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs);
final Field nativeLibraryPathElements = HiddenApiReflection.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.set(dexPathList, elements);
}
}
}
(2) 单 classloader 加载 dex
final class SplitCompatDexLoader {
private static final String TAG = "SplitCompatDexLoader";
private static int sPatchDexCount = 0;
static void load(ClassLoader classLoader, File dexOptDir, List<File> files)
throws Throwable {
if (!files.isEmpty()) {
if (Build.VERSION.SDK_INT >= 23) {
V23.load(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 19) {
V19.load(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.load(classLoader, files, dexOptDir);
} else {
throw new UnsupportedOperationException("don\'t support under SDK version 14!");
}
sPatchDexCount = files.size();
}
}
static void unLoad(ClassLoader classLoader) throws Throwable {
if (sPatchDexCount <= 0) {
return;
}
if (Build.VERSION.SDK_INT >= 14) {
Field pathListField = HiddenApiReflection.findField(classLoader, "pathList");
Object dexPathList = pathListField.get(classLoader);
HiddenApiReflection.reduceFieldArray(dexPathList, "dexElements", sPatchDexCount);
} else {
throw new RuntimeException("don\'t support under SDK version 14!");
}
}
/**
* Installer for platform versions 19.
*/
private static final class V23 {
private static void load(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
Field pathListField = HiddenApiReflection.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<>();
HiddenApiReflection.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
new ArrayList<>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
SplitLog.e(TAG, "Exception in makePathElement", e);
throw e;
}
}
}
/**
* A wrapper around
* {@code private static final dalvik.system.DexPathList#makePathElements}.
*/
private static Object[] makePathElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makePathElements;
try {
makePathElements = HiddenApiReflection.findMethod(dexPathList, "makePathElements", List.class, File.class,
List.class);
} catch (NoSuchMethodException e) {
SplitLog.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
try {
makePathElements = HiddenApiReflection.findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
} catch (NoSuchMethodException e1) {
SplitLog.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
try {
return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
} catch (NoSuchMethodException e2) {
throw e2;
}
}
}
return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
}
}
/**
* Installer for platform versions 19.
*/
private static final class V19 {
private static void load(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
Field pathListField = HiddenApiReflection.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<>();
HiddenApiReflection.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
SplitLog.e(TAG, "Exception in makeDexElement", e);
throw e;
}
}
}
/**
* 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;
try {
makeDexElements = HiddenApiReflection.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
ArrayList.class);
} catch (NoSuchMethodException e) {
SplitLog.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
try {
makeDexElements = HiddenApiReflection.findMethod(dexPathList, "makeDexElements", List.class, File.class, List.class);
} catch (NoSuchMethodException e1) {
SplitLog.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");
throw e1;
}
}
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 load(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
Field pathListField = HiddenApiReflection.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
HiddenApiReflection.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
new ArrayList<>(additionalClassPathEntries), optimizedDirectory));
}
/**
* 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 =
HiddenApiReflection.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
}
}
}
(3) 多 classloader 加载
final class SplitDexClassLoader extends BaseDexClassLoader {
private static final String TAG = "SplitDexClassLoader";
private final String moduleName;
private Set<SplitDexClassLoader> dependenciesLoaders;
private SplitDexClassLoader(String moduleName,
List<String> dexPaths,
File optimizedDirectory,
String librarySearchPath,
List<String> dependencies,
ClassLoader parent) throws Throwable {
super((dexPaths == null) ? "" : TextUtils.join(File.pathSeparator, dexPaths), optimizedDirectory, librarySearchPath, parent);
this.moduleName = moduleName;
this.dependenciesLoaders = SplitApplicationLoaders.getInstance().getClassLoaders(dependencies);
SplitUnKnownFileTypeDexLoader.loadDex(this, dexPaths, optimizedDirectory);
}
//...
}
3. Qigsaw 加载资源
Qigsaw 采用合并式加载资源,对宿主和插件的资源 id 做了分区:
@RestrictTo(LIBRARY_GROUP)
public class SplitCompatResourcesLoader {
private static final String TAG = "SplitCompatResourcesLoader";
private static final Object sLock = new Object();
public static void loadResources(Context context, Resources resources) throws Throwable {
checkOrUpdateResources(context, resources);
}
static void loadResources(Context context, Resources preResources, String splitApkPath) throws Throwable {
List<String> loadedResDirs = getLoadedResourcesDirs(preResources.getAssets());
if (!loadedResDirs.contains(splitApkPath)) {
installSplitResDirs(context, preResources, Collections.singletonList(splitApkPath));
}
}
private static void checkOrUpdateResources(Context context, Resources resources) throws SplitCompatResourcesException {
List<String> loadedResDirsInAsset;
try {
loadedResDirsInAsset = getLoadedResourcesDirs(resources.getAssets());
} catch (Throwable e) {
throw new SplitCompatResourcesException("Failed to get all loaded split resources for " + context.getClass().getName(), e);
}
Collection<String> loadedSplitPaths = getLoadedSplitPaths();
if (loadedSplitPaths != null && !loadedSplitPaths.isEmpty()) {
if (!loadedResDirsInAsset.containsAll(loadedSplitPaths)) {
List<String> unloadedSplitPaths = new ArrayList<>();
for (String splitPath : loadedSplitPaths) {
if (!loadedResDirsInAsset.contains(splitPath)) {
unloadedSplitPaths.add(splitPath);
}
}
try {
installSplitResDirs(context, resources, unloadedSplitPaths);
} catch (Throwable e) {
throw new SplitCompatResourcesException("Failed to install resources " + unloadedSplitPaths.toString() + " for " + context.getClass().getName(), e);
}
}
}
}
private static Collection<String> getLoadedSplitPaths() {
SplitLoadManager loadManager = SplitLoadManagerService.getInstance();
if (loadManager != null) {
return loadManager.getLoadedSplitApkPaths();
}
return null;
}
private static void installSplitResDirs(final Context context, final Resources resources, final List<String> splitResPaths) throws Throwable {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
V21.installSplitResDirs(resources, splitResPaths);
} else {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
V14.installSplitResDirs(context, resources, splitResPaths);
} else {
synchronized (sLock) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
synchronized (sLock) {
try {
V14.installSplitResDirs(context, resources, splitResPaths);
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
sLock.notify();
}
}
});
sLock.wait();
}
}
}
}
private static List<String> getLoadedResourcesDirs(AssetManager asset) throws NoSuchFieldException,
IllegalAccessException, NoSuchMethodException, InvocationTargetException, ClassNotFoundException {
List<String> existedAppResDirList = new ArrayList<>();
if (Build.VERSION.SDK_INT >= 28) {
Object[] apkAssets = (Object[]) VersionCompat.getGetApkAssetsMethod().invoke(asset);
for (Object apkAsset : apkAssets) {
String path = (String) VersionCompat.getGetAssetPathMethod().invoke(apkAsset);
existedAppResDirList.add(path);
}
} else {
Object[] appStringBlocks = (Object[]) VersionCompat.mStringBlocksInAssetManager().get(asset);
int totalResCount = appStringBlocks.length;
for (int appResIndex = 1; appResIndex <= totalResCount; ++appResIndex) {
String inApp = (String) VersionCompat.getGetCookieNameMethod().invoke(asset, appResIndex);
existedAppResDirList.add(inApp);
}
}
return existedAppResDirList;
}
private static class V21 extends VersionCompat {
private static void installSplitResDirs(Resources preResources, List<String> splitResPaths) throws Throwable {
Method method = VersionCompat.getAddAssetPathMethod();
for (String splitResPath : splitResPaths) {
method.invoke(preResources.getAssets(), splitResPath);
}
}
}
private static class V14 extends VersionCompat {
private static Context getBaseContext(Context context) {
Context ctx = context;
while (ctx instanceof ContextWrapper) {
ctx = ((ContextWrapper) ctx).getBaseContext();
}
return ctx;
}
private static void checkOrUpdateResourcesForContext(Context context, Resources preResources, Resources newResources)
throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
if (context instanceof ContextThemeWrapper && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Resources themeWrapperResources = (Resources) mResourcesInContextThemeWrapper().get(context);
if (themeWrapperResources == preResources) {
mResourcesInContextThemeWrapper().set(context, newResources);
mThemeInContextThemeWrapper().set(context, null);
}
}
//find base context
Context baseContext = getBaseContext(context);
if (baseContext.getClass().getName().equals("android.app.ContextImpl")) {
Resources baseContextRes = (Resources) mResourcesInContextImpl().get(baseContext);
if (baseContextRes == preResources) {
mResourcesInContextImpl().set(baseContext, newResources);
mThemeInContentImpl().set(baseContext, null);
}
} else {
try {
Resources baseContextRes = (Resources) HiddenApiReflection.findField(baseContext, "mResources").get(baseContext);
if (baseContextRes == preResources) {
HiddenApiReflection.findField(baseContext, "mResources").set(baseContext, newResources);
HiddenApiReflection.findField(baseContext, "mTheme").set(baseContext, null);
}
} catch (NoSuchFieldException e) {
}
Resources baseContextRes = (Resources) mResourcesInContextImpl().get(baseContext);
if (baseContextRes == preResources) {
mResourcesInContextImpl().set(baseContext, newResources);
mThemeInContentImpl().set(baseContext, null);
}
}
}
@SuppressLint("PrivateApi")
private static void installSplitResDirs(Context context, Resources preResources, List<String> splitResPaths) throws Throwable {
//create a new Resources.
Resources newResources = createResources(context, preResources, splitResPaths);
checkOrUpdateResourcesForContext(context, preResources, newResources);
Object activityThread = getActivityThread();
Map<IBinder, Object> activities = (Map<IBinder, Object>) mActivitiesInActivityThread().get(activityThread);
for (Map.Entry<IBinder, Object> entry : activities.entrySet()) {
Object activityClientRecord = entry.getValue();
Activity activity = (Activity) HiddenApiReflection.findField(activityClientRecord, "activity").get(activityClientRecord);
if (context != activity) {
SplitLog.i(TAG, "pre-resources found in @mActivities");
checkOrUpdateResourcesForContext(activity, preResources, newResources);
}
}
Map<Object, WeakReference<Resources>> activeResources;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
activeResources = (Map<Object, WeakReference<Resources>>) mActiveResourcesInActivityThread().get(activityThread);
} else {
Object resourcesManager = getResourcesManager();
activeResources = (Map<Object, WeakReference<Resources>>) mActiveResourcesInResourcesManager().get(resourcesManager);
}
for (Map.Entry<Object, WeakReference<Resources>> entry : activeResources.entrySet()) {
Resources res = entry.getValue().get();
if (res == null) {
continue;
}
if (res == preResources) {
activeResources.put(entry.getKey(), new WeakReference<>(newResources));
SplitLog.i(TAG, "pre-resources found in @mActiveResources");
break;
}
}
Map<String, WeakReference<Object>> instance_mPackages =
(Map<String, WeakReference<Object>>) mPackagesInActivityThread().get(activityThread);
for (Map.Entry<String, WeakReference<Object>> entry : instance_mPackages.entrySet()) {
Object packageInfo = entry.getValue().get();
if (packageInfo == null) {
continue;
}
Resources resources = (Resources) mResourcesInLoadedApk().get(packageInfo);
if (resources == preResources) {
SplitLog.i(TAG, "pre-resources found in @mPackages");
mResourcesInLoadedApk().set(packageInfo, newResources);
}
}
Map<String, WeakReference<Object>> instance_mResourcePackages =
(Map<String, WeakReference<Object>>) mResourcePackagesInActivityThread().get(activityThread);
for (Map.Entry<String, WeakReference<Object>> entry : instance_mResourcePackages.entrySet()) {
Object packageInfo = entry.getValue().get();
if (packageInfo == null) {
continue;
}
Resources resources = (Resources) mResourcesInLoadedApk().get(packageInfo);
if (resources == preResources) {
SplitLog.i(TAG, "pre-resources found in @mResourcePackages");
mResourcesInLoadedApk().set(packageInfo, newResources);
}
}
}
private static List<String> getAppResDirs(String appResDir, AssetManager asset) throws NoSuchFieldException,
IllegalAccessException, NoSuchMethodException, InvocationTargetException {
List<String> existedAppResDirList;
AssetManager sysAsset = Resources.getSystem().getAssets();
Object[] sysStringBlocks = (Object[]) mStringBlocksInAssetManager().get(sysAsset);
Object[] appStringBlocks = (Object[]) mStringBlocksInAssetManager().get(asset);
int totalResCount = appStringBlocks.length;
int sysResCount = sysStringBlocks.length;
existedAppResDirList = new ArrayList<>(totalResCount - sysResCount);
for (int appResIndex = sysResCount + 1; appResIndex <= totalResCount; ++appResIndex) {
String inApp = (String) getGetCookieNameMethod().invoke(asset, appResIndex);
existedAppResDirList.add(inApp);
}
if (!existedAppResDirList.contains(appResDir)) {
boolean inSystem = false;
for (int i = 1; i <= sysResCount; i++) {
final String cookieNameSys = (String) getGetCookieNameMethod().invoke(sysAsset, i);
if (appResDir.equals(cookieNameSys)) {
inSystem = true;
break;
}
}
if (!inSystem) {
existedAppResDirList.add(0, appResDir);
}
}
return existedAppResDirList;
}
private static Resources createResources(Context context, Resources oldRes, List<String> splitResPaths) throws NoSuchFieldException,
IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
String appResDir = context.getPackageResourcePath();
AssetManager oldAsset = oldRes.getAssets();
List<String> resDirs = getAppResDirs(appResDir, oldAsset);
resDirs.addAll(0, splitResPaths);
AssetManager newAsset = createAssetManager();
for (String recent : resDirs) {
int ret = (int) getAddAssetPathMethod().invoke(newAsset, recent);
if (ret == 0) {
throw new RuntimeException("invoke addAssetPath failure! apk format maybe incorrect");
}
}
return newResources(oldRes, newAsset);
}
private static Resources newResources(Resources originRes, AssetManager asset)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
return (Resources) HiddenApiReflection.findConstructor(originRes, AssetManager.class, DisplayMetrics.class, Configuration.class)
.newInstance(asset, originRes.getDisplayMetrics(), originRes.getConfiguration());
}
private static AssetManager createAssetManager() throws IllegalAccessException, InstantiationException {
return AssetManager.class.newInstance();
}
}
private static abstract class VersionCompat {
private static Field mStringBlocksField;
private static Method addAssetPathMethod;
private static Method getCookieNameMethod;
private static Method getAssetPathMethod;
private static Method getApkAssetsMethod;
private static Field mActivitiesInActivityThread;
private static Object activityThread;
private static Class<?> activityThreadClass;
private static Class<?> contextImplClass;
private static Field mResourcesInContextImpl;
private static Field mThemeInContentImpl;
private static Field mPackagesInActivityThread;
private static Field mResourcePackagesInActivityThread;
private static Field mActiveResourcesInActivityThread;
private static Field mActiveResourcesInResourcesManager;
private static Class<?> resourcesManagerClass;
private static Object resourcesManager;
private static Field mResourcesInContextThemeWrapper;
private static Field mThemeInContextThemeWrapper;
private static Class<?> loadedApkClass;
private static Field mResourcesInLoadedApk;
// ...
}
}