安卓插件化课程-第五篇:插件中的activity使用插件中资源

 序:

1.本文是安卓插件化课程的第五篇,完整课程链接参见下面链接:

安卓插件化课程-序章_分享+记录-CSDN博客前言:目前安卓领域,插件化十分的流行,本以为这一类的文章会有很多,但是百度一搜,基本上讲的插件化都是皮毛,没有涉及到核心。所以就想写一系列的文章来一步一步深入的讲解插件化,通过实现逐渐增加难度的需求,最终实现插件化方案。章节:插件化系列文章主要包含以下几个篇章:1.加载插件apk中的类2.启动插件apk中的activity3.使用插桩的方式启动apk中的activity4.使用插件apk中的资源5.使用插件apk中的so文件6.插件化加载移动apk。7.插件化目前所遇到的https://blog.csdn.net/rzleilei/article/details/122453035?spm=1001.2014.3001.5502https://blog.csdn.net/rzleilei/article/details/122453035?spm=1001.2014.3001.5502

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(R.id.text2)).setText(string);
    }
}

layout也很简单:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/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 android.app.Activity.initWindowDecorActionBar(Activity.java:3506)
        at android.app.Activity.setContentView(Activity.java:3521)
        at com.xt.appplugin.Plugin4Activity.onCreate(Plugin4Activity.java:21)
        at android.app.Activity.performCreate(Activity.java:8051)
        at android.app.Activity.performCreate(Activity.java:8031)
        at android.app.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(使用插件中的资源)。

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

三:要点总结

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

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

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

四。代码地址:


项目地址:

GitHub - aa5279aa/android_all_demo: 一直觉得研究各种技术,一个个demo的下载运行太费劲了,为什么不能有一个所有新技术的融合体demo呢?项目为此而生

插件项目位置:android_all_demo/DemoClient/appplugin at master · aa5279aa/android_all_demo · GitHub

调用类位置:android_all_demo/DynamicFragment.kt at master · aa5279aa/android_all_demo · GitHub


版权声明:本文为AA5279AA原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。