我们知道在AndroidManifest.xml文件中,四大组件都有android:exported属性,是个boolean值,可以为true或false。Activity的exported属性在单个App可能用得比较少,但对于对外接口的Activity或公司内部多个应用间接口调用的设计会有比较大的影响。

来自官方文档的描述

下面来详细的了解一下四大组件中的这个属性:

1. Activity

<activity
          ……
          android:exported=["true" | "false"]
          ……
/>

深入理解  Android 组件的 exported 属性-LMLPHP
Activity 是否可由其他应用的组件启动 —“true”表示可以,“false”表示不可以。若为“false”,则 Activity 只能由同一应用的组件或使用同一用户 ID 的不同应用启动。
默认值取决于 Activity 是否包含 Intent 过滤器。没有任何过滤器意味着 Activity 只能通过指定其确切的类名称进行调用。 这意味着 Activity 专供应用内部使用(因为其他应用不知晓其类名称)。 因此,在这种情况下,默认值为“false”。另一方面,至少存在一个过滤器意味着 Activity 专供外部使用,因此默认值为“true”。

该属性并非限制 Activity 对其他应用开放度的唯一手段。 您还可以利用权限来限制哪些外部实体可以调用 Activity(请参阅 permission 属性)。

2. Service中的:

<service android:enabled=["true" | "false"]
         android:exported=["true" | "false"]
         android:icon="drawable resource"
         android:isolatedProcess=["true" | "false"]
         android:label="string resource"
         android:name="string"
         android:permission="string"
         android:process="string" >
    . . .
</service>

深入理解  Android 组件的 exported 属性-LMLPHP

意思如下:
该属性用来标示,其它应用的组件是否可以唤醒service或者和这个service进行交互:true可以,false不可以。如果为false,只有同一个应用的组件或者有着同样user ID的应用可以启动这个service或者绑定这个service。

默认值根据当前service是否有intent filter来定。如果没有任何filter意味着当前service只有在被详细的描述class name后才会被唤醒。这意味这当前service只能在应用内部使用(因为其它应用不知道这个class name).所以在这种情况下它的默认值为 false.从另一方面讲,如果至少有一个filter的话那么就意味着这个service可以被外部应用使用,这种情况下默认值为true。

其实,不只有这个属性可以指定service是否暴露给其它应用。你也可以使用permission来限制外部实体唤醒当前service(详情见permission属性)

3. Provider中的:

<provider android:authorities="list"
          android:enabled=["true" | "false"]
          android:exported=["true" | "false"]
          android:grantUriPermissions=["true" | "false"]
          android:icon="drawable resource"
          android:initOrder="integer"
          android:label="string resource"
          android:multiprocess=["true" | "false"]
          android:name="string"
          android:permission="string"
         android:writePermission="string" >
    . . .
</provider>

深入理解  Android 组件的 exported 属性-LMLPHP
意思如下:
当前内容提供者是否会被其它应用使用:
true: 当前提供者可以被其它应用使用。任何应用可以使用Provider通过URI 来获得它,也可以通过相应的权限来使用Provider。
false:当前提供者不能被其它应用使用。设置Android:exported=“false”来限制其它应用获得你应用的Provider。只有拥有同样的user ID 的应用可以获得当前应用的Provider。
当Android sdk 的最小版本为16或者更低时他的默认值是true。如果是17和以上的版本默认值是false。
可以通过Android:exported=“fasle” 和 permission来限制当前应用Provider是否会被其它应用获取。

4. receiver中的:

<receiver android:enabled=["true" | "false"]
          android:exported=["true" | "false"]
          android:icon="drawable resource"
          android:label="string resource"
          android:name="string"
          android:permission="string"
          android:process="string" >
    . . .
</receiver>

深入理解  Android 组件的 exported 属性-LMLPHP意思如下:
当前broadcast Receiver 是否可以从当前应用外部获取Receiver message 。true,可以;false 不可以。如果为false ,当前broadcast Receiver 只能收到同一个应用或者拥有同一 user ID 应用发出广播。

默认值根据当前 broadcast Receiver 是否包含intent filter来定。如果没有任何的filter的话意味着只有在被详细的描述了class name的情况下才会被唤起。这意味着当前Receiver只能在应用内部被使用(因为其它应用不知道这个类的存在。)在这种情况下,默认值是false。如果至少包含一个filter意味着当前broadcast Receiver 将会收到来自系统或者其它应用的广播,所以这个时候默认值是true.

源码分析

那么,Android系统是如何设置exported属性的值呢?通过阅读系统源码,我们知道AndroidManifest.xml是在

frameword\base\core\java\android\content\pm\PackageParse.java

中解析,找到其中解析activity项的源码,在parseActivity函数:

private Activity parseActivity(Package owner, Resources res,
            XmlPullParser parser, AttributeSet attrs, int flags, String[] outError,
            boolean receiver, boolean hardwareAccelerated)

	    ...

        boolean setExported = sa.hasValue(R.styleable.AndroidManifestActivity_exported);
        if (setExported) {
            a.info.exported = sa.getBoolean(R.styleable.AndroidManifestActivity_exported, false);
        }

        ...

        if (!setExported) {
            a.info.exported = a.intents.size() > 0;
        }
        return a;
    }

这里,我们只保留了与exported属性解析相关的源码。从源码中,我们可以得出:

如果显式exported属性,不管这个activity有没有设置intent-filter,那么exported的值就是显式设置的值
如果没有设置exported属性,那么exported属性取决于这个activity是否有intent-filter
如有intent-filter,那么exported的值就是true
如没有intent-filter,那么exported的值就是false

