插件化笔记 #6 启动插件 Service

2017/03/08

Demo: https://github.com/2BAB/Android-Plugin-Dev-Notes

上节学习到了插件化资源 ID 冲突的问题和解法,本节主要讨论 Service 的插件化启动。按照本系列的尿性,肯定要简单易搞,所以预注册 Service 是本节的讨论前提。具体原因在 Activity 那节写了,但是需要额外说明的是其实 Service 由于量少且新增少,是比较少做复杂插件化方案的。

<!--more-->

Service 的启动流程

推荐看这篇 startService启动过程分析

其实由于我们提前注册了 Service,没有了各种校验问题,所以只需要跟 Activity 一样注入 ClassLoader 去加载对应的模块代码就可以启动 Service。但是 Service 并没有像 Activity 那样方便的切入点(Instrumention):

[ActivityThread.java]

private void handleCreateService(CreateServiceData data) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();

LoadedApk packageInfo = getPackageInfoNoCheck(
data.info.applicationInfo, data.compatInfo);
Service service = null;
try {
java.lang.ClassLoader cl = packageInfo.getClassLoader();
service = (Service) cl.loadClass(data.info.name).newInstance();
} catch (Exception e) {
if (!mInstrumentation.onException(service, e)) {
throw new RuntimeException("Unable to instantiate service " + data.info.name + ": " + e.toString(), e);
}
}
...
}

public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo) {
return getPackageInfo(ai, compatInfo, null, false, true, false);
}


private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage) {
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}

LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null && !packageInfo.mResources.getAssets().isUpToDate())) {
if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
: "Loading resource-only package ") + aInfo.packageName
+ " (in " + (mBoundApplication != null
? mBoundApplication.processName : null)
+ ")");
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);

if (mSystemThread && "android".equals(aInfo.packageName)) {
packageInfo.installSystemApplicationInfo(aInfo,
getSystemContext().mPackageInfo.getClassLoader());
}

if (differentUser) {
// Caching not supported across users
} else if (includeCode) {
mPackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
} else {
mResourcePackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
}
}
return packageInfo;
}
}

既然没有直接有效的切入点,那么其他的插件化方案都是怎么做的?除了文初 weishu 的那个高级方法以外,还有一种比较实在并且通吃的解法。

动态插入 Element - Dex 动态性的通解

Dex 动态载入的原理其实是从 Google MultiDex 方案出来后大家才敢投入研究和使用的,具体参考Android分包原理这篇文章。

在了解了有这样的方案之后,很多人纷纷表示这可以用来做插件化和热修复,类似的博客有大头鬼的Android热更新实现原理

看完这两篇之后,其实应该了解的差不多了。本质上这种 Dex 载入的方案就是把代码载入到自定义的 ClassLoader 中,跟之前写的 Activity Hook 方案异曲同工。只不过这是一种比较彻底、方便,一次性解决了所有需要注入 ClassLoader 的地方。不仅可以用来启动 Service,也可以用来启动 Activity(当然前提是你预注册了)。

具体代码 Demo 请参考文初的链接。

Demo Usage

  1. 在插件工程 ./gradlew assembleDebug 打出插件
  2. 导入插件到手机指定目录(这个目录是自己随便指定的,跟 Demo 代码里的加载路径一致即可):mv app/build/outputs/apk/app-debug.apk app/build/outputs/apk/6-Plugin.apk && adb push app/build/outputs/apk/6-Plugin.apk /system/dex/
  3. 在宿主工程 ./gradlew installDebug 打包并安装宿主 APK
  4. 打开宿主 App,查看效果

参考资料

本系列为笔记文,文中有大量的源码解析都是引用的其他作者的成果,本文参考资料均已在文中给出链接。

欢迎关注我的公众号和微博


评论和交流请发送邮件到 [email protected]

Wechat Donate QACode
通过微信扫描赞赏码赞助此文