背景
這么久了,我自己看來對此屬性的理解有點小偏差,當然不是表面上的理解誤差,而是涉及到具體實現的細節。這里先貼下官方關于此屬性的解釋:
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,看起來完全是在做無用功啊,反正怎么著都不可能打開,你把它找出來干啥呢!!!有想法的同學可以留言交流下,謝謝。