你可能不知道的Android Context动态升降级与Context权限范围限定

2 阅读9分钟

MutableContextWrapper 的使用与应用场景

前言

Android 的 Context 大家肯定是不陌生了,它是安卓开发中不可或缺的概念,它允许我们访问系统资源,管理应用程序的生命周期,并与系统交互。

实际开发中我们在使用 Toast,使用 Dialog,启动 Activity 与 Service ,加载资源,操作数据库,获取 App 相关的文件路径,创建 View 等操作时,都会涉及到一个 Context 引用。

而 Context 引用我们用到最多的就是 Activity 和 Application 两种实现,那么它们 Context 到底有什么区别,哪个更好呢?

这个问题留待下文作答,其实就算大家说不出来我相信大家都应该有所体会,💪 如果学习深入的一些同学可能连八股都能背出来 👍 ,如果大家对这方面想要有深入了解的话,可以搜索引擎搜索 Context 的原理与源码实现出现一大把的分析,这里我就不献丑了,这也不是本文重点。

本文主要说一下 MutableContextWrapper 的使用与应用场景。为什么有了 ContextWrapper 还需要 MutableContextWrapper ,有什么不同?有哪些用法?实际开发中哪些场景会用到?

一、前世今生,简单哼哼两句

不讲源码不讲八股,真的就是简单哼哼两句,为什么要有 MutableContextWrapper 。

Context 它是一个抽象类,它的执行被Android系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源的上下文,也就是说Context描述一个应用程序环境的信息。

它的具体实现如下,上经典图:

image.png

ContextWrapper 是 Context 类的一个封装类,而 ContextImpl 是 Context 抽象类的实现类。ContextWrapper 的构造函数中有一个真正的 Context 的引用不可通过外部传入

public class ContextWrapper extends Context {
    @UnsupportedAppUsage
    Context mBase;

    public ContextWrapper(Context base) {
        mBase = base;
    }
}

ContextThemeWrapper 作为 ContextWrapper 一个扩展,它是重写了 ContextImpl 中的一些关于 Theme 的实现,它提供的 Theme 是整个 APP 的 Theme ,而这里扩展了之后,支持了 Theme 的替换之后,在不同的页面支持了不同的 Theme 设置。

可以参考清单文件中 Application 标签设置全局 Theme 样式,不同的 Activity 可以设置单独的 Theme 。

这里点一下文章开头的问题,不同的上下文的功能有些差异:

image.png

例如在启动一个 Activity 和 Dialog 时,不推荐使用 Application 和 Service 。Android 系统出于安全原因的考虑,是不允许 Activity 或 Dialog 凭空出现的,一个Activity 的启动必须要建立在另一个 Activity 的基础之上,也就是以此形成的返回栈。而 Dialog 则必须在一个 Activity 上面弹出。前三个操作基本不可能在 Application 中出现。实际上,只要把握住一点,凡是跟 UI 相关的,都应该使用 Activity 做为 Context 来处理。

Context 作为应用程序环境和运行时状态的信息,设计初衷上它应该是固定的,在创建成功之后就禁止改变,在 ContextWrapper 中也有提现,但是随着应用的发展,一些特殊场景下比如跨页面使用View,或者提前创建View的时候,其实会有涉及到替换Context的情况。

所以在SDK31中(可兼容老版本)之后,官方提供了一个特殊版本的 ContextWrapper,也就是 MutableContextWrapper 它提供了在运行时动态更改和扩展上下文的功能,特别是允许替换基础上下文,这使得开发者可以动态地更改应用程序的行为,而不需要重新启动应用程序。

public class MutableContextWrapper extends ContextWrapper {
    public MutableContextWrapper(Context base) {
        super((Context)null);
        throw new RuntimeException("Stub!");
    }

    public void setBaseContext(Context base) {
        throw new RuntimeException("Stub!");
    }
}

可以清楚的看到 MutableContextWrapper 需要通过外部传入上下文。

说了半天 MutableContextWrapper 有哪些应用场景呢?

二、上下文动态替换实现升级与降级处理

比较典型的例子就是自定义 View 的创建使用 Application 级别的 Context 还是 Activity 级别的 Context 。

一些预创建的自定义 View ,我们可以使用 ApplicationContext 初始化,然后在对应 Activity 显示的时候替换为当前的 Context 。在销毁的时候如果是全局的自定义View 或者其他地方还会用到的话,需要释放当前的 Context 替换为 ApplicationContext 。

