摘要: 深刻剖析Android官方新模块化方案——App Bundles,技术要点全方位挖掘。java
App Bundles是2018 Google I/O新引入的一个概念。不一样于以往传统的App是以一个完整的Apk做为单位,而App Bundles有下面几个技术特色:android
一图胜千言,shell
App Bundles能够在运行时选取设备所需的APK组合运行,与传统方式对好比下:api
须要上传到Play商店的是一个Android App Bundle(.aab)文件,经过Android Studio中的Build > Build Bundle(s)/APK(s) > Build Bundle(s)
能够在project-name/module-name/build/outputs/bundle/
目录下生成。它有以下结构:安全
在我本身构造demo中,有base和feature0模块,在base中没法直接引用feature0模块的类,反过来是能够的。session
能够看到这个demo生成的aab文件内容是这样的:app
Archive: app/build/outputs/bundle/debug/bundle.aab Length Date Time Name -------- ---- ---- ---- 67086 05-17-18 13:21 META-INF/MANIFEST.MF 67248 05-17-18 13:21 META-INF/ANDROIDD.SF 773 05-17-18 13:21 META-INF/ANDROIDD.RSA 4056 05-17-18 13:21 base/manifest/AndroidManifest.xml 541 05-17-18 13:21 base/res/anim/abc_fade_in.xml 541 05-17-18 13:21 base/res/anim/abc_fade_out.xml ... ... 448151 05-17-18 13:21 base/resources.pb 3121604 05-17-18 13:21 base/dex/classes.dex 7 05-17-18 13:21 base/root/META-INF/com.android.support_appcompat-v7.version 7 05-17-18 13:21 base/root/META-INF/com.android.support_support-core-utils.version ... ... 182504 05-17-18 13:21 base/lib/arm64-v8a/libnative-lib.so 104160 05-17-18 13:21 base/lib/armeabi-v7a/libnative-lib.so 177924 05-17-18 13:21 base/lib/x86/libnative-lib.so 174712 05-17-18 13:21 base/lib/x86_64/libnative-lib.so 85 05-17-18 13:21 base/native.pb 1719 05-17-18 13:21 feature0/manifest/AndroidManifest.xml 375 05-17-18 13:21 feature0/res/layout/feature0_activity.xml 1272 05-17-18 13:21 feature0/res/layout/feature0_fragment.xml 573 05-17-18 13:21 feature0/resources.pb 4192 05-17-18 13:21 feature0/dex/classes.dex 27292 05-17-18 13:21 feature0/dex/classes2.dex 6 05-17-18 13:21 feature0/root/META-INF/android.arch.lifecycle_extensions.version 6 05-17-18 13:21 feature0/root/META-INF/android.arch.lifecycle_livedata.version 273 05-17-18 13:21 BundleConfig.pb -------- ------- 4977479 528 files
Play商店会根据这个文件自动生成基线APK与其余模块APK。async
另外Google也提供了bundletool调试工具,咱们经过执行如下命令,能够生成一个.apks
文件:编辑器
bundletool build-apks --bundle=app/build/outputs/bundle/debug/bundle.aab --output=bundle.apks
.apks
文件的内容是这样的:ide
Length Date Time Name -------- ---- ---- ---- 56679 05-11-18 10:32 base-xhdpi.apk 47738 05-11-18 10:32 base-ldpi.apk 51733 05-11-18 10:32 base-hdpi.apk 47350 05-11-18 10:32 base-mdpi.apk 67465 05-11-18 10:32 base-xxhdpi.apk 71691 05-11-18 10:32 base-xxxhdpi.apk 85949 05-11-18 10:32 base-tvdpi.apk 58942 05-11-18 10:32 base-armeabi_v7a.apk 75591 05-11-18 10:32 base-arm64_v8a.apk 79456 05-11-18 10:32 base-x86.apk 76614 05-11-18 10:32 base-x86_64.apk 1278608 05-11-18 10:32 base-master.apk 6456 05-11-18 10:32 feature0-ldpi.apk 6455 05-11-18 10:32 feature0-mdpi.apk 6453 05-11-18 10:32 feature0-hdpi.apk 6455 05-11-18 10:32 feature0-xhdpi.apk 6458 05-11-18 10:32 feature0-xxhdpi.apk 6459 05-11-18 10:32 feature0-xxxhdpi.apk 6459 05-11-18 10:32 feature0-tvdpi.apk 23840 05-11-18 10:32 feature0-master.apk 1399736 05-11-18 10:32 standalone-arm64_v8a_mdpi.apk 1404122 05-11-18 10:32 standalone-arm64_v8a_hdpi.apk 1400150 05-11-18 10:32 standalone-arm64_v8a_ldpi.apk 1409067 05-11-18 10:32 standalone-arm64_v8a_xhdpi.apk 1419850 05-11-18 10:32 standalone-arm64_v8a_xxhdpi.apk 1383502 05-11-18 10:32 standalone-armeabi_v7a_ldpi.apk 1424094 05-11-18 10:32 standalone-arm64_v8a_xxxhdpi.apk 1438398 05-11-18 10:32 standalone-arm64_v8a_tvdpi.apk 1383096 05-11-18 10:32 standalone-armeabi_v7a_mdpi.apk 1387471 05-11-18 10:32 standalone-armeabi_v7a_hdpi.apk 1392416 05-11-18 10:32 standalone-armeabi_v7a_xhdpi.apk 1403202 05-11-18 10:32 standalone-armeabi_v7a_xxhdpi.apk 1407436 05-11-18 10:32 standalone-armeabi_v7a_xxxhdpi.apk 1403612 05-11-18 10:32 standalone-x86_mdpi.apk 1404026 05-11-18 10:32 standalone-x86_ldpi.apk 1421745 05-11-18 10:32 standalone-armeabi_v7a_tvdpi.apk 1407995 05-11-18 10:32 standalone-x86_hdpi.apk 1412940 05-11-18 10:32 standalone-x86_xhdpi.apk 1427968 05-11-18 10:32 standalone-x86_xxxhdpi.apk 1423722 05-11-18 10:32 standalone-x86_xxhdpi.apk 1401172 05-11-18 10:32 standalone-x86_64_ldpi.apk 1442270 05-11-18 10:32 standalone-x86_tvdpi.apk 1400762 05-11-18 10:32 standalone-x86_64_mdpi.apk 1405145 05-11-18 10:32 standalone-x86_64_hdpi.apk 1420877 05-11-18 10:32 standalone-x86_64_xxhdpi.apk 1410090 05-11-18 10:32 standalone-x86_64_xhdpi.apk 1425119 05-11-18 10:32 standalone-x86_64_xxxhdpi.apk 1439423 05-11-18 10:32 standalone-x86_64_tvdpi.apk 6367 05-11-18 10:32 toc.pb -------- ------- 41572624 49 files
能够推测,这个就是上传到商店以后最终生成的须要按需下发的APK产物。
由于众所周知的缘由,国内的设备不会默认包含Google Play商店,所以App Bundles对于国内App是不适用的。因此咱们考虑,是否能够不经由商店,在App自身开发一套相似的更新机制?
咱们要想自行实现这个功能,就得探清其原理,知其因此然。
在本地安装以后,咱们发如今APK安装路径下存在两个APK,
gemini:/ # ls -l data/app/com.taobao.myappbundledemo-2/ total 4664 -rw-r--r-- 1 system system 2343434 2018-05-13 12:41 base.apk drwxr-xr-x 3 system system 4096 2018-05-13 12:41 lib drwxrwx--x 3 system install 4096 2018-05-13 12:41 oat -rw-r--r-- 1 system system 25412 2018-05-13 12:41 split_feature0.apk
这里确实有些违反常理,一般咱们一个App只有一个APK,而这里咱们能够看到,在一个App的安装目录下出现了多个APK。实际上,安装目录能够安装多Apk的特性是Android 5.0开始引入的,这也就解释了为什么4.4如下机型只能安装一个单独的完整包。那么,这种多个APK是以什么方式安装的?咱们是否能够模仿这种安装方式对App自身进行更新呢?
咱们先看下官方SDK采用的方式。首先,若是想经过商店请求安装新模块,能够经过下面的代码进行:
// Creates an instance of SplitInstallManager. SplitInstallManager splitInstallManager = SplitInstallManagerFactory.create(context); // Creates a request to install a module. SplitInstallRequest request = SplitInstallRequest .newBuilder() // You can download multiple on demand modules per // request by invoking the following method for each // module you want to install. .addModule("pictureMessages") .addModule("promotionalFilters") .build(); splitInstallManager // Submits the request to install the module through the // asynchronous startInstall() task. Your app needs to be // in the foreground to submit the request. .startInstall(request) // You should also be able to gracefully handle // request state changes and errors. To learn more, go to // the section about how to Monitor the request state. .addOnSuccessListener(sessionId -> { ... }) .addOnFailureListener(errorCode -> { ... });
使用这个类须要引入依赖com.google.android.play:core:1.2.0
这里的关键天然是startInstall
,咱们就顺藤摸瓜往下挖。
final class k { private static final b b = new b("SplitInstallService"); private static final Intent c = new Intent("com.google.android.play.core.splitinstall.BIND_SPLIT_INSTALL_SERVICE").setPackage("com.android.vending"); final com.google.android.play.core.a.b<a> a; private final Context d; private final String e; private final f f; private k(Context context, String str) { this.f = new j(this); this.d = context; this.e = str; this.a = new com.google.android.play.core.a.b(context.getApplicationContext(), b, "SplitInstallService", c, l.a, this.f); } public final Task<Integer> a(Collection<String> collection) { b.a("startInstall(%s)", collection); i iVar = new i(); this.a.a(new m(this, iVar, collection, iVar)); return iVar.a(); } ... ...
public final class b<T extends IInterface> { private static final Map<String, Handler> a = Collections.synchronizedMap(new HashMap()); private final Context b; private final com.google.android.play.core.splitcompat.b.b c; private final String d; private final List<a> e = new ArrayList(); private boolean f; private final Intent g; private final g<T> h; private final WeakReference<f> i; private final DeathRecipient j = new c(this); private ServiceConnection k; private T l; public b(Context context, com.google.android.play.core.splitcompat.b.b bVar, String str, Intent intent, g<T> gVar, f fVar) { this.b = context; this.c = bVar; this.d = str; this.g = intent; this.h = gVar; this.i = new WeakReference(fVar); } private final void b(a aVar) { if (this.l == null && !this.f) { this.c.a("Initiate binding to the service.", new Object[0]); this.e.add(aVar); this.k = new h(); this.f = true; if (!this.b.bindService(this.g, this.k, 1)) { this.c.a("Failed to bind to the service.", new Object[0]); this.f = false; for (a a : this.e) { i a2 = a.a(); if (a2 != null) { a2.a(new k()); } } this.e.clear(); } } else if (this.f) { this.c.a("Waiting to bind to the service.", new Object[0]); this.e.add(aVar); } else { aVar.run(); } } ... ...
能够看到class k的a字段是一个class b,在这个class b的构造函数中,经过com.google.android.play.core.splitinstall.BIND_SPLIT_INSTALL_SERVICE
绑定了一个Service,然而这个Service并不在App自身,而是属于Play商店的一个Service.
咱们反编译商店,发现确实存在com.google.android.play.core.splitinstall.BIND_SPLIT_INSTALL_SERVICE
的filter。
<service android:name="com.google.android.finsky.splitinstallservice.SplitInstallService" android:enabled="true" android:exported="true" android:visibleToInstantApps="true"> <meta-data android:name="instantapps.clients.allowed" android:value="true"/> <intent-filter> <action android:name="com.google.android.play.core.splitinstall.BIND_SPLIT_INSTALL_SERVICE"/> </intent-filter> </service>
其对应的正是SplitInstallService
,因为这里混淆比较严重,很难往下继续跟了,不过咱们能够知道的是,安装APK的操做确实是经过请求Play商店的Service进行的。
接下来咱们转换一下思路继续探索。
使用bundletool调试工具能够直接安装apks,命令以下:
bundletool install-apks --apks=bundle.apks
经过跟进bundletool源码,咱们最终找到了关键方法:
private String createMultiInstallSession(List<File> apkFiles, String pmOptions, long timeout, TimeUnit unit) throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { long totalFileSize = 0; for (File apkFile : apkFiles) { totalFileSize += apkFile.length(); } InstallCreateReceiver receiver = new InstallCreateReceiver(); this.mDevice.executeShellCommand(String.format(this.mPrefix + " install-create %1$s -S %2$d", new Object[]{pmOptions, Long.valueOf(totalFileSize)}), receiver, timeout, unit); return receiver.getSessionId(); } private boolean uploadApk(String sessionId, File fileToUpload, int uniqueId, long timeout, TimeUnit unit) { Throwable e; Throwable th; Log.d(sessionId, String.format("Uploading APK %1$s ", new Object[]{fileToUpload.getPath()})); if (!fileToUpload.exists()) { Log.e(sessionId, String.format("File not found: %1$s", new Object[]{fileToUpload.getPath()})); return false; } else if (fileToUpload.isDirectory()) { Log.e(sessionId, String.format("Directory upload not supported: %1$s", new Object[]{fileToUpload.getAbsolutePath()})); return false; } else { String baseName; if (fileToUpload.getName().lastIndexOf(46) != -1) { baseName = fileToUpload.getName().substring(0, fileToUpload.getName().lastIndexOf(46)); } else { baseName = fileToUpload.getName(); } baseName = UNSAFE_PM_INSTALL_SESSION_SPLIT_NAME_CHARS.replaceFrom(baseName, '_'); Log.d(sessionId, String.format("Executing : %1$s", new Object[]{String.format(this.mPrefix + " install-write -S %d %s %d_%s -", new Object[]{Long.valueOf(fileToUpload.length()), sessionId, Integer.valueOf(uniqueId), baseName})})); ... ... } } public void install(long timeout, TimeUnit unit) throws InstallException { try { String sessionId = createMultiInstallSession(this.mApks, this.mOptions, timeout, unit); if (sessionId == null) { Log.d(LOG_TAG, "Failed to establish session, quit installation"); throw new InstallException("Failed to establish session"); } int index = 0; boolean allUploadSucceeded = true; while (allUploadSucceeded) { if (index >= this.mApks.size()) { break; } int index2 = index + 1; allUploadSucceeded = uploadApk(sessionId, (File) this.mApks.get(index), index, timeout, unit); index = index2; } String command = this.mPrefix + " install-" + (allUploadSucceeded ? "commit " : "abandon ") + sessionId; InstallReceiver receiver = new InstallReceiver(); this.mDevice.executeShellCommand(command, receiver, timeout, unit); if (receiver.getErrorMessage() != null) { String message = String.format("Failed to finalize session : %1$s", new Object[]{receiver.getErrorMessage()}); Log.e(LOG_TAG, message); throw new InstallException(message); } else if (!allUploadSucceeded) { throw new InstallException("Failed to install all "); } } catch (InstallException e) { throw e; } catch (Throwable e2) { throw new InstallException(e2); } }
可见其真正使用的命令是如下三条:
adb shell pm install-create ... adb shell pm install-write ... adb shell pm install-commit ...
咱们继续看下Pm.java,具体看下这三个命令是如何实现安装的。
frameworks-base-p-preview-1/cmds/pm/src/com/android/commands/pm/Pm.java public int run(String[] args) throws RemoteException { ... ... mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package")); mInstaller = mPm.getPackageInstaller(); ... ... if ("install-create".equals(op)) { return runInstallCreate(); } if ("install-write".equals(op)) { return runInstallWrite(); } if ("install-commit".equals(op)) { return runInstallCommit(); } ... ... } private int runInstallCreate() throws RemoteException { final InstallParams installParams = makeInstallParams(); final int sessionId = doCreateSession(installParams.sessionParams, installParams.installerPackageName, installParams.userId); // NOTE: adb depends on parsing this string System.out.println("Success: created install session [" + sessionId + "]"); return PackageInstaller.STATUS_SUCCESS; } private int doCreateSession(SessionParams params, String installerPackageName, int userId) throws RemoteException { userId = translateUserId(userId, "runInstallCreate"); if (userId == UserHandle.USER_ALL) { userId = UserHandle.USER_SYSTEM; params.installFlags |= PackageManager.INSTALL_ALL_USERS; } final int sessionId = mInstaller.createSession(params, installerPackageName, userId); return sessionId; } private int runInstallWrite() throws RemoteException { long sizeBytes = -1; String opt; while ((opt = nextOption()) != null) { if (opt.equals("-S")) { sizeBytes = Long.parseLong(nextArg()); } else { throw new IllegalArgumentException("Unknown option: " + opt); } } final int sessionId = Integer.parseInt(nextArg()); final String splitName = nextArg(); final String path = nextArg(); return doWriteSession(sessionId, path, sizeBytes, splitName, true /*logSuccess*/); } private int doWriteSession(int sessionId, String inPath, long sizeBytes, String splitName, boolean logSuccess) throws RemoteException { if (STDIN_PATH.equals(inPath)) { inPath = null; } else if (inPath != null) { final File file = new File(inPath); if (file.isFile()) { sizeBytes = file.length(); } } final SessionInfo info = mInstaller.getSessionInfo(sessionId); PackageInstaller.Session session = null; InputStream in = null; OutputStream out = null; try { session = new PackageInstaller.Session( mInstaller.openSession(sessionId)); if (inPath != null) { in = new FileInputStream(inPath); } else { in = new SizedInputStream(System.in, sizeBytes); } out = session.openWrite(splitName, 0, sizeBytes); int total = 0; byte[] buffer = new byte[65536]; int c; while ((c = in.read(buffer)) != -1) { total += c; out.write(buffer, 0, c); if (info.sizeBytes > 0) { final float fraction = ((float) c / (float) info.sizeBytes); session.addProgress(fraction); } } session.fsync(out); if (logSuccess) { System.out.println("Success: streamed " + total + " bytes"); } return PackageInstaller.STATUS_SUCCESS; } catch (IOException e) { System.err.println("Error: failed to write; " + e.getMessage()); return PackageInstaller.STATUS_FAILURE; } finally { IoUtils.closeQuietly(out); IoUtils.closeQuietly(in); IoUtils.closeQuietly(session); } }
由此可知,pm是经过PackageInstaller
类来实现安装的。
知道了这个流程后,咱们就能够本身实现这种安装方法了:
PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams( PackageInstaller.SessionParams.MODE_INHERIT_EXISTING); PackageInstaller installer = context.getPackageManager().getPackageInstaller(); int sessionId = installer.createSession(sessionParams); PackageInstaller.Session session = installer.openSession(sessionId); File apkFile = new File(getFilesDir(), "feature0-debug.apk"); in = new FileInputStream(apkFile.getPath()); out = session.openWrite("anything", 0, apkFile.length()); int total = 0; byte[] buffer = new byte[65536]; int c; while ((c = in.read(buffer)) != -1) { total += c; out.write(buffer, 0, c); } session.fsync(out); IntentSender intentSender = createIntentSender(context, sessionId); session.commit(intentSender); session.close();
这里的重点是,须要在建立SessionParams
的时候指定模式为MODE_INHERIT_EXISTING
,才能覆盖安装模块APK。模块APK的名称为split_<module name>.apk
。这里的<module name>
是模块APK的AndroidManifest中指定的,<manifest ... ... package="com.taobao.myappbundledemo" split="feature0">
,所以,系统会自动识别安装位置并肯定安装后的文件名称,若是manifest中不带split,其名称就是base.apk
。
调用完上述代码,并非很顺利地直接装上了,而是弹出了几个界面:
容许未知安装来源提示:
容许该App进行安装:
确认安装:
容许未知来源只须要一次,确认安装窗口在每次进行安装操做的时候都会弹出。能够看到,安装操做执行完毕后,split_feature0.apk
确实发生了更新,而不是以前旧的,由于能够比较出与base.apk
的安装的时间不一样:
angler:/ # ls -l data/app/com.taobao.myappbundledemo-6XP2HdNBRWLquWr3U15iJA\=\=/ total 2376 -rw-r--r-- 1 system system 2392626 2018-05-16 12:04 base.apk drwxr-xr-x 3 system system 4096 2018-05-16 12:18 lib drwxrwx--x 3 system install 4096 2018-05-16 12:18 oat -rw-r--r-- 1 system system 25412 2018-05-16 12:18 split_feature0.apk
然而还有个问题,在安装执行后,App会立马被杀死。而根据官方文档,在Android 7.0及以上版本的设备是能够直接请求安装模块后当即进行使用,若是直接杀死,那确定体验很糟糕。所以应该能够找到办法,在安装后不杀当前进程。
然而从PackageInstaller.SessionParams
接口中咱们并无发现相关设置,是否有隐藏的API能够作到呢?咱们经过反编译Play商店来寻找答案。
private final synchronized void a(String str, long j, String str2, Bitmap bitmap, int i, int i2, fk fkVar, int i3) { if (this.c.containsKey(str)) { FinskyLog.e("Creating session for %s when one already exists", str); } else { SessionParams sessionParams = new SessionParams(i3); if (i3 == 2 && android.support.v4.os.a.a()) { sessionParams.setDontKillApp(true); } if (bitmap != null) { sessionParams.setAppIcon(bitmap); } if (!TextUtils.isEmpty(str2)) { sessionParams.setAppLabel(str2); } sessionParams.setAppPackageName(str); sessionParams.setInstallLocation(i); if (j > 0) { sessionParams.setSize(j); } if (android.support.v4.os.a.b()) { sessionParams.setInstallReason(i2); } ... ...
sessionParams.setDontKillApp
彷佛正是咱们须要的,它也的确不在公开API中,因此编辑器里面没法直接引用,咱们经过反射打开它:
sessionParams.getClass().getDeclaredMethod("setDontKillApp", boolean.class).invoke(sessionParams, true);
这样果真实现了安装后不杀进程。
还有一个疑问,虽然不杀死进程,但新安装的APK是否能够直接被如今的进程当即使用到呢?经过查看进程空间,咱们发现,
7265874000-7265876000 r--p 00000000 fd:00 1439142 /data/app/com.taobao.myappbundledemo-I1BztqjLDnwA1LZ7mxP6kg==/oat/arm64/split_feature0.odex 7265876000-7265879000 rw-p 00000000 00:00 0 [anon:.bss] 7265879000-726587a000 r--p 00002000 fd:00 1439142 /data/app/com.taobao.myappbundledemo-I1BztqjLDnwA1LZ7mxP6kg==/oat/arm64/split_feature0.odex 726587a000-726587b000 rw-p 00003000 fd:00 1439142 /data/app/com.taobao.myappbundledemo-I1BztqjLDnwA1LZ7mxP6kg==/oat/arm64/split_feature0.odex ... ... 7265ce1000-7265d0b000 r-xp 00000000 fd:00 1439025 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/lib/arm64/libnative-lib.so (deleted) 7265d0b000-7265d1b000 ---p 00000000 00:00 0 7265d1b000-7265d1d000 r--p 0002a000 fd:00 1439025 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/lib/arm64/libnative-lib.so (deleted) 7265d1d000-7265d1e000 rw-p 0002c000 fd:00 1439025 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/lib/arm64/libnative-lib.so (deleted) 7265d1e000-7265d30000 rw-p 00000000 00:00 0 [anon:.bss] 7265d53000-7265d54000 ---p 00000000 00:00 0 [anon:thread stack guard page] 7265d54000-7265d55000 ---p 00000000 00:00 0 7265d55000-7265e50000 rw-p 00000000 00:00 0 [stack:20896] 7265e50000-7265e5e000 r--p 00000000 fd:00 1439058 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/oat/arm64/base.odex (deleted) 7265e5e000-7265e5f000 r-xp 0000e000 fd:00 1439058 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/oat/arm64/base.odex (deleted) 7265e5f000-7265e6d000 rw-p 00000000 00:00 0 [anon:.bss] 7265e6d000-7265e6e000 r--p 0000f000 fd:00 1439058 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/oat/arm64/base.odex (deleted) 7265e6e000-7265e6f000 rw-p 00010000 fd:00 1439058 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/oat/arm64/base.odex (deleted) 7265eb1000-72661b4000 r--s 00000000 fd:00 1439062 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/oat/arm64/base.vdex (deleted) ... ... 72ff84f000-72ff850000 r--s 00004000 fd:00 1439097 /data/app/com.taobao.myappbundledemo-I1BztqjLDnwA1LZ7mxP6kg==/split_feature0.apk
从中能够看出,安装后,split_feature0.apk
确实被加载进了进程的内存空间。而且安装路径发生了变化——data/app/com.taobao.myappbundledemo-
后面跟字母由4tyKF-aM9SLiSM28Z4YkkQ
变为I1BztqjLDnwA1LZ7mxP6kg
而且base.odex
文件显示为deleted
。
上述是feature0还未被请求的状况,如今咱们来看下,若是本地已经安装过了feature0模块,再次进行覆盖安装的话,会是怎么的状况:
7265874000-7265876000 r--p 00000000 fd:00 1439142 /data/app/com.taobao.myappbundledemo-I1BztqjLDnwA1LZ7mxP6kg==/oat/arm64/split_feature0.odex (deleted) 7265876000-7265879000 rw-p 00000000 00:00 0 [anon:.bss] 7265879000-726587a000 r--p 00002000 fd:00 1439142 /data/app/com.taobao.myappbundledemo-I1BztqjLDnwA1LZ7mxP6kg==/oat/arm64/split_feature0.odex (deleted) 726587a000-726587b000 rw-p 00003000 fd:00 1439142 /data/app/com.taobao.myappbundledemo-I1BztqjLDnwA1LZ7mxP6kg==/oat/arm64/split_feature0.odex (deleted) ... ... 7265ce1000-7265d0b000 r-xp 00000000 fd:00 1439025 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/lib/arm64/libnative-lib.so (deleted) 7265d0b000-7265d1b000 ---p 00000000 00:00 0 7265d1b000-7265d1d000 r--p 0002a000 fd:00 1439025 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/lib/arm64/libnative-lib.so (deleted) 7265d1d000-7265d1e000 rw-p 0002c000 fd:00 1439025 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/lib/arm64/libnative-lib.so (deleted) 7265d1e000-7265d30000 rw-p 00000000 00:00 0 [anon:.bss] 7265d53000-7265d54000 ---p 00000000 00:00 0 [anon:thread stack guard page] 7265d54000-7265d55000 ---p 00000000 00:00 0 7265d55000-7265e50000 rw-p 00000000 00:00 0 [stack:20896] 7265e50000-7265e5e000 r--p 00000000 fd:00 1439058 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/oat/arm64/base.odex (deleted) 7265e5e000-7265e5f000 r-xp 0000e000 fd:00 1439058 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/oat/arm64/base.odex (deleted) 7265e5f000-7265e6d000 rw-p 00000000 00:00 0 [anon:.bss] 7265e6d000-7265e6e000 r--p 0000f000 fd:00 1439058 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/oat/arm64/base.odex (deleted) 7265e6e000-7265e6f000 rw-p 00010000 fd:00 1439058 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/oat/arm64/base.odex (deleted) 7265eb1000-72661b4000 r--s 00000000 fd:00 1439062 /data/app/com.taobao.myappbundledemo-4tyKF-aM9SLiSM28Z4YkkQ==/oat/arm64/base.vdex (deleted) ... 72ff5a4000-72ff5a5000 r--s 00006000 fd:00 1439145 /data/app/com.taobao.myappbundledemo-6XP2HdNBRWLquWr3U15iJA==/split_feature0.apk
能够发现,因为安装后,安装路径发生变化,data/app/com.taobao.myappbundledemo-I1BztqjLDnwA1LZ7mxP6kg==/oat/arm64/split_feature0.odex
已经显示为deleted
,同时,未发现有新安装路径下的split_feature0.apk被加载进内存。由此能够推断:若是模块已经被安装,当他进行覆盖安装更新时,必须冷启动App才能生效。
前面说过,Android 7.0及以上版本的设备能够在请求到新模块后直接使用。而对于6.0如下版本的机型,是没法直接使用下载的新模块的。不过Google也提供了一种兼容方式,使得低版本机型能够即时使用新模块,那就是采用SplitCompat
。
能够用如下两种方式来接入SplitCompat
:
1、直接继承SplitCompatApplication
public class MyApplication extends SplitCompatApplication { ... }
2、在attachBaseContext中调用 SplitCompat.install
protected void attachBaseContext(Context base) { super.attachBaseContext(base); // Emulates installation of future on demand modules using SplitCompat. SplitCompat.install(this); }
SplitCompatApplication
最后也是走的SplitCompat.install(this)
,这跟MultiDex
十分相似,咱们来看下SplitCompat.install(this)
的实现。
public class SplitCompat { private static final AtomicReference<SplitCompat> a = new AtomicReference(null); private final Context b; private final c c; private final Set<String> d = new HashSet(); public static boolean install(Context context) { return a(context, false); } private static boolean a(Context context, boolean z) { if (VERSION.SDK_INT >= 24) { return true; } if (VERSION.SDK_INT <= 19) { return false; } SplitCompat splitCompat = (SplitCompat) a.get(); if (a.compareAndSet(null, new SplitCompat(context))) { com.google.android.play.core.splitinstall.a.a(new com.google.android.play.core.splitcompat.a.a(context, a.a(), new c(context, splitCompat.c, new com.google.android.play.core.splitcompat.c.b()), splitCompat.c)); d.a(new l(splitCompat)); } try { splitCompat.a(z); return true; } catch (Throwable e) { Log.e("SplitCompat", "Error installing additional splits", e); return false; } } private final synchronized void a(boolean z) { Iterator it; if (z) { this.c.a(); } else { a.a().execute(new m(this)); } Set<o> b = b(z); d dVar = new d(this.c); a a = b.a(); ClassLoader classLoader = this.b.getClassLoader(); if (z) { a.a(classLoader, dVar.a()); } else { it = b.iterator(); while (it.hasNext()) { Set a2 = dVar.a((o) it.next()); if (a2 == null) { it.remove(); } else { a.a(classLoader, a2); } } } Set<File> hashSet = new HashSet(); for (o oVar : b) { if (a.a(classLoader, this.c.c(oVar.a()), oVar.b(), z)) { hashSet.add(oVar.b()); } } AssetManager assets = this.b.getAssets(); for (File path : hashSet) { int intValue = ((Integer) com.google.android.play.core.splitcompat.c.b.a(assets, "addAssetPath", Integer.class, String.class, path.getPath())).intValue(); StringBuilder stringBuilder = new StringBuilder(39); stringBuilder.append("addAssetPath completed with "); stringBuilder.append(intValue); Log.d("SplitCompat", stringBuilder.toString()); } for (o oVar2 : b) { String a3 = oVar2.a(); StringBuilder stringBuilder2 = new StringBuilder(String.valueOf(a3).length() + 30); stringBuilder2.append("Split '"); stringBuilder2.append(a3); stringBuilder2.append("' installation emulated"); Log.d("SplitCompat", stringBuilder2.toString()); this.d.add(oVar2.a()); } }
addAssetPath这里能够明显看到是在新增资源了,另外须要注意的是,模块APK中的资源包id并不是传统的0x7f,而是往下递减的0x7e、0x7d、...
加载代码和so的地方是在:
public static a a() { if (VERSION.SDK_INT == 21) { return new c(); } if (VERSION.SDK_INT == 22) { return new f(); } if (VERSION.SDK_INT == 23) { return new g(); } throw new AssertionError(); } final class c implements a { c() { } static Object a(ClassLoader classLoader) { return b.b(classLoader, "pathList", Object.class).a(); } static boolean a(ClassLoader classLoader, File file, File file2, boolean z, e eVar) { Collection arrayList = new ArrayList(); Object a = a(classLoader); a c = b.c(a, "dexElements", Object.class); List<Object> asList = Arrays.asList((Object[]) c.a()); List arrayList2 = new ArrayList(); for (Object b : asList) { arrayList2.add((File) b.b(b, "zip", File.class).a()); } if (arrayList2.contains(file2)) { return true; } int i = 0; if (!z) { if (!new File((String) b.a(a.getClass(), "optimizedPathFor", String.class, File.class, file2, File.class, file)).exists()) { return false; } } c.a(eVar.a(a, new ArrayList(Collections.singleton(file2)), file, arrayList)); if (arrayList.isEmpty()) { return true; } Throwable kVar = new k("DexPathList.makeDexElement failed"); ArrayList arrayList3 = (ArrayList) arrayList; int size = arrayList3.size(); while (i < size) { Object obj = arrayList3.get(i); i++; IOException iOException = (IOException) obj; Log.e("SplitCompat", "DexPathList.makeDexElement failed", iOException); com.google.a.a.a.a.a.a.a(kVar, iOException); } b.c(a, "dexElementsSuppressedExceptions", IOException.class).a(arrayList); throw kVar; } static void b(ClassLoader classLoader, Set<File> set) { if (!set.isEmpty()) { Collection hashSet = new HashSet(); for (File file : set) { String str = "Splitcompat"; String str2 = "Adding native library parent directory: "; String valueOf = String.valueOf(file.getParentFile().getAbsolutePath()); Log.d(str, valueOf.length() != 0 ? str2.concat(valueOf) : new String(str2)); hashSet.add(file.getParentFile()); } a c = b.c(a(classLoader), "nativeLibraryDirectories", File.class); hashSet.removeAll(Arrays.asList((File[]) c.a())); int size = hashSet.size(); StringBuilder stringBuilder = new StringBuilder(30); stringBuilder.append("Adding directories "); stringBuilder.append(size); Log.d("Splitcompat", stringBuilder.toString()); c.a(hashSet); } } public final void a(ClassLoader classLoader, Set<File> set) { b(classLoader, set); } public final boolean a(ClassLoader classLoader, File file, File file2, boolean z) { return a(classLoader, file, file2, z, new d()); } }
这里只列出了SDK = 21
状况下的c类,SDK = 22
和SDK = 23
的状况其实也大同小异。熟悉Instant Run
或者热修复的同窗都能一眼看出端倪了,这不就是新增DEX和so库吗?确实,SplitCompat
的本质,就是相似冷启动的热修复的方式,插入新模块包。
经过分析运行时各个模块类中ClassLoader的状况,咱们能够大体看出模块间的依赖关系。
咱们发现,全部模块中的类其实都是用的同一个ClassLoader:
dalvik.system.PathClassLoader [DexPathList[ [zip file "/data/app/com.taobao.myappbundledemo-y4Ih9MutYd4z43hpON6LVg==/base.apk", zip file "/data/app/com.taobao.myappbundledemo-y4Ih9MutYd4z43hpON6LVg==/split_feature0.apk", zip file "/data/app/com.taobao.myappbundledemo-y4Ih9MutYd4z43hpON6LVg==/split_feature1.apk"], nativeLibraryDirectories=[ /data/app/com.taobao.myappbundledemo-y4Ih9MutYd4z43hpON6LVg==/lib/arm64, /data/app/com.taobao.myappbundledemo-y4Ih9MutYd4z43hpON6LVg==/base.apk!/lib/arm64-v8a, /data/app/com.taobao.myappbundledemo-y4Ih9MutYd4z43hpON6LVg==/split_feature0.apk!/lib/arm64-v8a, /data/app/com.taobao.myappbundledemo-y4Ih9MutYd4z43hpON6LVg==/split_feature1.apk!/lib/arm64-v8a, /system/lib64, /vendor/lib64] ]]
而这个ClassLoader里面包含了三个模块全部的APK路径与so路径,可见模块之间的类和so库的访问是彻底相通的。也就是说一个模块中声明的类,可以直接在另外一个模块中毫无差异的使用,就像使用本身模块中的类同样。而若是两个模块引入了相同的依赖,两个依赖会被分别打进两个模块的APK中,这就可能引起问题。因此,若是两个不一样的模块依赖了相同的库,须要先改成provided(gradle 3.0.0以上称为compileOnly)依赖,而在base模块中引入compile(gradle 3.0.0以上称为api)依赖。
App Bundles提供了一整套动态模块化App的机制,依托Google官方的插件支持,开发者能够直接进行模块化开发,而再也不须要本身造轮子,也能够避免Android官方插件不断升级带来的兼容性问题。Google Play商店自然承载了更新APK的使命,用户能够直接在商店上发布新模块APK,来实现静默升级,因为是直接安装,于是不存在任何兼容性问题。按需获取对应特征APK,可以极大减少本地安装的包大小。
然而它的局限也是比较多的,
固然,随着时间的推移,App Bundles这一套模式对于海外应用仍具备很强的吸引力。
还须要注意的是,App Bundles没有实现多模块之间包依赖的自动管理。也就是说各个模块的依赖是独立的,所以各个模块对于使用的相同的依赖都会各自引入,从而致使冗余。而且,若多个模块使用相同依赖包的不一样版本,可能还会有兼容性问题。所以须要在各个模块中进行provided依赖,而把公共依赖放入base模块中。
回到咱们以前抛出的问题,咱们是否能够直接利用Google为咱们提供好的构建机制,模仿Play商店构造一个SDK对App自身进行更新呢?
经过前面的技术分析,咱们有两种方案能够实现自行更新:
App Bundles的模块化,主要基于APK直接安装的方式,不须要用户任何对系统组件的修改,就能自然得到原生优化能力(如JIT、DEX优化等),于是不存在任何兼容性问题。这个思路与传统组件化/插件化方式大相径庭,由于Google做为Android官方主体,把控了Play商店这个惟一安装渠道(国内除外),因此能够采用这种玩法来作到史无前例的稳定性和动态能力。App Bundles也存在一个问题,就是没法在运行过程当中直接新增四大组件,须要覆盖安装base模块以后再重启。然后面两个方案因为都是基于App Bundles的,于是有更大局限性。
目前看来,期望Google回归恐怕遥遥无期,不过这或许能吸引国内各个厂商自带应用市场的跟进,甚至可能像统一推送那样协商出一套符合国情的标准,从而改善国内的App更新环境。不过,App Bunndles的一些思想,如细致化配置,确实值得现有的模块化方案进行借鉴和吸取。