背景
這么久了升筏,我自己看來(lái)對(duì)此屬性的理解有點(diǎn)小偏差,當(dāng)然不是表面上的理解誤差煤率,而是涉及到具體實(shí)現(xiàn)的細(xì)節(jié)。這里先貼下官方關(guān)于此屬性的解釋:
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).
這段文字說(shuō)明乏冀,值得多讀幾遍5础!辆沦!
由于我們團(tuán)隊(duì)的關(guān)系昼捍,我們開(kāi)發(fā)的模塊經(jīng)常需要集成到多個(gè)app中,而我們不想為某個(gè)app單獨(dú)維護(hù)一份代碼肢扯,即我們的開(kāi)發(fā)中妒茬,所有的宿主app用的都是同一套代碼。比如就存在類(lèi)似這樣的代碼:
<activity
android:name=".SubActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false" // 注意這行代碼N党俊Uё辍!
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里铭腕,通過(guò)打開(kāi)mlpf://sub
這樣的短鏈就能輕松地來(lái)到我們模塊的SubActivity银择。注意這里android:exported=false
的設(shè)置,因?yàn)槿绻辉O(shè)置的話累舷,根據(jù)Android的規(guī)則只要有intent-filter存在浩考,那么exported就是true,即對(duì)外暴露的被盈;而這里我們顯然不希望是對(duì)外暴露的析孽,因?yàn)槿绻@樣的話搭伤,當(dāng)安裝了多個(gè)集成了我們模塊的App時(shí),當(dāng)要打開(kāi)這樣的短鏈請(qǐng)求時(shí)系統(tǒng)就會(huì)彈出選擇框讓用戶選擇在哪個(gè)app里打開(kāi)袜瞬,這當(dāng)然不是我們期望的怜俐。
當(dāng)同一個(gè)設(shè)備上裝了我們的多個(gè)app的時(shí)候,在8.0之前都是ok的吞滞,即A app里的SubActivity和B app里的SubActivity互相是沒(méi)任何關(guān)系的佑菩,也是互相看不到對(duì)方的,這是我們對(duì)exported=false的認(rèn)識(shí)裁赠;直到上周某天晚上快要下班了殿漠,QA同學(xué)拿著升級(jí)到8.0的Nexus 6P跟我說(shuō),你看你們這個(gè)頁(yè)面跳不過(guò)去了佩捞,還彈出了個(gè)討厭的沒(méi)有應(yīng)用可執(zhí)行此操作
的提示绞幌,我當(dāng)時(shí)也是一臉懵逼啊,但心里已經(jīng)有種不祥的預(yù)感一忱,看起來(lái)像是google改出來(lái)的bug莲蜘。
我接過(guò)設(shè)備,點(diǎn)擊了幾下帘营,確保能復(fù)現(xiàn)票渠,然后連著電腦,看了下adb logcat關(guān)于ActivityManager
相關(guān)的輸出芬迄,果然我們這個(gè)intent沒(méi)有找到對(duì)應(yīng)的cmp(component)问顷,而是到了系統(tǒng)的ResolverActivity,ResolverAct大家都知道禀梳,當(dāng)系統(tǒng)找到了多個(gè)目標(biāo)或者沒(méi)目標(biāo)時(shí)會(huì)彈出它提醒用戶杜窄。這就有點(diǎn)奇怪了,同樣的case在7.x的設(shè)備上就是好的算途,雖然行為上也是到了ResolverAct塞耕,但ResolverAct內(nèi)部最終還是導(dǎo)到了本app內(nèi)部的SubActivity,最終正確調(diào)起了嘴瓤。
解惑
有一點(diǎn)我們需要知道扫外,即當(dāng)我們通過(guò)Intent打開(kāi)act的時(shí)候,系統(tǒng)內(nèi)部會(huì)調(diào)用
Intent.resolveActivity(pm)
廓脆,其內(nèi)部又會(huì)接著調(diào)用PackageManager#resolveActivity
畏浆。另外你也可以調(diào)用PackageManager#queryIntentActivities
來(lái)查看某個(gè)intent究竟可以被哪個(gè)act處理。有一點(diǎn)需要特別注意的是狞贱,這些方法會(huì)考察設(shè)備上所有安裝的app里的activity刻获,即使是那些被顯式標(biāo)記成了exported=false的act
,這就是我上文說(shuō)到的理解偏差,這讓我很驚訝蝎毡。因?yàn)槲乙郧暗恼J(rèn)識(shí)中厚柳,既然標(biāo)記了不對(duì)外暴露,那么這些act也不應(yīng)該被找到才對(duì)沐兵,但很可惜别垮,看起來(lái)Android的實(shí)現(xiàn)不是這樣的,關(guān)于這點(diǎn)扎谎,可以參考以下問(wèn)題:Android queryintentactivities.
7.x(包括)之前雖然也能查到別的app里exported=false的act(這個(gè)行為看起來(lái)一直都有)碳想,但最終會(huì)正確打開(kāi)匹配到的本app里exported=false的act,但在8.0上這個(gè)行為break掉了毁靶,直接變成了上文提到的“沒(méi)有應(yīng)用可執(zhí)行此操作”胧奔,真是一個(gè)憂傷的故事。
8.0解決辦法
關(guān)于8.0的這個(gè)問(wèn)題预吆,AOSP上也有人報(bào)了bug:intent有多個(gè)match時(shí)無(wú)法正確跳轉(zhuǎn)龙填。不過(guò)看起來(lái)僅僅是個(gè)沒(méi)多少關(guān)注的P3bug,而且我手頭的5x升級(jí)到了8.1.0拐叉,此問(wèn)題依然存在岩遗,看來(lái)指望google修復(fù)希望不大。還好我們也有辦法處理下凤瘦,看下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;
}
我們面臨的主要問(wèn)題就是系統(tǒng)API在startActivity的過(guò)程中查到了別的app里面的非暴露act,這個(gè)方法看起來(lái)剛好可以將系統(tǒng)的這個(gè)查找行為局限在本app內(nèi)蔬芥,所以我們的fix如下:
if (Build.VERSION.SDK_INT >= 26) {
intent.setPackage(mContext.getPackageName());
}
最后梆靖,關(guān)于exported=false的實(shí)現(xiàn),我個(gè)人的看法是應(yīng)該再提早些,直接一開(kāi)始在匹配的過(guò)程中就找不到這樣的act,而不是一股腦全找到(導(dǎo)致本來(lái)就1個(gè)target滿足生蚁,結(jié)果找了多個(gè)出來(lái))档悠,等到最后要打開(kāi)了,看下exported是false壁酬,才彈個(gè)無(wú)權(quán)限的錯(cuò)誤4巫谩!舆乔!之前魅族更新了次系統(tǒng)后也出過(guò)這問(wèn)題岳服,彈出讓用戶選,結(jié)果選了之后又告訴用戶無(wú)權(quán)限(因?yàn)閷?shí)際是exported=false的activity)希俩。就像在實(shí)現(xiàn)某個(gè)方法的時(shí)候吊宋,有些前置條件不滿足,我們應(yīng)該盡早return颜武,而不是埋頭做了很多工作后璃搜,才檢查一些必要條件拖吼,發(fā)現(xiàn)不對(duì)了才退出,fail fast常常是很好用的策略这吻。
ps:實(shí)在是沒(méi)明白google這里的實(shí)現(xiàn)為啥要找到這些實(shí)際上private的act吊档,看起來(lái)完全是在做無(wú)用功啊,反正怎么著都不可能打開(kāi)唾糯,你把它找出來(lái)干啥呢5∨稹!移怯!有想法的同學(xué)可以留言交流下香璃,謝謝。