这就是一次简单的上下文升级与降级处理。

如果用全局的 ApplicationContext 创建自定义 View 不能用吗?大概率是能用的,但是会受限。

比如生命周期受限 Application 的上下文是全局的,而 Activity 的上下文与特定的 Activity 相关联。如果有生命周期的处理逻辑会受限。

比如资源访问受限 Application 的上下文可以访问应用程序的全局资源,例如应用程序级别的字符串、样式和尺寸等。而 Activity 的上下文则可以访问 Activity 单独的资源,例如特定 Activity 的布局文件或主题等。如果有资源或主题的逻辑会受限。

UI处理受限,在自定义 View 中处理一些 UI 逻辑如弹窗,吐司等会受限。

当然如果你并没有这些处理,只是简单的自定义 View 展示,那大概率是能直接用 ApplicationContext 一把梭,反正有问题再改呗。

如果要做上下文升级降级处理,一定要做好回收的处理,不然容易出现内存泄露,最好是封装一波。

市面上比较流行的用法就是 WebView 的缓存处理。

object WebViewPoolManager {

    private val webViewCache: MutableList<MyWebView> = ArrayList(1)

    private fun create(context: Context): MyWebView {
        return MyWebView(context)
    }

    /**
     * 初始化
     */
    @JvmStatic
    fun prepare(context: Context) {
        if (webViewCache.isEmpty()) {
            Looper.getMainLooper()
            Looper.myQueue().addIdleHandler {
                webViewCache.add(create(MutableContextWrapper(context)))
                false
            }
        }
    }

    /**
     * 获取WebView
     */
    @JvmStatic
    fun obtain(context: Context): MyWebView {

        if (webViewCache.isEmpty()) {
            webViewCache.add(create(MutableContextWrapper(context)))
        }
        val webView = webViewCache.removeFirst()
        val contextWrapper = webView.context as MutableContextWrapper
        contextWrapper.baseContext = context
        webView.clearHistory()
        webView.resumeTimers()
        return webView
    }

