查看原文
其他

我以为发现了Android 14系统中的一个bug,然而...

郭霖 郭霖 2024-01-12


今天来跟大家探讨一个Android 14很细节的知识点。


事情的起因是这样的,某天工作群里,我看到我们部门的同事guting发了这样一条消息。



我看到这条消息之后的第一感觉就是,貌似和我印象中Android 14的行为并不一致。


因为没有任何错误日志可以观察到这种现象是不应该的,我印象中用法不正确的话是会直接导致应用程序崩溃。


但其实我自己也记不太清楚了,我写Android 14新特性的文章已经是去年3月份发布的了。于是我还特意找到了 Android 14 Developer Preview一览 这篇文章重新又学习了一遍。


为了这篇文章大家能够看得明白,所以我把当时写的Android 14在涉及隐式Intent限制变动的部分摘抄出来,跟大家再快速过一遍。


首先这项改动只针对targetSdkVersion指定到34(Android 14)及以上的App才生效。


我们先来看一下如下代码:

<activity
    android:name=".PickPhotoActivity"
    android:exported="false">

    <intent-filter>
        <action android:name="com.example.action.PICK_PHOTO" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

这里定义了一个PickPhotoActivity,用于选择用户需要的照片。注意这个Activity的exported属性是false,说明它是仅供内部使用的。


那么如果我们想要调用这个Activity来选择照片,可以怎么写呢?由于它能够响应com.example.action.PICK_PHOTO这个自定义action,很容易就能写出如下代码:

val intent = Intent("com.example.action.PICK_PHOTO")
context.startActivity(intent)

这是用隐式Intent来启动Activity的写法。这段代码确实可以正常工作,但是大家有没有想过一个问题,假如现在你的手机上有另外一个App,它的AndroidManifest.xml里是这么写的:

<activity
    android:name=".HookPickPhotoActivity"
    android:exported="true">

    <intent-filter>
        <action android:name="com.example.action.PICK_PHOTO" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

可以看到,它也能够响应com.example.action.PICK_PHOTO这个action,并且它的exported属性是true,说明它是允许外部调用的。


那么此时你还使用上述的代码来选择照片,启动的到底是谁的Activity?


这种情况下,系统也不知道你到底想要启动谁,所以就只能弹出一个对话框,让用户自己去选择。


由此可以看出,恶意软件在这种场景下是有空子可以钻的,因为必然会有用户选择错误。


那么为了解决这方面的安全隐患,Android 14对隐式Intent的使用做出了更多的限制。


当你的targetSdkVersion指定到了34及以上,再使用上述代码去启动Activity,系统就会抛出异常。


这是因为,PickPhotoActivity的exported属性是false,说明它是仅供内部使用的。仅供内部使用的组件,在用隐式Intent启动它的时候一定要给Intent指定当前App的包名,转换成显式Intent才行,如下所示:

val intent = Intent("com.example.action.PICK_PHOTO")
intent.package = context.packageName
context.startActivity(intent)

这样的话,对于系统来说就不会存在二义性,它会非常明确地知道要去启动哪一个Activity,从而进一步提升了安全性。


相信看完这段讲解之后,大家已经能理解Android 14在限制隐式Intent方面的变动了。


唯一的问题就是,我所使用的上述示例,在没有明确指定当前App包名的情况会崩溃,而我的同事guting却反馈说是没有任何错误日志可以观察到。


我又去Android的官方文档上面做了二次核对,官方文档里也有明确提到,用错的情况下是会抛出异常的。



所以问题到底出在哪里呢?


我和guting做了线下沟通,并且看了看他所写的代码。


代码没看出任何毛病,但是和我上述代码示例中不同的地方在于,我用Intent触发的行为是startActivity,而他用Intent触发的行为是sendBroadcast。


难道是在Android 14上Activity和BroadcastReceiver的行为会有不一致?我们又去查看了一遍官方文档:



文档里确实没提到过Activity或BroadcastReceiver的字眼,它用的是components。既然是components,那么就应该包含Activity、Service、BroadcastReceiver和ContentProvider的。所以刚才的猜想不成立。


