1 . 前言
Virtual APK 是滴滴出行自研的一款优良的插件化框架,其次要开发人员有任玉刚老师
说到任玉刚老师,他能够说是我 Android FrameWork 层的启蒙老师。刚接触 Android 的时候,在拖了几年控件、写了一些 CURD 操作后,就得出了这样的论断:客户端太无聊了,当初曾经齐全精通安卓开发了。直到有一天看了一本叫做《Android 开发艺术摸索》的书,不禁感叹:原来 Android 开发居然还能这么玩,之前的认知切实是肤浅
言归正传,Virtual APK 的个性和应用办法不是本文重点,如有须要理解更多请移步 VirtualAPK 的个性和应用办法。本文次要针对 Virtual APK 的实现做解说。
2 . 重要的知识点
- Activity 启动流程(AMS)
- DexClassLoader
- 动静代理
- 反射
- 播送的动静注册
3 . 宿主 App 的实现
中心思想:
- 对插件 APK 进行解析,获取插件 APK 的信息
- 在框架初始化时,对一系列零碎组件和接口进行替换,从而对 Activity、Service、ContentProvider 的启动和生命周期进行批改和监控,达到欺瞒零碎或者劫持零碎的目标来启动插件 Apk 的对应组件。
3.1 插件 Apk 的解析和加载
插件 Apk 的加载在 PluginManager#loadPlugin
办法,在加载实现后,会生成一个 LoadedPlugin
对象并保留在 Map 中。LoadedPlugin 里保留里插件 Apk 里绝大多数的重要信息和一个 DexClassLoader,这个 DexClassLoader 是作为插件 Apk 的类加载器应用。
看下 LoadedPlugin 的具体实现,正文表明了各个属性的含意:
public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception {
// PluginManager
this.mPluginManager = pluginManager;
// 宿主 Context
this.mHostContext = context;
// 插件 apk 门路
this.mLocation = apk.getAbsolutePath();
this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
// 插件 apk metadata
this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
// 插件 apk package 信息
this.mPackageInfo = new PackageInfo();
this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();
// 插件 apk 签名信息
if (Build.VERSION.SDK_INT >= 28
|| (Build.VERSION.SDK_INT == 27 && Build.VERSION.PREVIEW_SDK_INT != 0)) { // Android P Preview
try {this.mPackageInfo.signatures = this.mPackage.mSigningDetails.signatures;} catch (Throwable e) {PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
this.mPackageInfo.signatures = info.signatures;
}
} else {this.mPackageInfo.signatures = this.mPackage.mSignatures;}
// 插件 apk 包名
this.mPackageInfo.packageName = this.mPackage.packageName;
// 如果曾经加载过雷同的 apk, 抛出异样
if (pluginManager.getLoadedPlugin(mPackageInfo.packageName) != null) {throw new RuntimeException("plugin has already been loaded :" + mPackageInfo.packageName);
}
this.mPackageInfo.versionCode = this.mPackage.mVersionCode;
this.mPackageInfo.versionName = this.mPackage.mVersionName;
this.mPackageInfo.permissions = new PermissionInfo[0];
this.mPackageManager = createPluginPackageManager();
this.mPluginContext = createPluginContext(null);
this.mNativeLibDir = getDir(context, Constants.NATIVE_DIR);
this.mPackage.applicationInfo.nativeLibraryDir = this.mNativeLibDir.getAbsolutePath();
// 创立插件的资源管理器
this.mResources = createResources(context, getPackageName(), apk);
// 创立 一个 dexClassLoader
this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
tryToCopyNativeLib(apk);
// Cache instrumentations
Map<ComponentName, InstrumentationInfo> instrumentations = new HashMap<ComponentName, InstrumentationInfo>();
for (PackageParser.Instrumentation instrumentation : this.mPackage.instrumentation) {instrumentations.put(instrumentation.getComponentName(), instrumentation.info);
}
this.mInstrumentationInfos = Collections.unmodifiableMap(instrumentations);
this.mPackageInfo.instrumentation = instrumentations.values().toArray(new InstrumentationInfo[instrumentations.size()]);
// Cache activities
// 保留插件 apk 的 Activity 信息
Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>();
for (PackageParser.Activity activity : this.mPackage.activities) {
activity.info.metaData = activity.metaData;
activityInfos.put(activity.getComponentName(), activity.info);
}
this.mActivityInfos = Collections.unmodifiableMap(activityInfos);
this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]);
// Cache services
// 保留插件 apk 的 Service 信息
Map<ComponentName, ServiceInfo> serviceInfos = new HashMap<ComponentName, ServiceInfo>();
for (PackageParser.Service service : this.mPackage.services) {serviceInfos.put(service.getComponentName(), service.info);
}
this.mServiceInfos = Collections.unmodifiableMap(serviceInfos);
this.mPackageInfo.services = serviceInfos.values().toArray(new ServiceInfo[serviceInfos.size()]);
// Cache providers
// 保留插件 apk 的 ContentProvider 信息
Map<String, ProviderInfo> providers = new HashMap<String, ProviderInfo>();
Map<ComponentName, ProviderInfo> providerInfos = new HashMap<ComponentName, ProviderInfo>();
for (PackageParser.Provider provider : this.mPackage.providers) {providers.put(provider.info.authority, provider.info);
providerInfos.put(provider.getComponentName(), provider.info);
}
this.mProviders = Collections.unmodifiableMap(providers);
this.mProviderInfos = Collections.unmodifiableMap(providerInfos);
this.mPackageInfo.providers = providerInfos.values().toArray(new ProviderInfo[providerInfos.size()]);
// 将所有动态注册的播送全副改为动静注册
Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>();
for (PackageParser.Activity receiver : this.mPackage.receivers) {receivers.put(receiver.getComponentName(), receiver.info);
BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
for (PackageParser.ActivityIntentInfo aii : receiver.intents) {this.mHostContext.registerReceiver(br, aii);
}
}
this.mReceiverInfos = Collections.unmodifiableMap(receivers);
this.mPackageInfo.receivers = receivers.values().toArray(new ActivityInfo[receivers.size()]);
// try to invoke plugin's application
// 创立插件 apk 的 Application 对象
invokeApplication();}
3.2 Activity 的启动解决及生命周期治理
Virtual APK 启动插件 APK 中 Activity 的整体计划:
- Hook Instrumentaion 和主线程 Halder 的 callback,在重要启动过程节点对 Intent 或 Activity 进行替换
- 在宿主 APP 中事后设置一些
插桩 Activity
,这些插桩 Activity 并不会真正的启动,而是对 AMS 进行坑骗。如果启动的 Activity 是插件 APK 中的,则依据该 Actiivty 的启动模式抉择适合的插桩 Activity, AMS 在启动阶段对插桩 Activity 解决后,在创立 Activity 实例阶段,理论创立插件 APK 中要启动的 Activity。
3.2.1 插桩 Activity 的申明:
插桩 Activity 有很多个,挑一些看一下:
<!-- Stub Activities -->
<activity android:exported="false" android:name=".A$1" android:launchMode="standard"/>
<activity android:exported="false" android:name=".A$2" android:launchMode="standard"
android:theme="@android:style/Theme.Translucent" />
<!-- Stub Activities -->
<activity android:exported="false" android:name=".B$1" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$2" android:launchMode="singleTop"/>
<activity android:exported="false" android:name=".B$3"
3.2.2 hook Instrumentation
- 将零碎提供的 Instrumentation 替换为自定义的 VAInstrumentation,将主线程 Handler 的 Callback 也替换为 VAInstrumentation(VAInstrumentation 实现了 Handler.Callback 接口)
protected void hookInstrumentationAndHandler() {
try {
// 获取以后过程的 activityThread
ActivityThread activityThread = ActivityThread.currentActivityThread();
// 获取以后过程的 Instrumentation
Instrumentation baseInstrumentation = activityThread.getInstrumentation();
// 创立自定义 Instrumentation
final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
// 将以后过程原有的 Instrumentation 对象替换为自定义的
Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
// 将以后过程原有的主线程 Hander 的 callback 替换为自定义的
Handler mainHandler = Reflector.with(activityThread).method("getHandler").call();
Reflector.with(mainHandler).field("mCallback").set(instrumentation);
this.mInstrumentation = instrumentation;
Log.d(TAG, "hookInstrumentationAndHandler succeed :" + mInstrumentation);
} catch (Exception e) {Log.w(TAG, e);
}
}
3.2.3 启动 Activity 时对 AMS 进行坑骗
如果咱们相熟 Activity 启动流程的话,咱们肯定晓得 Activity 的启动和生命周期治理,都间接通过 Instrumentation 进行治理的。– 如果不相熟也没关系,能够看我之前写的 AMS 系列文章,看完保障秒懂(雾)。VAInstrumentation 重写了这个类的一些重要办法,咱们依据 Activity 启动流程一个一个说
3.2.3.1 execStartActivity
这个办法有很多个重载,挑其中一个:
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode) {
// 对原始 Intent 进行解决
injectIntent(intent);
return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode);
}
injectIntent
办法对 Intent 的解决在 ComponentsHandler#markIntentIfNeeded
办法,对原始 Intent 进行解析,获取指标 Actiivty 的包名和类名,如果指标 Activity 的包名和以后过程不同且该包名对应的 LoadedPlugin 对象存在,则阐明它是咱们加载过的插件 APK 中的 Activity,则对该 Intent 的指标进行替换:
public void markIntentIfNeeded(Intent intent) {
...
String targetPackageName = intent.getComponent().getPackageName();
String targetClassName = intent.getComponent().getClassName();
// 判断是否须要启动的是插件 Apk 的 Activity
if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
...
// 将原始 Intent 的指标 Acitivy 替换为预设的插桩 Activity 中的一个
dispatchStubActivity(intent);
}
}
dispatchStubActivity 办法依据原始 Intent 的启动模式抉择适合的插桩 Activity,将原始 Intent 中的类名批改为插桩 Activity 的类名, 示例代码:
case ActivityInfo.LAUNCH_SINGLE_TOP: {
usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
break;
}
case ActivityInfo.LAUNCH_SINGLE_TASK: {
usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;
stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
break;
}
case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {
usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;
stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
break;
}
3.2.3.2 newActivity
如果只是对原始 Intent 进行替换,那么最终启动的会是插桩 Activity,这显然达不到启动插件 Apk 中 Acitivty 的目标,在 Activity 实例创立阶段,还须要对理论创立的 Actiivty 进行替换,办法在VAInstrumentation#newActivity
:
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
try {cl.loadClass(className);
Log.i(TAG, String.format("newActivity[%s]", className));
} catch (ClassNotFoundException e) {ComponentName component = PluginUtil.getComponent(intent);
String targetClassName = component.getClassName();
Log.i(TAG, String.format("newActivity[%s : %s/%s]", className, component.getPackageName(), targetClassName));
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component);
// 应用在 LoadedPlugin 对象中创立的 DexClassLoader 进行类加载,该 ClassLoader 指向插件 APK 所在门路
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);
// 插件 Activity 实例创立后,将 Resource 替换为插件 APK 的资源
Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources());
return newActivity(activity);
}
return newActivity(mBase.newActivity(cl, className, intent));
}
如果咱们启动的是插件 APK 里的 Activity,这个办法的 Catch 语句块是肯定会被执行的,因为入参 className 曾经被替换为插桩 Activity 的,然而咱们只是在宿主 App 的 AndroidManifest.xml 中定义了这些 Actiivty,并没有真正的实现。在进入 Catch 语句块后,应用 LoadedPlugin 中保留的 DexClassloader 进行 Activity 的创立。
3.2.3.3 AMS 对插件 APK 中的 Activity 治理
看到这里,可能就会有同学有问题了,你把要启动的 Activity 给替换了,然而 AMS 中不是还记录的是插桩 Actiivty 么,那么这个 Activity 实例后续跟 AMS 的交互怎么办?那岂不是在 AMS 中的记录找不到了?释怀,不会呈现这个问题的。温习之前 AMS 系列文章咱们就会晓得,AMS 中对 Activity 治理的根据是一个叫 appToken 的 Binder 实例,在客户端对应的 token 会在 Instrumentation#newActivity 执行实现后调用 Activity#attach 办法传递给 Actiivty。
这也是为什么对 AMS 进行坑骗这种插件化计划可行的起因,因为后续治理是应用的 token,如果 Android 应用 className 之类的来治理的话,恐怕这种计划就不太好实现了。
3.2.3.4 替换 Context、applicaiton、Resources
在零碎创立插件 Activity 的 Context 创立实现之后,须要将其替换为 PluginContext,PluginContext 和 Context 的区别是其外部保留有一个 LoadedPlugin 对象,不便对 Context 中的资源进行替换。代码在VAInstrumentaiton#injectActivity
,调用处在VAInstrumentaiton#callActivityOnCreate
protected void injectActivity(Activity activity) {final Intent intent = activity.getIntent();
if (PluginUtil.isIntentFromPlugin(intent)) {Context base = activity.getBaseContext();
try {LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
Reflector.with(base).field("mResources").set(plugin.getResources());
Reflector reflector = Reflector.with(activity);
reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext()));
reflector.field("mApplication").set(plugin.getApplication());
// set screenOrientation
ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {activity.setRequestedOrientation(activityInfo.screenOrientation);
}
// for native activity
ComponentName component = PluginUtil.getComponent(intent);
Intent wrapperIntent = new Intent(intent);
wrapperIntent.setClassName(component.getPackageName(), component.getClassName());
wrapperIntent.setExtrasClassLoader(activity.getClassLoader());
activity.setIntent(wrapperIntent);
} catch (Exception e) {Log.w(TAG, e);
}
}
}
3.3 Service 的解决
Virtual APK 启动插件 APK 中 Activity 的整体计划:
- 应用动静代理代理宿主 APP 中所有对于 Service 的申请
- 判断是否为插件 APK 中的 Service,如果不是,则阐明为宿主 APP 中的,间接关上即可
- 如果是插件 APK 中的 Service,则判断是否为远端 Service,如果是远端 Service,则启动 RemoteService,并在其 StartCommand 办法中依据所代理的生命周期办法进行解决
- 如果是本地 Service,则启动 LocalService,并在其 StartCommand 办法中依据所代理的生命周期办法进行解决
3.3.1 插件化框架初始化时代理零碎的 IActivityManager
IActivityManager 是 AMS 的实现接口,它的实现类别离是 ActivityManagerService 和其 proxy
这里咱们须要代理的是 Proxy, 实现办法在PluginManager#hookSystemServices
protected void hookSystemServices() {
try {
Singleton<IActivityManager 对象 > defaultSingleton;
// 获取 IActivityManager 对象
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {defaultSingleton = Reflector.on(ActivityManager.class).field("IActivityManagerSingleton").get();} else {defaultSingleton = Reflector.on(ActivityManagerNative.class).field("gDefault").get();}
IActivityManager origin = defaultSingleton.get();
// 创立 activityManager 对象的动静代理
IActivityManager activityManager 对象的动静代理 = (IActivityManager) Proxy.newProxyInstance(mContext.getClassLoader(), new Class[] { IActivityManager.class},
createActivityManagerProxy(origin));
// 应用动静代理替换之前的 IActivityManager 对象实例
Reflector.with(defaultSingleton).field("mInstance").set(activityManagerProxy);
if (defaultSingleton.get() == activityManagerProxy) {
this.mActivityManager = activityManagerProxy;
Log.d(TAG, "hookSystemServices succeed :" + mActivityManager);
}
} catch (Exception e) {Log.w(TAG, e);
}
}
通过将动静代理对系统创立的 ActivityManager 的 proxy 进行替换,这样,调用 AMS 办法时,会转到 ActivityManagerProxy 的 invoke 办法,并依据办法名对 Service 的生命周期进行治理,生命周期办法较多,筛选其中一个:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if ("startService".equals(method.getName())) {
try {return startService(proxy, method, args);
} catch (Throwable e) {Log.e(TAG, "Start service error", e);
}
}
startService:
protected Object startService(Object proxy, Method method, Object[] args) throws Throwable {IApplicationThread appThread = (IApplicationThread) args[0];
Intent target = (Intent) args[1];
ResolveInfo resolveInfo = this.mPluginManager.resolveService(target, 0);
if (null == resolveInfo || null == resolveInfo.serviceInfo) {
// 插件中没找到,阐明是宿主 APP 本人的 Service
return method.invoke(this.mActivityManager, args);
}
// 启动插件 APK 中的 Service
return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
}
startDelegateServiceForTarget
中会调用 wrapperTargetIntent
解决,最终在 RemoteService 或者 LocalService 的 onStartCommand 中对 Service 的各生命周期解决。
须要留神的是,在 RemoteService 中须要从新对 APK 进行解析和装载,生成 LoadedPlugin,因为它运行在另一个过程中。
这也阐明插件 APK 的 Service 过程如果申明了多个是有效的,因为他们最终都会运行在宿主 RemoteService 所在过程。
3.4 ContentProvider 的解决
ContentProvicer 的解决和 Service 是相似的,不多说了。
4 . 插件 App 的实现
插件 APP 实践上并不需要做什么非凡解决,惟一须要留神的是资源文件的抵触问题,因而,须要在插件工程 app 目录下的 build.gradle 中增加如下代码:
virtualApk {
packageId = 0x6f // the package id of Resources.
targetHost = '../../VirtualAPK/app' // the path of application module in host project.
applyHostMapping = true //optional, default value: true.
}
它的作用是在插件 APK 编译时对资源 ID 进行重写,解决办法在 ResourceCollector.groovy 文件的 collect
办法:
def collect() {
//1、First, collect all resources by parsing the R symbol file.
parseResEntries(allRSymbolFile, allResources, allStyleables)
//2、Then, collect host resources by parsing the host apk R symbol file, should be stripped.
parseResEntries(hostRSymbolFile, hostResources, hostStyleables)
//3、Compute the resources that should be retained in the plugin apk.
filterPluginResources()
//4、Reassign the resource ID. If the resource entry exists in host apk, the reassign ID
// should be same with value in host apk; If the resource entry is owned by plugin project,
// then we should recalculate the ID value.
reassignPluginResourceId()
//5、Collect all the resources in the retained AARs, to regenerate the R java file that uses the new resource ID
vaContext.retainedAarLibs.each {gatherReservedAarResources(it)
}
}
首先获取插件 app 和宿主 app 的资源汇合,而后寻找其中抵触的资源 id 进行批改,批改 id 是 reassignPluginResourceId 办法:
private void reassignPluginResourceId() {
// 对资源 ID 依据 typeId 进行排序
resourceIdList.sort { t1, t2 ->
t1.typeId - t2.typeId
}
int lastType = 1
// 重写资源 ID
resourceIdList.each {if (it.typeId < 0) {return}
def typeId = 0
def entryId = 0
typeId = lastType++
pluginResources.get(it.resType).each {it.setNewResourceId(virtualApk.packageId, typeId, entryId++)
}
}
}
这里要说一下资源 ID 的组成:
资源 ID 是一个 32 位的 16 进制整数,前 8 位代表 app, 接下来 8 位代表 typeId(string、layout、id 等),从 01 开始累加,前面四位为资源 id, 从 0000 开始累加。轻易反编译了一个 apk, 看一下其中一部分的构造:
对资源 ID 的遍历应用了双重循环,外层循环从 01 开始对 typeId 进行遍历,内层循环从 0000 开始对 typeId 对应的资源 ID 进行遍历,并且在内层循环调用 setNewResourceId
进行重写:
public void setNewResourceId(packageId, typeId, entryId) {newResourceId = packageId << 24 | typeId << 16 | entryId}
packageId 是咱们在 build.gradle 中定义的 virtualApk.packageId,将其左移 24 位,与资源 id 的前 8 位对应,typeId 与第 9 -16 位对应,前面是资源 id
这样,在插件 app 编译过程中就实现了抵触资源 id 的替换,前面也不会有抵触的问题了
5 . 总结
回顾整个 Virtual APK 的实现,其实逻辑并不是特地简单,然而能够看到作者们对 AMS 以及资源加载、类加载器等 API 的相熟水平,如果不是对这些常识体系特地精通的话,是很难实现的,甚至连思路都不可能有,这也是咱们学习源码的意义所在。
本文转自 https://www.androidos.net.cn/doc/2021/10/13/1024.html,如有侵权,请分割删除。
相干视频举荐
【B 站超具体解说】Android 开源库最新完整版课程!深刻底层原理,案例 + 我的项目实战解说通俗易懂
【合集】全网超具体的 Android 开源框架应用学习(组件化 / 集成化 /RxJava/IOC/AOP/MVP/ 网络架构 /Google 标准化架构 / 热修复设计)
Android 外围开源框架我的项目实战解析选集 / 热修复 / 类加载 / 插件化 / 组件化 /AMS/ 路由框架 ……
Android 开发零基础教程网络框架篇 /OKHTTP/Retrofit