    /**
     * 回收资源
     */
    @JvmStatic
    fun recycle(webView: MyWebView) {
        try {
            webView.stopLoading()
            webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
            webView.clearHistory()
            webView.pauseTimers()
            webView.clearFormData()
            webView.removeJavascriptInterface("webkit")

            // 重置并回收当前的上下文对象,根据池容量判断是否销毁,也可以置换为ApplicationContext
            val contextWrapper = webView.context as MutableContextWrapper
            contextWrapper.baseContext = webView.context.applicationContext

            val parent = webView.parent
            if (parent != null) {
                (parent as ViewGroup).removeView(webView)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            if (!webViewCache.contains(webView)) {
                webViewCache.add(webView)
            }
        }
    }

    /**
     * 销毁资源
     */
    @JvmStatic
    fun destroy() {
        try {
            webViewCache.forEach {
                it.removeAllViews()
                it.destroy()
                webViewCache.remove(it)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

}

对于这种池化技术,再加上上下文的升级与降级处理,就对内存优化很友好,关于 WebView 为什么要用 Activity 级别的 Context ,还是上面的观点,如果你用到了 WebView 中的一些标签不起作用,H5的某些标签无法弹框之类的,那你肯定是用的 ApplicationContext 。赶紧换吧!

除了这些比较经典的用法,我们还能用 MutableContextWrapper 实现局部的上下文处理范围。

三、限定上下文处理范围

举一个 XX 的例子:

image.png

比如一个页面中,我们有三个容器,A 和 C 要求正常的按设备的国际化展示文本,而 B 容器我想固定展示指定的国际化布局。

有办法做到吗?当然有,之前没有 MutableContextWrapper 的时候咱们也不是没有硬写过:

给布局打个Tag

<FrameLayout
    android:id="@+id/special_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:tag="special_container">
    ...
</FrameLayout>

我们把带国际化的布局和不带国际化的布局分为两个不同的xml,然后判断之后分别加载

// 获取当前语言环境
Locale currentLocale = getResources().getConfiguration().locale;
String currentLanguage = currentLocale.getLanguage();

// 获取特定容器的引用
FrameLayout specialContainer = findViewById(R.id.special_container);

// 判断是否需要使用英文布局
if (currentLanguage.equals("en")) {
    // 使用英文布局
    LayoutInflater.from(this).inflate(R.layout.special_container_english, specialContainer);
} else {
    // 使用默认布局
    LayoutInflater.from(this).inflate(R.layout.special_container, specialContainer);
}

而现在有了 MutableContextWrapper 之后我们就能直接为这个 special 的容器指定一个特殊的上下文环境即可。

    val flBox1 = findViewById<ViewGroup>(R.id.fl_box1)
    LayoutInflater.from(mActivity).inflate(R.layout.inflate_context_item, flBox1, true)
    val test1Str = resources.getString(R.string.test1)
    YYLogUtils.w("test1Str:$test1Str")  //打印默认值

    // 创建一个自定义的 MutableContextWrapper,在特定的范围内上下文语言属性因为英文
    val wrapper = MutableContextWrapper(baseContext).apply {
        resources.updateConfiguration(Configuration().apply {
            setLocale(Locale.ENGLISH)
        }, resources.displayMetrics)
    }

    val flBox2 = findViewById<ViewGroup>(R.id.fl_box2)
    LayoutInflater.from(wrapper).inflate(R.layout.inflate_context_item, flBox2, true)
    val test2Str = wrapper.resources.getString(R.string.test1)
    YYLogUtils.w("test2Str:$test2Str")  //打印指定限定上下文范围的值

布局如图,可以做正常的国际化:

image.png

image.png

效果图:

image.png

打印 resources 资源不同的 Context 打印的 String 资源:

image.png

如果哪个文本或者哪个容器想要特殊的国际化,直接使用这个 MutableContextWrapper 加载即可,而除了国际化的自定义,在 Activity 或 Application 的其他资源都可以通过此方法重设上下文的处理范围,包括 Activity 的样式和 Application 的全局样式,应用场景太多就不一一举例了。

这样是不是更方便呢?

四、上下文资源访问权限限制

上下文限定的资源访问是一种通过对上下文对象进行包装,并在内部重写资源访问方法来限制资源访问权限的方法。

我们可以在 MutableContextWrapper 的内部重写了 getResources() 方法,返回一个受限制的 Resources 对象。这个受限制的 Resources 对象是 RestrictedResources 类的实例。

在 RestrictedResources 类中,重写了 getString() 方法来限制资源访问。只有当请求的资源 ID 与 指定 ID 相同时,才会调用父类的 getString() 方法获取资源。如果请求的资源 ID 不是 指定的 ID,就会抛出 NotFoundException 异常,表示资源不存在。

通过这种方式,你可以在特定的上下文中限制对资源的访问权限。

伪代码如下:

// 创建一个受限制的 MutableContextWrapper
MutableContextWrapper restrictedContextWrapper = new MutableContextWrapper(originalContext) {
    @Override
    public Resources getResources() {
        // 返回一个受限制的 Resources 对象,只允许访问特定的资源
        return new RestrictedResources(super.getResources());
    }
};

// 在 RestrictedResources 类中对资源访问进行限制
private static class RestrictedResources extends Resources {

    ...

    @Override
    public String getString(int id) throws NotFoundException {
        // 例如限制只能访问特定的字符串资源,其他资源将抛出异常
        if (id == R.string.app_name) {
            return super.getString(id);
        } else {
            throw new NotFoundException();
        }
    }
}


// 只要使用受限制的上下文进行资源访问,不符合的访问会抛异常
String appName = restrictedContextWrapper.getString(R.string.app_name);

需要注意的是,这种方法只是一种简单的示例,实际使用时可能需要根据具体需求进行修改和扩展。同时,要谨慎使用上下文限定的资源访问,确保不会影响应用程序的正常运行和用户体验。

后记

当然了 Context 的使用场景不限制我使用示例的这些,而 MutableContextWrapper 的使用场景也绝不限制于我示例的这些场景。

只要是 Context 的使用场景,如获取资源,加载布局,加载样式,初始化布局等都可以用 MutableContextWrapper 扩展对应的使用场景。

虽然有了动态的 Context 之后其实有了更多的扩展空间,但是也需要注意内存泄露的问题,动态虽好不要贪杯!

关于本文的内容如果想查看源码可以点击这里 【传送门】。你也可以关注我的这个Kotlin项目(虽然是两年前的老项目了)我有时间都会持续更新。

如果本文有错漏的地方或者有其他更好的方案,希望同学们可以在评论区指出或交流。

好了,如果感觉本文对你有一点点的启发与帮助,还望你能点赞支持一下,你的支持是我最大的动力啦。

Ok,这一期就此完结。