后来我们又尝试了一下使用隐式Intent启动Serivce,在不指定包名的情况下也会崩溃。只有发送广播时不会崩溃,且这条广播是收不到的,相当于广播莫名其妙丢失了。


所以我又做了另外一个猜想,或许这是触发了广播某些其他的特殊规则,而和Android 14的这项新特性并无关系。


我去翻了翻《第一行代码 第3版》中对广播这部分的解释,里面确实有提到,从Android 8系统开始,静态注册的BroadcastReceiver,如果想要接收得到广播消息,Intent中必须明确指定App的包名才行。


但是我和guting检查了一下BroadcastReceiver的写法,使用的是动态注册的方式,所以和上述这条规则又不相符。


那么是不是这项规则在什么系统版本下又延展到动态注册上面了呢?我没有查阅到任何相关的资料。


最后,我尝试把targetSdkVersion设置成33,发现即使不指定App包名,广播消息也能收到。只要设置成了34,不指定App包名广播就会丢失,且没有任何错误出现。所以这个问题一定是和Android 14的新特性有关了。


我翻遍了Android 14全部的行为变更,只有限制隐式Intent这项能够勉强匹配得上,但BroadcastReceiver不同于Activity和Service的行为又让我感觉无法解释。


百思不得其解的我只好开始尝试把锅往Google身上甩了,我在想着要么这就是Android 14系统中的一个bug,要么就是Android官方文档没写清楚,把BroadcastReceiver这种特殊情况漏写了。


我跟guting说,我再花点时间研究一下,要是实在整不明白我就去给Google提bug。


结果这一研究,还真让我发现了真实的问题所在。


现在我们已经知道,App target到Android 14之后,隐式Intent启动内部Activity和Serivce是会崩溃的。


但是这个崩溃的日志是什么,我却从来没有仔细观察过。


我本来以为应该是什么Security Exception之类的错误,提醒我们当前的代码是有安全问题的。


结果并不是,崩溃的原因是ActivityNotFoundException: No Activity found to handle Intent。



这个崩溃原因让我豁然开朗。


所以这里并不是因为代码的写法不够安全从而系统抛出了一个安全异常,而是纯粹地系统找不到一个Activity能够处理我们发起的这个Intent。


那么这里考一下大家Android这三大组件在无法处理发起Intent的情况下,各自的行为是什么?


如果没有任何一个Activity能够处理Intent启动Activity的请求,App会崩溃。


如果没有任何一个Service能够处理Intent启动Service的请求,App会崩溃。


如果没有任何一个BroadcastReceiver能够接收到Intent发送出来的广播,什么都不会发生。


想想这是不是我们所熟知的三大组件原有的默认行为,长期以来一直都是如此,只是这个问题套了个Android 14的壳子,让我一度迷失在了Android各系统版本行为变更的细节里面,以至于没能快速找出问题的本质。


所以现在我也不着急去给Google提bug了,我又再次仔细阅读了一下Android官方文档上面的说明:



重点都在第一句话上了,隐式Intent只会发送给外部组件,内部组件压根无法接收到隐式Intent。那么对应到Activity、Service和BroadcastReceiver上的行为当然就是崩溃、崩溃和丢失了。


最终证明,Android官方文档的严谨性确实是滴水不漏,还是我自己太稚嫩了。


最后讲一个小插曲。


前段时间去上海参加Devfest的时候碰到了Google的AI技术推广工程师魏巍老师,最近一年全球范围内AI实在是太火了,而魏老师也是这个领域的专家。


吃饭时候跟魏老师闲聊,他提到自己在Google之前也是做过Android的,后来又转去做了AI。


魏老师跟我开玩笑说,自己做Android的时候觉得Android实在是太难了,各个系统版本的变化新特性什么的绕来绕去,根本记不住,所以才去做了AI,说AI比较简单。我听后笑了笑。


今天我再次苦笑一声。


推荐阅读:
我的新书,《第一行代码 第3版》已出版!
写给初学者的Jetpack Compose教程,为什么学习Compose?
2023年终总结,这个冬天你过得还好吗?

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注
继续滑动看下一个

我以为发现了Android 14系统中的一个bug,然而...

郭霖 郭霖

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存