Android App Bundle(AAB) 是一种改进的应用程序打包方式,能大幅度减少应用体积。简而言之,可以理解 Google 官方的动态发布方案。
GitHub: https://gitee.com/mirrors/Qigsaw
由于国内无法直接使用 Google Play,从而也无法使用 AAB. 爱奇艺开源框架 Qigsaw 利用国内的插件化原理,实现了在国内使用 AAB 的动态交付的动态下载安装功能。
Qigsaw 是一套基于 Android App Bundles 实现的 Android 动态组件化方案,它无需应用重新安装即可动态分发插件。
Qigsaw 的核心优势:
已知问题:
看看 58App 利用 Qigsaw 实现的 AAB 动态下载安装效果:
接下来来看一下 Qigsaw 的实现原理。
Qigsaw 提供了两个 gradle 插件:
我这边进行了提炼,它们主要做的工作包括如下几块:
组件 | 处理 |
---|---|
Activity | getResources() 方法注入 SplitInstallHelper.loadResources(…), 解决访问插件资源的问题 |
Service | onCreate() 函数中插入 SplitInstallHelper.loadResources(…),解决访问插件资源的问题 |
BroadcastReceiver | onCreate() 函数中插入 SplitInstallHelper.loadResources(…),解决访问插件资源的问题 |
ContentProvider | 为每个 ContentProvider 创建名为 providerName + “Decorated” + splitName 的代理类,其中 providerName 为原始 provider 类名,splitName 为插件 apk 对应的名称,并且该类继承 SplitContentProvider |
可以看到,Activity, Service, BroadcastReceiver 的处理都是为了解决资源访问问题,而 ContentProvider 的处理,是替换成代理类,这么做的原因是在 app 启动时 ContentProvider 的执行时机是比较靠前的: ContentProvider 的初始化位于 Application 的 attachBaseContext 和 onCreate 之间,在这个过程中插件 apk 并没有加载进来,一定会报 ClassNotFound。所以将插件 apk 的 provider 生成一个代理类,然后替换掉,如果插件没有加载进来,代理类什么也不执行即可。
新增 class | 说明 |
---|---|
javaSplitLibraryLoader | 用于多 ClassLoader 模式下,调用插件自身的 ClassLoader 加载 so |
ComponentInfo | 用于存储插件四大组件信息 |
ContentProvider 代理类 | 用于解决 ContentProvider 初始化问题 |
SplitLibraryLoader | 处理多 classloader 场景下 so 加载的问题 |
QigsawConfig | 包含 Qjigsaw 模式,id, 版本号,feature 数组配置 |
每个应用了 Dynamic Feature Plugin 的库会变成动态 feature 库,动态 feature 库编译的产物的 Apk 文件。可以配置是内置还是动态下载安装。如果配置为动态下载安装,则会将对应的 Apk 文件上传到 CDN,否则会打包到基础包的 Apk 中。
splitUpload {
// 配置上传接口
uploadUrl = "https:xxxxxx"
}
实现 Qigsaw 对外暴露的上传接口,上传成功返回动态 Apk 的上传地址,写入到配置文件中。
生成 Qigsaw 配置文件,文件中包含版本信息,以及每个动态库模块的信息(是否内置,下载地址,Apk 文件 md5 …),如下所示:
{
"qigsawId": "9.11.0_0593ffca2",
"appVersionName": "9.11.0",
"builtInUrlPrefix": "native://",
"splits": [{
"splitName": "WubaPincheFeature",
"url": "native://libsplit_WubaPincheFeature.so",
"builtIn": true,
"onDemand": false,
"size": 558633,
"version": "1.0@1",
"md5": "1c338962f8a89edc48f2c1ac95b71649",
"minSdkVersion": 19,
"dexNumber": 3
}, {
"splitName": "WubaJobFeature",
"url": "http://10.252.209.45:3007/file/WubaJobFeature.apk",
"builtIn": false,
"onDemand": true,
"size": 5016932,
"applicationName": "com.wuba.jobfeature.JobApp",
"version": "1.0@1",
"md5": "970d88c082e7fc9647978a85b3657542",
"minSdkVersion": 19,
"dexNumber": 3,
"nativeLibraries": [{
"abi": "armeabi-v7a",
"jniLibs": [{
"name": "libaesutil.so",
"md5": "c26a5631ebad7ec47d19c837328526e4",
"size": 17960
}]
}]
}, {
"splitName": "WubaCarFeature",
"url": "http://10.252.209.45:3007/file/WubaCarFeature.apk",
"builtIn": false,
"onDemand": true,
"size": 5415268,
"applicationName": "com.wuba.carfeature.CarApp",
"version": "1.0@1",
"md5": "18b57baf0dae6745e8de3b2b27f6a937",
"minSdkVersion": 19,
"dexNumber": 3
}
],
"abiFilters": ["armeabi-v7a"]
}
内置的模块,如果是多 abi 架构,则会以 zip 格式内置在 assets 中,如果是单 abi 架构,则会以 so 格式内置在 libs 中。
最后对基础包 Apk 进行重打包签名处理。
动态安装时,需要传递安装下载的 module 名称和回调接口。安装下载过程会判断 split 是内置的还是需要远程下载的,如果是非内置,则会开启跨进程服务进行下载,下载成功后执行安装流程。
拷贝:
判断 split 动态包是存放在 libs 还是 assets 中,如果是存放在 assets,则拷贝到 data 的指定目录,存放在 libs 中不需要进行拷贝操作。安装完成会在 /data/app 包名/app_qigsaw/ 目录生成对应的文件夹,如安装 native 模块,则会生成 native 目录:
校验:
拷贝完成后,进行校验,会校验两个部分:签名和 split 文件的 md5,签名校验是可选的,而 md5 校验是必须的
以上两步为安装前处理,接下来进入安装流程。
首次安装为顺序触发内置->安装/下载->安装的流程,二次安装为 App 启动时,加载已安装模块。
替换 App 默认的 classloader: 代理包装自定义的 PathClassLoader, 替换掉当前 LoadApk 的 classLoader. 特别注意,运行时在 class 加载失败时,如果 class 是 Activity, Service, BroadcastReceiver,则会返回一个对应类型的 Fake class, 然后可以在 Fake class 中加载安装加载对应的组件。
final class SplitDelegateClassloader extends PathClassLoader {
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);
}
}
// ...
@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
protected URL findResource(String name) {
URL resource = super.findResource(name);
if (resource == null) {
Set<SplitDexClassLoader> splitDexClassLoaders = SplitApplicationLoaders.getInstance().getClassLoaders();
for (SplitDexClassLoader loader : splitDexClassLoaders) {
resource = loader.findResourceItself(name);
if (resource != null) {
break;
}
}
}
return resource;
}
@Override
protected Enumeration<URL> findResources(String name) {
Enumeration<URL> resources = super.findResources(name);
if (resources == null) {
Set<SplitDexClassLoader> splitDexClassLoaders = SplitApplicationLoaders.getInstance().getClassLoaders();
for (SplitDexClassLoader loader : splitDexClassLoaders) {
resources = loader.findResourcesItself(name);
if (resources != null) {
break;
}
}
}
return resources;
}
@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;
}
}
单 classloader 安装实现:
从 android 7.0(25), android 6.0(23) 等做了不同的适配,反射获取当前的 classloader 的 pathList, 将插件的 so path 添加进去。适配不同 API,反射 classloader 的 pathList -> dexElements,将 dex 加进去。
/**
* 单 classloder 安装
*/
final class SplitLoaderImpl2 extends SplitLoader {
SplitLoaderImpl2(Context context) {
super(context);
}
@Override
void loadCode2(@Nullable List<String> dexPaths,
File optimizedDirectory,
@Nullable File librarySearchPath) throws SplitLoadException {
ClassLoader curCl = SplitLoader.class.getClassLoader();
loadLibrary(curCl, librarySearchPath);
loadDex(curCl, dexPaths, optimizedDirectory);
}
private void loadLibrary(ClassLoader classLoader, File librarySearchPath) throws SplitLoadException {
if (librarySearchPath != null) {
try {
SplitCompatLibraryLoader.load(classLoader, librarySearchPath);
} catch (Throwable cause) {
throw new SplitLoadException(SplitLoadError.LOAD_LIB_FAILED, cause);
}
}
}
private void loadDex(ClassLoader classLoader, List<String> dexPaths, File optimizedDirectory) throws SplitLoadException {
if (dexPaths != null) {
List<File> dexFiles = new ArrayList<>(dexPaths.size());
for (String dexPath : dexPaths) {
dexFiles.add(new File(dexPath));
}
try {
SplitCompatDexLoader.load(classLoader, optimizedDirectory, dexFiles);
SplitUnKnownFileTypeDexLoader.loadDex(classLoader, dexPaths, optimizedDirectory);
} catch (Throwable cause) {
throw new SplitLoadException(SplitLoadError.LOAD_DEX_FAILED, cause);
}
}
}
}
/**
* 单 classloder 安装 so
*/
final class SplitCompatLibraryLoader {
static void load(ClassLoader classLoader, File folder)
throws Throwable {
if (folder == null || !folder.exists()) {
return;
}
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!");
}
}
}
/**
* 单 classloder 安装 dex
*/
final class SplitCompatDexLoader {
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();
}
}
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;
}
}
}
// ...
}
多 classloader 安装实现:
使用 DexClassLoader, 每个插件 new 一个 DexClassLoader, 然后使用这个 DexClassLoader 加载插件的代码。所有插件的 DexClassLoader 都会缓存起来。 而且该自定义的 DexClassLoader 重写了 findClass, findResources, findLibrary, 加载异常时去分别从插件它依赖的 classloader 中尝试加载 (其他 split classloader)。
final class SplitDexClassLoader extends BaseDexClassLoader {
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);
}
static SplitDexClassLoader create(String moduleName,
List<String> dexPaths,
File optimizedDirectory,
File librarySearchFile,
List<String> dependencies) throws Throwable {
SplitDexClassLoader cl = new SplitDexClassLoader(
moduleName,
dexPaths,
optimizedDirectory,
librarySearchFile == null ? null : librarySearchFile.getAbsolutePath(),
dependencies,
SplitDexClassLoader.class.getClassLoader()
);
return cl;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
} catch (ClassNotFoundException e1) {
if (dependenciesLoaders != null) {
for (SplitDexClassLoader loader : dependenciesLoaders) {
try {
return loader.loadClassItself(name);
} catch (ClassNotFoundException e2) {
SplitLog.w(TAG, "SplitDexClassLoader: Class %s is not found in %s ClassLoader", name, loader.moduleName());
}
}
}
throw e1;
}
}
String moduleName() {
return moduleName;
}
@Override
public String findLibrary(String name) {
String libName = super.findLibrary(name);
if (libName == null) {
if (dependenciesLoaders != null) {
for (SplitDexClassLoader loader : dependenciesLoaders) {
libName = loader.findLibrary(name);
if (libName != null) {
break;
}
}
}
}
if (libName == null) {
if (getParent() instanceof BaseDexClassLoader) {
libName = ((BaseDexClassLoader) getParent()).findLibrary(name);
}
}
return libName;
}
// ...
}
5.0 以上,反射调用 AssetManager.addAssetPath, 5.0 以下使用反射创建 Resources, 传入当前宿主的 resources 与插件的 resource path, 进行 add 添加,最后将 ContextImpl,或者当前 context 的 resources 对象替换成这个新的 resources 对象.
@RestrictTo(LIBRARY_GROUP)
public class SplitCompatResourcesLoader {
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 {
// ...
}
}
}
/**
* 反射 AssetManager.addAssetPath 进行添加
*/
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 void installSplitResDirs(Context context, Resources preResources, List<String> splitResPaths) throws Throwable {
//create a new Resources.
Resources newResources = createResources(context, preResources, splitResPaths);
// ...
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);
}
}
}
}
}
由于 AAB 的特性,会在编译期间提前合并四大组件的清单配置,所以不支持四大组件的热更新。这样也就没有别的插件化框架针对四大组件热更新的处理:包括预埋、代理、hook ASM 等,所以 Qigsaw 框架整体 hook 少,稳定性强。
安装完成后,会激活插件的 Applicaton, 调用其 attach():
public void createAndActiveSplitApplication(Context appContext, boolean qigsawMode) {
if (qigsawMode) {
return;
}
final Set<String> aabLoadedSplits = new SplitAABInfoProvider(appContext).getInstalledSplitsForAAB();
if (!aabLoadedSplits.isEmpty()) {
for (String splitName : aabLoadedSplits) {
try {
Application app = createApplication(AABExtension.class.getClassLoader(), splitName);
if (app != null) {
activeApplication(app, appContext);
aabApplications.add(app);
}
} catch (AABExtensionException e) {
SplitLog.w(TAG, "Failed to create " + splitName + " application", e);
}
}
}
}
启动 Application 时,将已安装的 split,根据对应的 classloader 来加载插件。
abstract class SplitLoadTask implements Runnable {
// ...
@Override
public final void run() {
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
loadSplitInternal();
} else {
// ...
}
}
private void loadSplitInternal() {
SplitLoader loader = createSplitLoader();
Set<Split> splits = new HashSet<>();
List<SplitLoadError> loadErrors = new ArrayList<>(0);
List<SplitBriefInfo> splitBriefInfoList = new ArrayList<>(splitFileIntents.size());
for (Intent splitFileIntent : splitFileIntents) {
String splitName = splitFileIntent.getStringExtra(SplitConstants.KET_NAME);
SplitInfo info = infoManager.getSplitInfo(appContext, splitName);
if (info == null) {
continue;
}
// 加载 resources
String splitApkPath = splitFileIntent.getStringExtra(SplitConstants.KEY_APK);
try {
loader.loadResources(splitApkPath);
} catch (SplitLoadException e) {
SplitLog.printErrStackTrace(TAG, e, "Failed to load split %s resources!", splitName);
loadErrors.add(new SplitLoadError(splitBriefInfo, e.getErrorCode(), e.getCause()));
continue;
}
// 加载 dex (class 和 so)
List<String> addedDexPaths = splitFileIntent.getStringArrayListExtra(SplitConstants.KEY_ADDED_DEX);
File optimizedDirectory = SplitPathManager.require().getSplitOptDir(info);
File librarySearchPath = null;
if (info.hasLibs()) {
librarySearchPath = SplitPathManager.require().getSplitLibDir(info);
}
File splitDir = SplitPathManager.require().getSplitDir(info);
ClassLoader classLoader;
try {
classLoader = loadCode(loader, splitName, addedDexPaths, optimizedDirectory, librarySearchPath, info.getDependencies());
} catch (SplitLoadException e) {
continue;
}
// 激活 split, 包含插件的 applicaton 以及 contentProvider
try {
activator.activate(classLoader, splitName);
} catch (SplitLoadException e) { onSplitActivateFailed(classLoader);
continue;
}
// ...
splits.add(new Split(splitName, splitApkPath));
}
loadManager.putSplits(splits);
reportLoadResult(splitBriefInfoList, loadErrors, System.currentTimeMillis() - time);
}
}
Qigsaw 热更新:
App 启动时会请求服务器检查当前版本是否有更新的配置文件,如果有,则会下载更新配置文件,然后在对应的动态包中进行下载更新。