如何合理设置exported

了解了系统如何设定exported值之后,我们接下来谈谈在开发中如何更合理设置exported。
我们知道代码离不开用户场景,那么我把一个Activity被调用的场景分为3种:封闭式、半封闭式和开放式

1. 封闭式
被调用的Activity与调用的Context必须在同一个App,其他任何App不能调用
这种是我们最常见的Activity,有2种情况:

没有intent-filter情况,可以不设置exported或者设置exported为false

<activity
            android:name=".SecondActivity"
            android:label="@string/app_name" />
或
<activity
            android:name=".SecondActivity"
            android:label="@string/app_name"
            android:exported="false"/>

有intent-filter情况,必须设置exported为false

2. 半封闭式

被调用的Activity只能被部分其他App调用,如同一个公司的2个App之间
这种场景下,除了满足封闭式设置外,还必须把调用App和被调用App设置相同的uid,即在2个App的AndroidManifest.xml添加相同的android:sharedUserId,如

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:sharedUserId="com.example.categorytest">

为什么添加相同的android:sharedUserId就可以呢?还是看系统AMS启动Activity的源码,在

framework\base\core\java\com\android\server\am\ActivityStackSupervisor.java

final int startActivityLocked(IApplicationThread caller,
            Intent intent, String resolvedType, ActivityInfo aInfo,
            IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
            IBinder resultTo, String resultWho, int requestCode,
            int callingPid, int callingUid, String callingPackage,
            int realCallingPid, int realCallingUid, int startFlags, Bundle options,
            boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity,
            ActivityContainer container, TaskRecord inTask) {
            ...
	final int startAnyPerm = mService.checkPermission(
	                START_ANY_ACTIVITY, callingPid, callingUid);

	        if (startAnyPerm != PERMISSION_GRANTED) {
	            final int componentRestriction = getComponentRestrictionForCallingPackage(
	                    aInfo, callingPackage, callingPid, callingUid, ignoreTargetSecurity);
	            final int actionRestriction = getActionRestrictionForCallingPackage(
	                    intent.getAction(), callingPackage, callingPid, callingUid);

	            if (componentRestriction == ACTIVITY_RESTRICTION_PERMISSION
	                    || actionRestriction == ACTIVITY_RESTRICTION_PERMISSION) {
	                if (resultRecord != null) {
	                    resultStack.sendActivityResultLocked(-1,
	                            resultRecord, resultWho, requestCode,
	                            Activity.RESULT_CANCELED, null);
	                }
	                String msg;
	                if (actionRestriction == ACTIVITY_RESTRICTION_PERMISSION) {
	                    msg = "Permission Denial: starting " + intent.toString()
	                            + " from " + callerApp + " (pid=" + callingPid
	                            + ", uid=" + callingUid + ")" + " with revoked permission "
	                            + ACTION_TO_RUNTIME_PERMISSION.get(intent.getAction());
	                } else if (!aInfo.exported) {
	                    msg = "Permission Denial: starting " + intent.toString()
	                            + " from " + callerApp + " (pid=" + callingPid
	                            + ", uid=" + callingUid + ")"
	                            + " not exported from uid " + aInfo.applicationInfo.uid;
	                }
...
}

其中就调用mService.checkPermission进行鉴权,mService就是

framework\base\core\java\com\android\server\am\ActivityManagerService.java

checkPermission实际上又调用到
framework\base\core\java\android\app\ActivityManager.java的checkComponentPermission

/** @hide */
    public static int checkComponentPermission(String permission, int uid,
            int owningUid, boolean exported) {
        // Root, system server get to do everything.
        final int appId = UserHandle.getAppId(uid);
        if (appId == Process.ROOT_UID || appId == Process.SYSTEM_UID) {
            return PackageManager.PERMISSION_GRANTED;
        }
        // Isolated processes don't get any permissions.
        if (UserHandle.isIsolated(uid)) {
            return PackageManager.PERMISSION_DENIED;
        }
        // If there is a uid that owns whatever is being accessed, it has
        // blanket access to it regardless of the permissions it requires.
        if (owningUid >= 0 && UserHandle.isSameApp(uid, owningUid)) {
            return PackageManager.PERMISSION_GRANTED;
        }
        // If the target is not exported, then nobody else can get to it.
        if (!exported) {
            /*
            RuntimeException here = new RuntimeException("here");
            here.fillInStackTrace();
            Slog.w(TAG, "Permission denied: checkComponentPermission() owningUid=" + owningUid,
                    here);
            */
            return PackageManager.PERMISSION_DENIED;
        }
        if (permission == null) {
            return PackageManager.PERMISSION_GRANTED;
        }
        try {
            return AppGlobals.getPackageManager()
                    .checkUidPermission(permission, uid);
        } catch (RemoteException e) {
            // Should never happen, but if it does... deny!
            Slog.e(TAG, "PackageManager is dead?!?", e);
        }
        return PackageManager.PERMISSION_DENIED;
    }

从代码中我们知道,设置相同的sharedUserId之后,UserHandle.isSameApp(uid, owningUid)就会返回true,因此尽管我们把exported设为false,也不会抛出Permission Denial的异常。

3. 开放式

可以被任何App调用 这种场景主要是对外接口,如微信、微博的分享接口。大多数情况下,这样的Activity都会有intent-filter,因此也没必要显式地把exported设为true,不设是可以的,当然不能设为false。但如果你没有intent-filter,那就必须显式地把exported设为true。 当然,对于三方app接口的intent-filter设置还有一些要求,如在隐式intent调用必须添加android.intent.category.DEFAULT

07-11 20:13