Android activity exported属性理解

背景

这么久了,我自己看来对此属性的理解有点小偏差,当然不是表面上的理解误差,而是涉及到具体实现的细节。这里先贴下官方关于此属性的解释:

android:exported
This element sets whether the activity can be launched by components of other applications — “true” if it can be, and “false” if not. If “false”, the activity can be launched only by components of the same application or applications with the same user ID.
If you are using intent filters, you should not set this element “false”. If you do so, and an app tries to call the activity, system throws an ActivityNotFoundException. Instead, you should prevent other apps from calling the activity by not setting intent filters for it.

If you do not have intent filters, the default value for this element is “false”. If you set the element “true”, the activity is accessible to any app that knows its exact class name, but does not resolve when the system tries to match an implicit intent.

This attribute is not the only way to limit an activity’s exposure to other applications. You can also use a permission to limit the external entities that can invoke the activity (see the permission attribute).

这段文字说明,值得多读几遍!!!

由于我们团队的关系,我们开发的模块经常需要集成到多个app中,而我们不想为某个app单独维护一份代码,即我们的开发中,所有的宿主app用的都是同一套代码。比如就存在类似这样的代码:

<activity
            android:name=".SubActivity"
            android:configChanges="orientation|keyboardHidden"
            android:exported="false" // 注意这行代码!!!
            android:screenOrientation="portrait"
            android:windowSoftInputMode="stateHidden">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="mlpf" />
                <data android:host="sub" />
            </intent-filter>
        </activity>

这样在宿主app里,通过打开mlpf://sub这样的短链就能轻松地来到我们模块的SubActivity。注意这里android:exported=false的设置,因为如果不设置的话,根据Android的规则只要有intent-filter存在,那么exported就是true,即对外暴露的;而这里我们显然不希望是对外暴露的,因为如果这样的话,当安装了多个集成了我们模块的App时,当要打开这样的短链请求时系统就会弹出选择框让用户选择在哪个app里打开,这当然不是我们期望的。

当同一个设备上装了我们的多个app的时候,在8.0之前都是ok的,即A app里的SubActivity和B app里的SubActivity互相是没任何关系的,也是互相看不到对方的,这是我们对exported=false的认识;直到上周某天晚上快要下班了,QA同学拿着升级到8.0的Nexus 6P跟我说,你看你们这个页面跳不过去了,还弹出了个讨厌的没有应用可执行此操作的提示,我当时也是一脸懵逼啊,但心里已经有种不祥的预感,看起来像是google改出来的bug。

我接过设备,点击了几下,确保能复现,然后连着电脑,看了下adb logcat关于ActivityManager相关的输出,果然我们这个intent没有找到对应的cmp(component),而是到了系统的ResolverActivity,ResolverAct大家都知道,当系统找到了多个目标或者没目标时会弹出它提醒用户。这就有点奇怪了,同样的case在7.x的设备上就是好的,虽然行为上也是到了ResolverAct,但ResolverAct内部最终还是导到了本app内部的SubActivity,最终正确调起了。

解惑

有一点我们需要知道,即当我们通过Intent打开act的时候,系统内部会调用
Intent.resolveActivity(pm),其内部又会接着调用PackageManager#resolveActivity。另外你也可以调用PackageManager#queryIntentActivities来查看某个intent究竟可以被哪个act处理。有一点需要特别注意的是,这些方法会考察设备上所有安装的app里的activity,即使是那些被显式标记成了exported=false的act,这就是我上文说到的理解偏差,这让我很惊讶。因为我以前的认识中,既然标记了不对外暴露,那么这些act也不应该被找到才对,但很可惜,看起来Android的实现不是这样的,关于这点,可以参考以下问题:Android queryintentactivities.

7.x(包括)之前虽然也能查到别的app里exported=false的act(这个行为看起来一直都有),但最终会正确打开匹配到的本app里exported=false的act,但在8.0上这个行为break掉了,直接变成了上文提到的“没有应用可执行此操作”,真是一个忧伤的故事。

8.0解决办法

关于8.0的这个问题,AOSP上也有人报了bug:intent有多个match时无法正确跳转。不过看起来仅仅是个没多少关注的P3bug,而且我手头的5x升级到了8.1.0,此问题依然存在,看来指望google修复希望不大。还好我们也有办法处理下,看下Intent.setPackage方法,如下:

/**
     * (Usually optional) Set an explicit application package name that limits
     * the components this Intent will resolve to.  If left to the default
     * value of null, all components in all applications will considered.
     * If non-null, the Intent can only match the components in the given
     * application package.
     *
     * @param packageName The name of the application package to handle the
     * intent, or null to allow any application package.
     *
     * @return Returns the same Intent object, for chaining multiple calls
     * into a single statement.
     *
     * @see #getPackage
     * @see #resolveActivity
     */
    public Intent setPackage(String packageName) {
        if (packageName != null && mSelector != null) {
            throw new IllegalArgumentException(
                    "Can't set package name when selector is already set");
        }
        mPackage = packageName;
        return this;
    }

我们面临的主要问题就是系统API在startActivity的过程中查到了别的app里面的非暴露act,这个方法看起来刚好可以将系统的这个查找行为局限在本app内,所以我们的fix如下:

    if (Build.VERSION.SDK_INT >= 26) {
        intent.setPackage(mContext.getPackageName());
    }

最后,关于exported=false的实现,我个人的看法是应该再提早些,直接一开始在匹配的过程中就找不到这样的act,而不是一股脑全找到(导致本来就1个target满足,结果找了多个出来),等到最后要打开了,看下exported是false,才弹个无权限的错误!!!之前魅族更新了次系统后也出过这问题,弹出让用户选,结果选了之后又告诉用户无权限(因为实际是exported=false的activity)。就像在实现某个方法的时候,有些前置条件不满足,我们应该尽早return,而不是埋头做了很多工作后,才检查一些必要条件,发现不对了才退出,fail fast常常是很好用的策略。

ps:实在是没明白google这里的实现为啥要找到这些实际上private的act,看起来完全是在做无用功啊,反正怎么着都不可能打开,你把它找出来干啥呢!!!有想法的同学可以留言交流下,谢谢。

    原文作者:tmp_zhao
    原文地址: https://www.jianshu.com/p/05cf37ee971b
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