Activity 是否注册在 Manifest 的校验是由 AMS 进行的,所以我们在于 AMS 交互前,提前将 ActivityManagerProxy 提交给 AMS 的 ComponentName
替换为我们占坑的名字即可。 通常我们可以选择 Hook Instrumentation 或者 Hook ActivityManagerProxy 都可以达到目标,VirtualAPK 选择了 Hook Instrumentation 。
private void hookInstrumentationAndHandler() {
try {
Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
if (baseInstrumentation.getClass().getName().contains(“lbe”)) {
// reject executing in paralell space, for example, lbe.
System.exit(0);
}
// 用于处理替换 Activity 的名称
final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);
Object activityThread = ReflectUtil.getActivityThread(this.mContext);
// Hook Instrumentation 替换 Activity 名称
ReflectUtil.setInstrumentation(activityThread, instrumentation);
// Hook handleLaunchActivity
ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
this.mInstrumentation = instrumentation;
} catch (Exception e) {
e.printStackTrace();
}
}
上面我们已经成功的 Hook 了 Instrumentation ,接下来就是需要我们的李鬼上场了
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
// 只有是插件中的Activity 才进行替换
if (intent.getComponent() != null) {
Log.i(TAG, String.format(“execStartActivity[%s : %s]”, intent.getComponent().getPackageName(),
intent.getComponent().getClassName()));
// 使用"李鬼"进行替换
this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
}
ActivityResult result = realExecStartActivity(who, contextThread, token, target,
intent, requestCode, options);
return result;
}
我们来看一看 markIntentIfNeeded(intent);
到底做了什么
public void markIntentIfNeeded(Intent intent) {
if (intent.getComponent() == null) {
return;
}
String targetPackageName = intent.getComponent().getPackageName();
String targetClassName = intent.getComponent().getClassName();
// 保存我们原有数据
if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) {
intent.putExtra(Constants.KEY_IS_PLUGIN, true);
intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
dispatchStubActivity(intent);
}
}
private void dispatchStubActivity(Intent intent) {
ComponentName component = intent.getComponent();
String targetClassName = intent.getComponent().getClassName();
LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);
ActivityInfo info = loadedPlugin.getActivityInfo(component);
// 判断是否是插件中的Activity
if (info == null) {
throw new RuntimeException("can not find " + component);
}
int launchMode = info.launchMode;
// 并入主题
Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
themeObj.applyStyle(info.theme, true);
// 将插件中的 Activity 替换为占坑的 Activity
String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
Log.i(TAG, String.format(“dispatchStubActivity,[%s -> %s]”, targetClassName, stubActivity));
intent.setClassName(mContext, stubActivity);
}
可以看到上面将我们原本的信息保存至 Intent 中,然后调用了 getStubActivity(targetClassName, launchMode, themeObj);
进行了替换
public static final String STUB_ACTIVITY_STANDARD = “%s.A
p
u
b
l
i
c
s
t
a
t
i
c
f
i
n
a
l
S
t
r
i
n
g
S
T
U
B
A
C
T
I
V
I
T
Y
S
I
N
G
L
E
T
O
P
=
"
%d"; public static final String STUB_ACTIVITY_SINGLETOP = "%s.B
publicstaticfinalStringSTUBACTIVITYSINGLETOP="%d”;
public static final String STUB_ACTIVITY_SINGLETASK = “%s.C
p
u
b
l
i
c
s
t
a
t
i
c
f
i
n
a
l
S
t
r
i
n
g
S
T
U
B
A
C
T
I
V
I
T
Y
S
I
N
G
L
E
I
N
S
T
A
N
C
E
=
"
%d"; public static final String STUB_ACTIVITY_SINGLEINSTANCE = "%s.D
publicstaticfinalStringSTUBACTIVITYSINGLEINSTANCE="%d”;
public String getStubActivity(String className, int launchMode, Theme theme) {
String stubActivity= mCachedStubActivity.get(className);
if (stubActivity != null) {
return stubActivity;
}
TypedArray array = theme.obtainStyledAttributes(new int[]{
android.R.attr.windowIsTranslucent,
android.R.attr.windowBackground
});
boolean windowIsTranslucent = array.getBoolean(0, false);
array.recycle();
if (Constants.DEBUG) {
Log.d(“StubActivityInfo”, "getStubActivity, is transparent theme ? " + windowIsTranslucent);
}
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
switch (launchMode) {
case ActivityInfo.LAUNCH_MULTIPLE: {
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
if (windowIsTranslucent) {
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
}
break;
}
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;
}
default:break;
}
mCachedStubActivity.put(className, stubActivity);
return stubActivity;
}
StubActivityInfo 根据同的 launchMode
启动相应的“李鬼” Activity 至此,我们已经成功的 欺骗了 AMS ,启动了我们占坑的 Activity 但是只成功了一半,为什么这么说呢?因为欺骗过了 AMS,AMS 执行完成后,最终要启动的并非是占坑的 Activity ,所以我们还要能正确的启动目标Activity。
我们在 Hook Instrumentation 的同时一并 Hook 了 handleLaunchActivity,所以我们之间到 Instrumentation 的 newActivity 方法查看启动 Activity 的流程。
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
try {
// 是否能直接加载,如果能就是宿主中的 Activity
cl.loadClass(className);
} catch (ClassNotFoundException e) {
// 取得正确的 Activity
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
String targetClassName = PluginUtil.getTargetActivity(intent);
Log.i(TAG, String.format(“newActivity[%s : %s]”, className, targetClassName));
// 判断是否是 VirtualApk 启动的插件 Activity
if (targetClassName != null) {
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
// 启动插件 Activity
activity.setIntent(intent);
try {
// for 4.1+
ReflectUtil.setField(ContextThemeWrapper.class, activity, “mResources”, plugin.getResources());
} catch (Exception ignored) {
// ignored.
}
return activity;
}
}
// 宿主的 Activity 直接启动
return mBase.newActivity(cl, className, intent);
}
好了,到此Activity就可以正常启动了。
小结
VritualApk 整理思路很清晰,在这里我们只介绍了 Activity 的启动方式,感兴趣的同学可以去网上了解下其余三大组建的代理方式。不论如何如果想使用插件化框架,一定要了解其中的实现原理,文档上描述的并不是所有的细节,很多一些属性什么的,以及由于其实现的方式造成一些特性的不支持。
引入插件化之痛
由于项目的宿主与插件需要进行较为紧密的交互,在插件化的同时需要对项目进行模块化,但是模块化并不能一蹴而就,在模块化的过程中经常出现,牵一发而动全身的问题,在经历过无数个通宵的夜晚后,我总结出了模块化的几项准则。
VirtualAPK 本身的使用并不困难,困难的是需要逐步整理项目的模块,在这期间问题百出,因为自身没有相关经验在网上看了很多关于模块化的文章,最终我找到有赞模块化的文章,对他们总结出来的经验深刻认同。
在项目模块化时应该遵循以下几个准则
- 确定业务逻辑边界
- 模块的更改上保持克制
- 公共资源及时抽取
确定业务逻辑边界 在模块化之前,我们先要详细的分析业务逻辑,App 作为业务链的末端,由于角色所限,开发人员对业务的理解比后端要浅,所谓欲速则不达,重构不能急,理清楚业务逻辑之后再动手。
在模块化进行时,我们需要将业务模块进行隔离,业务模块之间不能互相依赖能存在数据传输,只能单向依赖宿主项目,为了达到这个效果 我们需要借用市面上的路由方案 ARouter ,由于篇幅原因,我在这里不做过多介绍,感兴趣的同学可以自行搜索。
项目改造后宿主只留下最简单的公共基础逻辑,其他部分都由插件的形式装载,这样使得我们在版本更新的过程中自由度很高,从项目结构上我们看起来很像所有插件都依赖了宿主 App 的代码,但实际上在打包的过程中 VirtualAPK 会帮助我们剔除重复资源。
模块的更改上保持克制 在模块化进行时,不要过分的追求完美的目标,简单粗暴一点,后续再逐渐改善,很多业务逻辑经常会和其他业务逻辑产生牵连,它们俩会处于一个相对暧昧的关系,这种时候我们不要去强行的分割它们的业务边界,过分的分割往往会因为编码人员对于模块的不清晰导致项目改造的全盘崩溃。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
![](https://i-blog.csdnimg.cn/blog_migrate/1eb158fce2ab69768c2eef6602fec294.jpeg)
最后
想要了解更多关于大厂面试的同学可以点赞支持一下,除此之外,我也分享一些优质资源,包括:Android学习PDF+架构视频+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这几块的内容。非常适合近期有面试和想在技术道路上继续精进的朋友。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
要了解更多关于大厂面试的同学可以点赞支持一下,除此之外,我也分享一些优质资源,包括:Android学习PDF+架构视频+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这几块的内容。非常适合近期有面试和想在技术道路上继续精进的朋友。