0%

04-插件化-第三节

HookAMS 使用的是动态代理,Hook Handler 使用的是反射。

Android 10 的 AMS 变成 ATMS ,Singleton 貌似也变了,需要注意适配。Handler 的话,由于原理没变,一般不需要另行适配

二、资源加载

我们加载资源就关注2个地方:

  • assets 目录下的资源

  • res 目录下的资源

资源替换有2种可选方案:

  • 插件与宿主的资源直接合并,但是可能引起资源冲突

  • 专门创建一个新的 AssetManager (或者 Resource) 来加载插件的资源

2.1 创建 Resources 对象

首先通过 class.newInstance (这是因为 AssetManager 不能直接去new ,hide 的)的方式创建一个 AssetManager 对象 myAssetManager,然后通过 addAssetPath 方法将插件的路径给加进去。之后new 出来 Resources 对象,传入这个创建的 myAssetManager 即可。

那么,怎么让插件的 Activity 去用上这个 Resources 对象呢?我们可以借助 Application 去实现这个事情,分为以下几步:

  1. 在宿主中定义 Application ,并且其 getResource 返回我们自己创建的 Resource 对象

  2. 插件 Activity 都继承 BaseActivity

  3. 在插件的 BaseActivity 中,getResource 返回 getApplication.getResource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//宿主里面
public class MyApplication extends Application {

private Resources mResources;

@Override
public void onCreate() {
super.onCreate();
mResources = LoadUtil.loadResource(this);
}

@Override
public Resources getResources() {
return mResources == null ? super.getResources() : mResources;
}
}
1
2
3
4
5
6
7
8
9
10
11
//插件的 BaseActivity
public class BaseActivity extends AppCompatActivity {

@Override
public Resources getResources() {
if (getApplication() != null && getApplication().getResources() != null) {
return getApplication().getResources();
}
return super.getResources();
}
}

但是,这样对 宿主 App 是会有影响的,我们能不能插件自己的资源插件自己去加载?并且我们知道,插件的 Application 是不会被执行的。为了不影响宿主,我们可以在 BaseActivity 中去做 Resource 的加载:

1
2
3
4
5
6
7
8
9
public class BaseActivity extends AppCompatActivity {

@Override
public Resources getResources() {
Resources resources = LoadUtil.getResources(getApplication());
// 如果插件作为一个单独的app,返回 super.getResources()
return resources == null ? super.getResources() : resources;
}
}

这里要尤其注意的是,LoadUtil.getResouces(context) 的时候一定要传入 Application 这个Context, 如果传入this 也就是当前 Activity 的Context ,就会导致循环调用了。 看下 LoadUtil 的实现:

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
public class LoadUtil {

private final static String apkPath = "/sdcard/plugin-debug.apk";

private static Resources mResources;

public static Resources getResources(Context context) {
if (mResources == null) {
mResources = loadResource(context);
}
return mResources;
}

private static Resources loadResource(Context context) {
// assets.addAssetPath(key.mResDir)
try {
AssetManager assetManager = AssetManager.class.newInstance();
// 让 这个 AssetManager对象 加载的 资源为插件的
Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, apkPath);

// 如果传入的是Activity的 context 会不断循环,导致崩溃
Resources resources = context.getResources();

// 加载插件的资源的 resources
return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

2.2 冲突导致崩溃

不过,这里要提一嘴,我们通过插件的 BaseActivity 这种方式能够完成插件资源加载并且不报错,是因为我们的 BaseActivity 继承了 Activity ,如果我们继承了 AppcompatActivity 的话,就会产生崩溃了,会提示这段代码:

1
2
3
mDecorContentParent = (DecorContentParent) subDecor
.findViewById(R.id.decor_content_parent);
mDecorContentParent.setWindowCallback(getWindowCallback());

其中的 mDecorContentParent 为 null ,为什么会这样呢?我们可以看打出的包中,我们知道,R.id.xxx 最终在打包好的 Apk 中是 数值存在的,一般是 0x7fxxxxx之类的,我们可以看下宿主APk 和 插件 APk 中 R.id.decor_content_parent 最终的数值:

// 宿主的
0x7f07004e decor_content_parent false

// 插件的
0x7f07004d decor_content_parent false

所以,在 宿主中,编译后应该是这样的:

1
2
mDecorContentParent = (DecorContentParent) subDecor
.findViewById(0x7f07004e);

在插件中应该是这样的:

1
2
mDecorContentParent = (DecorContentParent) subDecor
.findViewById(0x7f07004d);

说明二者是不一样的值,一个是 4e ,一个是 4d 。而由于双亲委派的原因,这个AppcompatActivity 用的是宿主的,所以就会产生问题。注意,这个问题不论是aapt 修改还是new 出来 Resourse(AssetManger)的方式,都会出现

这里还是没怎么搞懂,讲道理因为宿主有 0x7f07004e ,那么插件在运行的时候也能找到这个 0x7f07004e 这个资源。

老师说是因为 插件用的还是宿主那个 Context 导致的,所以需要在插件 BaseActivity 中创建自己的 Context (注意,这个操作不会影响宿主的,指挥对插件有影响):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BaseActivity extends AppCompatActivity {

protected Context mContext;
// 不会影响到宿主
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Resources resources = LoadUtil.getResources(getApplication());

mContext = new ContextThemeWrapper(getBaseContext(), 0);

Class<? extends Context> clazz = mContext.getClass();
try {
Field mResourcesField = clazz.getDeclaredField("mResources");
mResourcesField.setAccessible(true);
mResourcesField.set(mContext, resources);
} catch (Exception e) {
e.printStackTrace();
}
}
}

然后,我们在插件的 Activity 中都要用这个创建出来的 mContext 去加载资源:

1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends BaseActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e("leo", "onCreate: 启动插件的Activity");

View view = LayoutInflater.from(mContext).inflate(R.layout.activity_main, null);
setContentView(view);
}
}

这个看起来很麻烦,还需要自己创建Context ,那么为什么步通过反射替换 Activity 的Context 呢?这还是因为双亲委派操作,如果你替换了 Activity 的,根据双亲委派原则,宿主也会受影响了。

2.3 宿主与插件的资源合并

只要不与系统的 id 冲突,可以自己去设置id ,你都可以 0x10 开头,不过我们约定一般是 0x70 等开始,到 0x7e

插件化,建议宿主的 dex 文件在前面,不然如果插件的代码有问题,就把宿主也带崩了。所以,宿主先加载会好点。

谢谢你的鼓励