总结
**其实上面说了这么多,钱是永远赚不完的,在这个知识付费的时代,知识技能提升才是是根本!我作为一名8年的高级工程师,知识技能已经学习的差不多。**在看这篇文章的可能有刚刚入门,刚刚开始工作,或者大佬级人物。
像刚刚开始学Android开发小白想要快速提升自己,最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以这里分享一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。
这么重要的事情说三遍啦!点赞+点赞+点赞!
【Android高级架构师系统学习资料】高级架构师进阶必备——设计思想解读开源框架
第一章、热修复设计
第二章、插件化框架设计
第三章、组件化框架设计
第四章、图片加载框架
第五章、网络访问框架设计
第六章、RXJava 响应式编程框架设计
第七章、IOC 架构设计
第八章、Android 架构组件 Jetpack
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
- 可与宿主工程通信
- 兼容性强
- 使用简单
- 编译插件方便
- 经过大规模使用
如果你要加载一个插件,并且这个插件无需和宿主有任何耦合,也无需和宿主进行通信,并且你也不想对这个插件重新打包,那么推荐选择DroidPlugin。
插件化原理
VirtualAPK 对插件没有额外的约束,原生的apk即可作为插件。插件工程编译生成 Apk 后,即可通过宿主 App 加载,每个插件apk被加载后,都会在宿主中创建一个单独的 LoadedPlugin 对象。如下图所示,通过这些 LoadedPlugin 对象,VirtualAPK 就可以管理插件并赋予插件新的意义,使其可以像手机中安装过的 App 一样运行。
我们在引入一款框架的时候往往不能只单纯的了解如何使用,应去深入的了解它是如何工作的,特别是插件化这种热门的技术,十分感谢开源项目给了我们一把探寻 Android 世界的金钥匙,下面将和大家简易的分析下 VirtualAPK 的原理。
四大组件对于安卓人员都是再熟悉不过了,我们都清楚四大组建都是需要在 AndroidManifest 中注册的,而对于 VirtualAPK 来说是不可能预先知晓名字,提前注册在宿主 Apk 中的,所以现在基本都采用 hack 方案解决,VirtualAPK 大致方案如下:
- Activity:在宿主 Apk 中提前占坑,然后通过 Hook Activity 的启动过程,“欺上瞒下”启动插件 Apk 中的 Activity,因为 Activity 存在不同的 LaunchMode 以及一些特殊的熟悉,所以需要多个占坑的“李鬼” Activity。
- Service:通过代理 Service 的方式去分发;主进程和其他进程,VirtualAPK 使用了两个代理Service。
- BroadcastReceiver:静态转动态。
- ContentProvider:通过一个代理Provider进行分发。
在本文,我们主要分析 Activity 的占坑过程,如果需要更深入的了解 VirtualAPK 请点我
Activity 流程
我们如果要启用 VirtualAPK 的话,需要先调用pluginManager.loadPlugin(apk)
,进行加载插件,然后我们继续向下调用
// 调用 LoadedPlugin 加载插件 Activity 信息
LoadedPlugin plugin = LoadedPlugin.create(this, this.mContext, apk);
// 加载插件的 Application
plugin.invokeApplication();
我们可以发现插件 Activity 的解析是交由LoadedPlugin.create 去完成的,完成之后保存至 mPlugins
这个 Map 当中方便下次调用与解绑插件,我们继续往下探索
// 拷贝Resources
this.mResources = createResources(context, apk);
// 使用DexClassLoader加载插件并与现在的Dex进行合并
this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
// 如果已经初始化不解析
if (pluginManager.getLoadedPlugin(mPackageInfo.packageName) != null) {
throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName);
}
// 解析APK
this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
// 拷贝插件中的So
tryToCopyNativeLib(apk);
// 保存插件中的 Activity 参数
Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>();
for (PackageParser.Activity activity : this.mPackage.activities) {
activityInfos.put(activity.getComponentName(), activity.info);
}
this.mActivityInfos = Collections.unmodifiableMap(activityInfos);
this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]);
LoadedPlugin 中将我们插件中的资源合并进了宿主 App 中,至此插件 App 的加载过程就已经完成了,这里大家肯定会有疑惑,该Activity必然没有在Manifest中注册,这么启动不会报错吗?
这就要涉及到 Activity 的启动流程了,我们在startActivity
之后系统最终会调用 Instrumentation 的 execStartActivity 方法,然后再通过 ActivityManagerProxy 与 AMS 进行交互。
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;
}
最后
我见过很多技术leader在面试的时候,遇到处于迷茫期的大龄程序员,比面试官年龄都大。这些人有一些共同特征:可能工作了7、8年,还是每天重复给业务部门写代码,工作内容的重复性比较高,没有什么技术含量的工作。问到这些人的职业规划时,他们也没有太多想法。
其实30岁到40岁是一个人职业发展的黄金阶段,一定要在业务范围内的扩张,技术广度和深度提升上有自己的计划,才有助于在职业发展上有持续的发展路径,而不至于停滞不前。
不断奔跑,你就知道学习的意义所在!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!