结合源码谈谈Activity的exported属性

本文深入探讨了Android中Activity的exported属性的作用及其设定方式,并根据不同的应用场景提供了合理的设置建议。

Activity的exported属性在单个App可能用得比较少,但对于对外接口的Activity或公司内部多个应用间接口调用的设计会有比较大的影响。本文基于android 6.0.1的源码谈谈Activity的exported属性,内容分为2部分:

  • 系统如何设定exported值
  • 如何合理设置exported

系统如何设定exported值

我们知道在AndroidManifest.xml文件中,四大组件都有android:exported属性,是个boolean值,可以为true或false
如:

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

我们这里只讲activity相关,但对其他组件也有参考意义,大家可以去研究。
android:exported是个可选属性,它不像android:name是必须设置的。那么,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种:封闭式、半封闭式和开放式

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

    1. 没有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"/>
     2. 有intent-filter情况,必须设置exported为false
<activity
            android:name=".SecondActivity"
            android:label="@string/app_name" 
            android:exported="false">
            <intent-filter>
            ...
  • 半封闭式:被调用的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的源码,在

frameword\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的异常。

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

的category。这个我会在新的文章中介绍。

<think> 我们正在讨论AndroidActivityexported属性。根据之前的引用,我们可以总结如下: 1. **exported属性的定义**: exported属性Android四大组件(Activity, Service, BroadcastReceiver, ContentProvider)共有的一个属性,用于控制该组件是否允许被其他应用程序调用(即是否暴露给外部应用)。 2. **默认值规则**: - 如果组件包含`<intent-filter>`,则exported默认值为`true`。 - 如果组件没有`<intent-filter>`,则exported默认值为`false`。 3. **功能作用**: - 当exported="true"时: - 该组件可以被其他应用程序的组件启动(通过显式Intent或隐式Intent匹配)。 - 对于没有intent-filter的Activity,必须使用显式Intent(需要指定包名和类名)才能启动,且此时exported必须为true,否则外部无法启动。 - 当exported="false"时: - 该组件只能被同一个应用程序内的组件启动,或者被具有相同用户ID(UID)的应用程序启动(通常指同一个开发者签名且共享UID的应用)。 - **重要例外**:系统组件(如Launcher)无法启动exported="false"的Activity(这就是用户最初问题中"Default Activity not found"的原因之一)。 4. **安全影响**: - 设置exported="false"可以避免组件被恶意应用调用,提升安全性。 - 对于不需要被外部调用的组件,建议显式设置exported="false"(即使有intent-filter,如果不需要外部调用也应设为false)。 5. **特殊注意点**(根据引用[4]): - 即使Activity设置了exported="false",通过`PackageManager#queryIntentActivities`或`Intent.resolveActivity()`仍然可能查询到该Activity的存在(因为系统会考察所有已安装应用的Activity)。但是,尝试启动它将会失败(除非满足上述同应用或同UID的条件)。 - 因此,exported="false"的真正作用是限制启动,而非完全隐藏组件。 ### 为什么会出现"Default Activity not found"? 在用户最初的问题中,Activity配置如下: ```xml <activity android:name=".MainActivity" android:exported="false"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.category.LAUNCHER"/> <!-- 错误:应为category --> </intent-filter> </activity> ``` 这里有两个问题: 1. **intent-filter错误**:将`android.intent.category.LAUNCHER`错误写成了`<action>`标签(应使用`<category>`)。 2. **exported="false"**:即使修正了intent-filter,exported="false"也会导致Launcher(系统组件)无法启动该Activity,从而报错。 **解决方案**: - 将`<action android:name="android.intent.category.LAUNCHER"/>`改为`<category android:name="android.intent.category.LAUNCHER"/>`。 - 移除`android:exported="false"`或设为`true`(因为Launcher是外部应用,必须exported="true"才能被启动)。 ### 总结exported属性的关键点: - **显式Intent调用**:即使Activity没有intent-filter且exported="true",其他应用也可以通过显式Intent(指定包名和类名)启动它[^5]。 - **隐式Intent调用**:需要同时满足: - 组件有匹配的intent-filter。 - exported="true"(默认有intent-filter时即为true)。 - **Launcher要求**:作为应用入口的Activity必须同时满足: - 包含`<action android:name="android.intent.action.MAIN"/>`和`<category android:name="android.intent.category.LAUNCHER"/>`。 - exported="true"(显式设置或依赖默认值)。 §§相关问题§§ 1. 如何安全地暴露一个Activity给其他应用使用? 2. Android中如何防止Activity被未授权应用启动? 3. 在哪些场景下需要设置Activityexported属性为false? </think> ### Android Activityexported 属性的含义详解 `android:exported` 是 Android 四大组件(Activity、Service、BroadcastReceiver、ContentProvider)共有的关键安全属性,专门控制组件是否允许被**外部应用**访问。其核心规则如下: #### 1. **基本定义** - ✅ **`exported="true"`**:允许其他应用调用当前 Activity (例如:通过隐式 Intent 启动) - ❌ **`exported="false"`**:仅允许**同应用内组件**或**相同用户 ID 的应用**调用 (系统级应用如 Launcher 无法启动) #### 2. **默认值规则** | 组件配置 | 默认值 | 说明 | |----------|--------|------| | 包含 `<intent-filter>` | `true` | 系统认为该组件需响应外部请求 [^3] | | **无** `<intent-filter>` | `false` | 组件默认为私有状态 [^5] | #### 3. **安全影响** - **隐式 Intent 启动**: 外部应用通过 `Intent.ACTION_VIEW` 等隐式 Intent 启动 Activity 时,必须满足: ```java exported="true" AND 匹配 intent-filter ``` - **显式 Intent 启动**: 即使 `exported="false"`,**同应用内**仍可通过类名直接启动: ```java Intent intent = new Intent(this, PrivateActivity.class); startActivity(intent); ``` #### 4. **特殊注意点** 1. **Launcher 启动要求**: 应用入口 Activity **必须**设置为: ```xml exported="true" + <category android:name="android.intent.category.LAUNCHER"/> ``` 否则会出现 `Default Activity not found` 错误 [^1] 2. **安全漏洞风险**: 错误设置 `exported="true"` 可能导致: - 未授权数据访问 - 恶意功能调用 - 拒绝服务攻击(DoS) 3. **查询可见性**: 即使 `exported="false"`,通过 `PackageManager.queryIntentActivities()` 仍可检测到该 Activity 存在(但不能启动)[^4] #### 5. **最佳实践建议** ```xml <!-- 安全暴露给外部应用的组件 --> <activity android:name=".ShareActivity" android:exported="true"> <!-- 显式声明更安全 --> <intent-filter> <action android:name="android.intent.action.SEND"/> </intent-filter> </activity> <!-- 私有组件(禁止外部访问) --> <activity android:name=".PrivateActivity" android:exported="false"/> <!-- 即使有intent-filter也应显式设置 --> ``` > 关键总结:**`exported` 属性本质是组件的访问控制门禁** —— 它决定了"谁能从外部敲门",而 `intent-filter` 决定了"如何识别敲门声" [^2][^3]。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值