2. 架构师晋级
一. 换肤核心技术
1. 首先分析View的创建流程
即分析AppCompatActivity的setContentView(R.layout.activity_main);
1 | public class MainActivity extends AppCompatActivity { |
1 | public class AppCompatActivity extends FragmentActivity implements AppCompatCallback, |
1 | public abstract class AppCompatDelegate { |
1 | public abstract class LayoutInflater { |
综上分析,Activity在setContentView(R.layout.activity_main)的时候,通过mFactory2.onCreateView来创建view,我们在Activity中重写onCreateView拦截系统的方法,来创建自己可以实现换肤的控件,没有拦截的控件还是走系统的方法去创建。
既然找到了需要拦截的方法,那我们再看在哪里可以拦截这个方法。开始分析源码:
2. 源码分析,寻找突破点,即找一个可以换肤的节点。
1 | public class TestActivity extends AppCompatActivity { |
1 | public class AppCompatActivity extends FragmentActivity implements AppCompatCallback, |
1 | public abstract class AppCompatDelegate { |
1 | class AppCompatDelegateImpl extends AppCompatDelegate implements Callback, Factory2 { |
1 | public class AppCompatViewInflater { |
1 | public class Activity extends ContextThemeWrapper |
通过以上分析我们得出结论:
- Activity在onCreate方法里调用了super.onCreate(savedInstanceState) -> delegate.installViewFactory() -> LayoutInflaterCompat.setFactory2(layoutInflater, this) -> this是Factory2的实现类,实现类实现了onCreateView方法 ->mAppCompatViewInflater.createView()
- mAppCompatViewInflater.createView() 在这里面通过name(即控件的标签名)初始化了对应的view。
- 我们可以在初始化view的时候做工作,即把控件换成自定义控件,自定义控件加上可以动态换肤的工作。
- 所以如果想实现换肤,可以实现mAppCompatViewInflater.createView()方法里的工作,又因为Factory2.onCreateView方法调用了这个方法,那么我们可以通过重写onCreateView()来实现目的,又因为Activity实现了Factory2,所以我们可以直接在自己的Activity中重写onCreateView()方法。
- 通过查看AppCompatActivity.
3. 换肤实现
1 | public class MainActivity extends SkinActivity { |
1 | public class SkinActivity extends AppCompatActivity { |
1 | /** |
1 | /** |
二. 组件化框架设计
1. APT(Annotation Processing Tool)
是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码,如果想要自定义的注解处理器能够正常运行,必须通过APT工具来进行处理。
也可以这样理解,只有通过声明APT工具后,程序在编译期间自定义注解解释器才能执行。
通俗理解:根据规则,帮我们生成代码、生成类文件。
2. 工具:
- square/javapoet:JavaPoet 是一个生成.java源文件的Java API.
- alibaba/ARouter: 帮助 Android App 进行组件化改造的路由框架 .
3. 组件化里路由的基本思想
(创建注解)新建Java Library类型的arouter_annotation模块。在里面新建路由注解(ARouter)。
(处理注解)新建Java Library类型的compiler模块。在里面新建继承AbstractProcessor的注解处理类,根据注解获取类的信息,以及注解上的参数等信息,来编写生成源代码的代码。最后在编译时生成源代码文件。
使用的类库
1
2
3
4
5
6
7
8// As-3.4.1 + gradle5.1.1-all + auto-service:1.0-rc4
compileOnly'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
// 帮助我们通过类调用的形式来生成Java代码
implementation "com.squareup:javapoet:1.9.0"
// 引入annotation,处理@ARouter注解
implementation project(':arouter_annotation')
(使用注解)@ARouter(path = “/app/MainActivity”)
三. 插件化框架设计
1. 原理:可安装运行的宿主APP ->可以通过插件化,运行调用下载到本地的没有安装的apk文件。
2. 占位(插桩)式插件化框架
基本思想:
- 通过AssetManager获取sd卡中下载好的插件apk,可以获取Resources和DexClassLoader。
- 新建公共库,定义标准(接口ActivityInterface),比如Activity、service的生命周期方法,以及获取宿主的Context等接口。
- 作为插件的apk,里面的Activity实现标准接口ActivityInterface。
- 宿主中新建一个占位(代理)的Activity(ProxyActivity),并且通过重写相关方法,替换为插件的Resources和DexClassLoader。
- 当启动插件中的Activity时,先获取插件Activity的ActivityInfo,然后就可以通过反射获取到Activity对象,即ActivityInterface的实现类对象,通过这个对象就可以执行插件里面Activity的方法了。
- 因为插件中使用的是宿主中的Activity,只需要在宿主中的清单文件中注册代理Activity就行,插件中其实不是真正的Activity,无需注册。
- 总结为,在宿主中创建一个空的代理Activity,当调用插件中的Activity时,在代理Activity中的方法中调用插件中相同的方法。
Activity、service使用插件的流程差不多,但是BroadcastReceivery有发送和接收两步,所以多了一步接收的操作。
静态注册的广播,是什么时候注册的?
- 手机开机的时候,所有的app,再次进行安装一遍,系统会去解析AndroidManifest,解析静态广播,进行注册。
有关插件的问题
在插件中,为什么不能使用 this
答:因为插件是没有安装手机上安装的,是无法拥有组件环境的。为什么要有代理的Activity
答:由于插件中的Activity,并不是一个能够运行的组件,所以需要代理的Activity去代替 插件中的Activity (例如:Activity任务进栈)。这种插件化,在写插件开发的时候有什么(要注意的事项)
答:所有关于操作,组件环境的地方,都必须使用 宿主的环境。
Apk解析原理系统源码分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
581.静态注册的广播,是什么时候注册的?
手机开机的时候, 所有的app,再次进行安装一遍, 系统会去解析AndroidManifest,解析静态广播后,就会自动注册
2.我们去分析 安装
data/app 放置目录
data/data/包名/ 应用所属目录
data/dalvik-cache 虚拟机去加载执行指令
3.该分析那个目录
data/app 放置目录
手机开机安装app的时候,安装 过后 马上就会 全盘扫描,data/app 放置目录
解析出 app apk 文件 里面所有组件,包括权限,系统会去解析AndroidManifest
Android 会在 安装过后,会马上扫描此目录data/app 放置目录 ---> 解析 apk 文件 里面的配置信息AndroidManifest.xml ,如果里面有静态配置的广播
就会要去注册广播
分析系统源码,是如何进行解析apk
PackageManagerService
【目标】:看系统是如何 去解析 apk 文件里面的 组件信息的
系统是在安装的时候,才会去扫描,apk
PackageManagerService
Linux内核驱动 --- init进程 -- zygote进程 孵化 SystemServer --- PackageManagerService启动
PackageManagerService启动
pms 如何去 处理 data/app/目录 ,如何解析apk
dataDir /data/目录
mAppInstallDir = new File(dataDir, "app"); /data/app/目录
mAppInstallDir:如何去解析apk文件的
scanDirTracedLI 是要去扫描 /data/app/目录下的apk文件 ---> 解析AndroidManifest 里面的所有信息
扫描 apk 文件,解析apk scanPackageLI --->
parsePackage:解析 apk 文件里面的所有信息
Package ---> apk 里面的 AndroidManifest配置信息 (所有的)
拿到了Package,就能拿到 静态的广播信息
最终的目标:
<!-- 静态注册的广播 -->
<receiver android:name=".StaticReceiver">
<intent-filter>
<action android:name="plugin.static_receiver" />
</intent-filter>
</receiver>总结为:PackageManagerService可以加载手机里的任何apk,从它的构造方法可以看出,安装的apk文件都在data/app目录里,当扫描这个目录里的某个apk文件时,,调用了PackageParser.parsePackage()方法,并且返回Package类,这个类有个字段属性receivers(ArrayList
),就是AndroidMannifest里的receiver,receivers里面的泛型Activity不是四大组件的Activity,通过这个Activity可以获取跟广播相关的一些信息。所以,我们可以通过反射,根据上面的源码,一步步获取AndroidManifest里有关广播的一些信息,然后在代码中注册广播即可。
3. Hook式插件化框架
Hook 基础 – 从入门到熟练
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
341.替换(把系统里面的 替换成 动态代理)
2.添加动态代理(做我们自己的业务逻辑)
---- Hook 系统源码 ----> TestActivity 不再AndroidManifest里面注册,也能启动
会报错:TestActivity}; have you declared this activity in your AndroidManifest.xml? 没有在AndroidManifest里面注册
原因:
startActivity ---> TestActivity ----》 (Hook) (AMS)ActivityManagerService(检测,当前要启动的Activity是否注册了)
Hook (Hook):
1.把TestActivity 替换我们真实有效的Activity
startActivity(TestActivity) ---> Activity --> Instrumentation.execStartActivity ---> ActivityManagerNative.getDefault()
IActivityManager.startActivity ---> (Hook) AMS.startActivity(检测,当前要启动的Activity是否注册了)
思想切入点:既然会得到IActivityManager,会设置IActivityManager,(寻找替换点(动态代理))
动态代理:由于执行startActivity之前,我们需要先执行我们的代码(把TestActivity 替换成 已经注册的 Activity)
2.ASM检查过后,要把这个ProxyActivity 换回来 --> TestActivity
startActivity ---> TestActivity -- (Hook ProxyActivity)(AMS)检测,当前要启动的Activity是否注册了)ok ---》
ActivityThread(即将加载启动Activity)----(要把这个ProxyActivity 换回来 --> TestActivity)
Hook LAUNCH_ACTIVITY
我们要在Handler。handleMessage 之前执行,就是为了(要把这个ProxyActivity 换回来 --> TestActivity),所有需要Hook
1.hook ams检查 把TestActivity 换成 ProxyActivity
2.hook 即将要加载Activity又把ProxyActivity 换回来了 TestActivity类加载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59学习类加载之前,我们去startActivity跳转 到 插件中的Activity看会发生什么错误(分析错误的过程中学习类加载)
宿主 跳转 宿主的Actvity ok
宿主 跳转 插件里面的Activity 报错
分析错误原因,来学习Android类加载:
Caused by: java.lang.ClassNotFoundException: Didn't find class "com.netease.plugin_package.PluginActivity" on path:
DexPathList[[zip file "/data/app/com.netease.hookproject-1/base.apk", zip file "/data/app/com.netease.hookproject-1/split_lib_
dependencies_apk.apk", zip file "/data/app/com.netease.hookproject-1/split_lib_slice_0_apk.apk", zip file "/data/app/com
.netease.hookproject-1/split_lib_slice_1_apk.apk", zip file "/data/app/com.netease.hookproject-1/split_lib_slice_2_apk.apk"
, zip file "/data/app/com.netease.hookproject-1/split_lib_slice_3_apk.apk", zip file "/data/app/com.netease.hookproject-1/s
plit_lib_slice_4_apk.apk", zip file "/data/app/com.netease.hookproject-1/split_lib_slice_5_apk.apk", zip file "/data/app/com
.netease.hookproject-1/split_lib_slice_6_apk.apk", zip file "/data/app/com.netease.hookproject-1/split_lib_slice_7_apk.apk",
zip file "/data/app/com.netease.hookproject-1/split_lib_slice_8_apk.apk", zip file "/data/app/com.netease.hookproject-1/spli
t_lib_slice_9_apk.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]
startActivity --> AMS ---> ActivityThread(把代理的Activity给换回来了) ---> 要去实例化Activity (报错)
Activity --> Instrumentation ---> AMS检查 --->
ActivityThread (即将加载)-(handleLaunchActivity 类加载Activity performLaunchActivity ---> newActivity(cl == PathClassLoader))
分析Android中的ClassLoader:
1.java中的ClassLoader 和 Android的ClassLoader 是不一样
2.ClassLoader == PathClassLoader
3.PathClassLoader == cl.loadClass(className).newInstance();
PathClassLoader.loadClass ---》 BaseDexClassLoader --》ClassLoader.loadClass--findClass(空方法) 让覆盖的子类方法去完成 --》
BaseDexClassLoader.findClass() ---》pathList.findClass
BaseDexClassLoader.findClass() -- c 为什么为null,--》 DexPathList.findClass(className) ---》DexFile.loadClassBinaryName(系列步骤后 NDK)
for遍历 dexElements == Element[] ,分析 Element 是什么 ,为什么Element.dexFile==null?
Android虚拟机 dex文件的 dex == 对Dex表现形式的描述 Element --- dexFile拥有可执行
为什么 Element ==null?
答:就是因为类加载机制加载的是 ---》 宿主的 classes.dex--Elements, 【没有插件的Element】
解决方案:把插件的dexElements 和 宿主中的 dexElements 融为一体 PathClassLoader 就能加载到 插件/宿主 都可以加载到了
Hook式 插件化
------ Android ClassLoader介绍
1.java中的ClassLoader 和 Android的ClassLoader 是不一样
2.Android中的ClassLoader 分为两类:系统提供的ClassLoader ---》BootClassLoader,PathClassLoader,DexClassLoader
自定义ClassLoader
给系统预加载使用的 :BootClassLoader
给程序/系统程序/应用程序 加载class的 PathClassLoader
加载 apk zip apk文件 DexClassLoader
1.内核启动 ...
2.init第一个进程
3.zygote进程
// 启动是很早就要启动
---> zygoteInit --> BootClassLoader.getInstance(); handleSystemServerProcess PathClassLoaderFactory --》PathClassLoader
4.zygote进程孵化 SystemServer
5.SystemServer启动很多的服务 ---(AMS,PSM,...)
// 不能在这里启动宿主和插件融为一体,解决插件没有宿主环境等问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 第一步:找到宿主 dexElements 得到此对象 PathClassLoader代表是宿主
// 第二步:找到插件 dexElements 得到此对象,代表插件 DexClassLoader--代表插件
// 第三步:创建出 新的 newDexElements [],类型必须是Element,必须是数组对象
// 第四步:宿主dexElements + 插件dexElements =----> 融合 新的 newDexElements for遍历
// 第五步:把新的 newDexElements,设置到宿主中去
以上操作,就可以去加载插件里面的class
需要去加载插件里面的 layout
StringBlock ---》 string.xml color.xml anim.xml ...
mStringBlocks[] == string.xml color.xml anim.xml
只有mStringBlocks[] 初始化了,才能去加载 插件里面的资源LoadedApk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78占位式 插件化 ---》(stander标准) 在插件中必须使用 宿主的环境 appActivity 宿主context ---》 插件
Hook式 (宿主 和 插件 element 进行融合) 在插件中可以随意使用this,既然式融合一起,插件可以使用到宿主的环境
插件越多 内存中的 newDexElements 就会越大
LoadedApk式 插件化框架的手写,我们控制 ClassLoader
PathClassLoader ---> 宿主的class
自定义ClassLoader --》 插件的class
解决这个问题:插件越多 内存中的 newDexElements 就会越大
ActivityThread源码的分析:
startActivity ---》 Activity --》Instrumentation ---> AMS检查 --》
ActivityThread -- mH LAUNCH_ACTIVITY(自己处理LoaderApk中的ClassLoader)
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
// 跳转的Activity纪录 --》
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
// 如果缓存mPackages中有LoadedApk 就直接返回,如果没有LoaaedApk就创建出LoadedApk ---》 宿主的LoadedApk.ClassLoader
// 如果是加载插件,从mPackages取出 插件专用的LoadedApk.自定义ClassLoader
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
// 真正的即将 实例化Activity 然后进行启动
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
1.public final LoadedApk getPackageInfoNoCheck == 宿主的
2.缓存中的 final ArrayMap<String, WeakReference<LoadedApk>> mPackages 默认保存的是宿主的LoadedApk
LoadedApk --- 宿主的 ----》 LoadedApk.ClassLoader ---> 宿主中的class
java.lang.ClassLoader cl = r.packageInfo.getClassLoader(); // LoadedApk里面的ClassLoader
(Activity)cl.loadClass(className).newInstance(); 实例化的Activity --》 宿主的 LoadedApk里面的ClassLoader 去实例化的
以上代码结论:宿主的LoadedApk.ClassLoader 去加载 宿主中的class,然后实例化的
--- > 自定义一个LoadedApk 自定义一个ClassLoader 用于专门加载插件里面的class,然后实例化
自定义一个 LoadedApk 然后保存 --》 mPackages
LoadedApk --- 插件的 ----》 LoadedApk.ClassLoader ---> 插件中的class
3.梳理流程:
宿主: startActivity ---》 Activity --》Instrumentation ---> AMS检查 --》ActivityThread
mPackages.value取出 LoadedApk.ClassLoader ---> 实例化Activity (只能加载宿主的)
插件(下一节课,要完成的功能):
我们在取出之前,需要自定义一个 (插件专用的 LoadedApk 自定义ClassLoader) 添加到 --》 mPackages
startActivity ---》 Activity --》Instrumentation ---> AMS检查 --》ActivityThread
mPackages.value取出 插件专用的LoadedApk.ClassLoader --> 实例化插件的Activity
下节课的目标:绕过 PMS 的检查处理 -->
流程:startActivity ---》 Activity --》Instrumentation ---> AMS检查 --》ActivityThread --》
--> 获取自定义的LoadedApk.ClassLoader --> 实例化 initializeJavaContextClassLoader(PMS检查要启动的包名是否安装)
-->生命周期方法的处理 (才能真正启动加载到 插件包里面的Activity)
PMS检测 插件包包名是否安装,如果没有安装就 会抛出异常:
java.lang.RuntimeException: Unable to instantiate application android.app.Application: java.lang.IllegalStateException:
Unable to get package info for com.netease.plugin_package; is package not installed?
pi = pm.getPackageInfo(mPackageName, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
UserHandle.myUserId());
Hook 我们要在 getPackageInfo 执行之前 给Hook拦截住,控制pi不为null
分析:pm.getPackageInfo 客户端进程 ----》 PMS进程-检测是否安装了插件和宿主融为一体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 第一步:找到宿主 dexElements 得到此对象 PathClassLoader代表是宿主
// 第二步:找到插件 dexElements 得到此对象,代表插件 DexClassLoader--代表插件
// 第三步:创建出 新的 newDexElements [],类型必须是Element,必须是数组对象
// 第四步:宿主dexElements + 插件dexElements =----> 融合 新的 newDexElements for遍历
// 第五步:把新的 newDexElements,设置到宿主中去
以上操作,就可以去加载插件里面的class
需要去加载插件里面的 layout
StringBlock ---》 string.xml color.xml anim.xml ...
mStringBlocks[] == string.xml color.xml anim.xml
只有mStringBlocks[] 初始化了,才能去加载 插件里面的资源总结
1
2
3
4
5
6
7
8
9
10
11
12
13
14
151.占位插件化 Activity - ProxyActivity --》插件里面,在插件开发中,必须时时刻刻记住是 是使用宿主的环境
2.占位插件化 Service - ProxyService --》插件里面,在插件开发中,必须时时刻刻记住是 是使用宿主的环境
3.占位插件化 动态广播 - ProxyReceiver --》插件里面,在插件开发中,必须时时刻刻记住是 是使用宿主的环境
4.占位插件化 静态广播 -- 分析系统是如何解析APK文件的,源码的阅读,阅读源码的思路,PMS入手-(模仿系统是如何解析,我们就怎么解析)(难点)
稳定,插件化开发很痛苦 -------》 开源中的框架 插件化框架 DL
5.Hook从入门 到 熟练 --(1.替换,2.被替换的 动态代理/接口设置) --> Hook系统源码 -- (Hook1)AMS检测是否注册 --- (Hook2)换回来
6.安卓的类加载 --》PathClassLoader(加载运行App中的class),DexClassLoader(apk zip), DexPathList Element,插件Element和宿主Element
7.真正的融合--》就可以加载插件里面的class, 插件里面的Layout怎么去加载呢,AseetManager Resources (难点)
Hook式插件化框架 --》 融为一体,Hook方式: 不用考虑宿主的环境,不稳定==兼容性, 360开源框架 Hook方式实现的
8.LoadedApk插件化-- ActivityThread LAUNCH_ACTIVITY源码切入点,ClassLoader--》宿主 --> LoadedApk.ClassLoader --> 宿主class
9.自定义LoadedApk.自定义ClassLoader ---> 插件的class, mPackages -- size=2 , 没法绕过PMS检测,检测插件包名是否安装了
10.开关 切换 宿主 插件, 再次运行会报错,是因为 PMS检测, Hook PMS,绕过了pi==null的情况
不稳定==兼容性Hook代码推理技巧
1
2
31.Hook反射处理,倒序写代码,只写一行代码,推理千万行代码
2.兼容的 21 ~ 28 系统版本 -- Hook插件化框架 手写