2.主要内容

上一篇章已经讲了如何去使用插件中的资源。但是也仅限

本篇主要讲如何启动插件中的activity,并且插件中的activity可以正常使用插件中的资源文件(不包含layout)。

一:原理简述

1.activity中如何获取资源

上一篇章中,我们已经实现了在宿主中使用插件中的资源,但是有一个限制,只能通过我们自定义的resource才能获取到插件的资源。那么问题就来了,如果我们在插件actvitiy中使用资源文件的,获取到的resource是哪里额呢?

我们activity中调用getResource()方法,最终调用到的是ContextThemeWrapper中的getResources()方法,最终会调用到getResourcesInternal()。

@Override
    public Resources getResources() {
        return getResourcesInternal();
    }

    private Resources getResourcesInternal() {
        if (mResources == null) {
            if (mOverrideConfiguration == null) {
                mResources = super.getResources();
            } else {
                final Context resContext = createConfigurationContext(mOverrideConfiguration);
                mResources = resContext.getResources();
            }
        }
        return mResources;
    }

在看super.getResources(),最终调用的其实是ContextImpl中的mResources对象。

2.插件中如何获取资源

看了获取resource的流程,我们可以知道,如果替换掉activity最终获取getResources返回的对象,那么就可以实现在插件中获取插件中的资源内容。如何替换,我们发现有两个地方可以尝试hook。

1.ContextThemeWrapper中存有mResources的缓存,如果我们替换掉这个缓存,那么activity中获取到的就是我们自定义的mResources对象了。

2.在ContextThemeWrapper中的mResources创建之前,直接替换掉ContextImpl中的mResources。

考虑到影响范围,我们选择影响更小一点的第一种方案。既然确定了可以hook的点,那么下一步我们就考虑hook的时机了。

3.替换时机

时机肯定要在onCreate方法之前,因为插件中在onCreate方法中就会使用到很多资源文件。插件中的actiivty我们是不能去改造的,所以通过重写父类方法的方案也是不行的。

这时候我们又想到了Instrumentation,因为activity就是它来负责创建的,在创建后,在onCreate之前,我们就可以去操作。最终我选择callActivityOnCreate方法,activity的onCreate就是它来负责调用的,所以作为替换的时机最为合适。

二:代码编写

1.插件项目中创建Plugin3Activity

这里引用了插件中的string,name:plugin_str1

public class Plugin3Activity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Resources resources = getResources();
        setContentView(R.layout.layout_plugin3);
        String string = resources.getString(R.string.plugin_str1);
        ((TextView) findViewById(.text2)).setText(string);
    }
}

layout也很简单:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http:///apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="layout中直接使用字符串layout_plugin3" />

    <TextView
        android:id="@+id/text2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/plugin_str3" />
</LinearLayout>

2.编译项目,拷贝apk

老步骤了,编译后拷贝APK到宿主的asset目录

3.宿主项目中,重写Instrumentation中的callActivityOnCreate方法

@Override
    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        //替换掉resource
        Resources plugin = DynamicResourceManager.getInstance().resourcesMap.get("plugin");
        if (plugin != null) {
            try {
                Field declaredField = ContextThemeWrapper.class.getDeclaredField("mResources");
                declaredField.setAccessible(true);
                declaredField.set(activity, plugin);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        super.callActivityOnCreate(activity, icicle);
    }

我们通过反射获取到mResources对象,然后替换成我们自定义的Resources。

4.启动Plugin3Activity

if (position == 5) {
            //activity中使用插件的png和string
            if (DynamicResourceManager.getInstance().resourcesMap["plugin"] == null) {
                ToastUtil.showCenterToast("请先点击使用插件中的资源")
                return
            }
            val intent = Intent(context, HostActivity::class.java)
            intent.putExtra(MyInstrumentation.ClassName, "com.xt.appplugin.Plugin3Activity")
            startActivity(intent)
            return
        }

5.修复资源崩溃

这时候点击启动,竟然意外的发现崩溃了,崩溃日志如下:

Caused by: android.content.res.Resources$NotFoundException: Resource ID #0x7f0b0001
        at android.content.res.ResourcesImpl.getValueForDensity(ResourcesImpl.java:234)
        at android.content.res.Resources.getDrawableForDensity(Resources.java:982)
        at android.content.res.Resources.getDrawable(Resources.java:922)
        at android.content.Context.getDrawable(Context.java:753)
        at com.android.internal.widget.ToolbarWidgetWrapper.setIcon(ToolbarWidgetWrapper.java:322)
        at com.android.internal.widget.ActionBarOverlayLayout.setIcon(ActionBarOverlayLayout.java:755)
        at com.android.internal.policy.PhoneWindow.setDefaultIcon(PhoneWindow.java:1812)
        at .Activity.initWindowDecorActionBar(Activity.java:3506)
        at .Activity.setContentView(Activity.java:3521)
        at com.xt.appplugin.Plugin4Activity.onCreate(Plugin4Activity.java:21)
        at .Activity.performCreate(Activity.java:8051)
        at .Activity.performCreate(Activity.java:8031)
        at .Instrumentation.callActivityOnCreate(Instrumentation.java:1329)
        at com.xt.client.function.dynamic.hook.MyInstrumentation.callActivityOnCreate(MyInstrumentation.java:64)

排查步骤就不说了,直接说原因吧。onCreate创建界面的时候,系统会去尝试创建actionBar,创建的时候会使用到APP中的图片资源。因为我们是在宿主中执行的,获取对应的图片资源ID也是宿主的,而这里的resouces我们已经替换成我们自定义的了,所以自定义的resouces肯定找不到宿主的图片资源,就崩溃了。

解决方案想到了两个:

第一,可以不让activity去加载actionBar。但是这个等于说对插件APP产生了要求,所以放弃了。

第二,自定义resouces中做一层补偿逻辑,如果自身获取不到,可以从宿主resources中获取。

最终采用的是第二种方案,重写Resouces方法的getDrawable方法。

@Override
    public Drawable getDrawable(@DrawableRes int id, Theme theme)
            throws NotFoundException {
        try {
            return getDrawableForDensity(id, 0, theme);
        } catch (Exception e) {

        }
        return DynamicResourceManager.getInstance().resourcesMap.get("host").getDrawable(id, theme);
    }

这里host是在加载资源插件的时候缓存的:

DynamicResourceManager.getInstance().resourcesMap["host"] = getResources()

6.测试验证

点击加载插件,然后点击启动插件activity(使用插件中的资源)。

我们可以看到插件已经正常启动了。

android 插件化 资源错乱 安卓 插件化 资源_插件化

三:要点总结

1.activity中的resources,最终来自于ContextImpl,每个activity都存在一份缓存resources。

2.Activity和Application的Resouces对象并不是一个,本文虽然没介绍,但是调试过程中发现了。并且不同Activity的Resouces对象也不一定是一个。

3.替换掉Instrmentation之后,在callActivityOnCreate方法中,我们可以做很多事情,这是我们hook的最佳时机。因为此时activity中的各种对象都已经赋值,而onCreate恰好还没有执行。