大家好,又见面了,我是你们的朋友全栈君。
本文档基于谷歌Android 11 Developer Preview 4(DP4)版本的变更输出
一、兼容性调试工具
Android 11 引入了新的工具,用于针对最新版平台中的行为变更来测试和调试应用。这些工具属于新的兼容性框架的一部分,可让应用开发者单独开启和关闭各项变更。有了这种灵活性,您可以关闭单项变更,然后继续针对平台中的其他变更测试应用;也可以每次单独针对一项行为变更测试应用。
不管是影响所有应用的行为变更还是只影响以 Android 11 为目标平台的应用的行为变更,您都可以随意开启或关闭。
您可以使用开发者选项、logcat 或 ADB 命令来查看当前已启用的行为变更。具体使用方法参考: https://developer.android.google.cn/preview/test-changes
使用过程中需要注意的点:
1 对于每项变更,每个进程最多只会记录一次。为确保看到所有相关的 logcat 消息,请强行停止应用进程,然后再重启该进程。
2 每次您使用开发者选项或 ADB 命令为应用开启或关闭变更时,应用都会终止,以确保您的替换操作立即生效。
3 切换变更的开关限制
android:debuggable: 如果可以调试,则设为 “true”;如果无法调试,则设为 “false”。默认值为 “false”。
一、隐私更新
2.1 存储 2.1.1 分区存储 1.1. 背景
Android 11 进一步增强了平台功能,为外部存储设备上的应用和用户数据提供了更好的保护。作为这项工作的一部分,平台引入了进一步的改进,以简化向分区存储的转换。 为了让用户更好地控制自己的文件,保护用户隐私数据,并限制文件混乱情况,Android 11在分区存储基础上限制了应用访问其他应用的文件。
分区存储将存储空间分为两部分:
● 公共目录:Downloads、Documents、Pictures 、DCIM、Movies、Music、Ringtones等
■ 公共目录的文件在App卸载后,不会删除 ■ 可以通过SAF、MediaStore接口访问 ■ 拥有权限,也能通过路径直接访问 ● 应用专属目录 ■ 应用专属目录只能自己直接访问 ■ App卸载,数据会清除。
1.2. 兼容影响
当您将应用更新为以 Android 11 为目标平台后,您将无法使用requestLegacyExternalStorage,而且也没有其他标记可以提供停用分区存储。 分区存储对于App访问存储方式、App数据存放以及App间数据共享,都产生很大影响。 而Environment.getExternalStorageDirectory() 在 API Level 29 开始已被弃用,开发者应迁移至 Context#getExternalFilesDir(String), MediaStore, 或Intent#ACTION_OPEN_DOCUMENT。
1.3. 适配
1 应用targetSdkVersion 应用targetSdkVersion >= 30,都会强制打开分区存储,同时requestLegacyExternalStorage将会无效。 如果您需要对已安装的应用进行适配分区存储的数据迁移,则可以在应用更新到目标平台为Android 11版本后仍暂时保留原有的存储模式。请在应用的manifest中设置preserveLegacyExternalStorage属性为true,应用更新到android 11可以保留存储继承模式。
2 应用私有目录访问 对于运行在Android 11的应用,无论targetSdkVersion是什么都无法访问Emulated存储中的其他应用私有目录(Android/data)。SAF(Storage Access Framework)同样也禁止访问应用私有目录。 某些应用的核心用例需要访问大量的文件,如文件管理操作或备份和恢复操作。这些应用可通过执行以下操作获取“所有文件访问权限”:
● 声明 MANAGE_EXTERNAL_STORAGE 权限。 ● 使用 ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION intent 操作将用户引导至一个系统设置页面,在该页面上,用户可以为您的应用启用以下选项:授予所有文件的管理权限。
● 注意:获得此权限的应用仍然无法访问属于其他应用的应用专用目录。这些目录在存储卷上显示为 Android/data/ 的子目录。
3 直接路径访问 注意:使用直接路径和原生库保存媒体文件时,应用的性能会略有下降。请尽可能改用MediaStore API。
具体适配参考: https://developer.android.google.cn/training/data-storage#scoped-storage https://developer.android.google.cn/preview/privacy/storage 1.3.1. 运行模式 1.3.1.1. App运行模式 在Android 11版本上,系统会根据App targetSdkVersion决定运行模式: ● App targetSdkVersion >= 30,默认为分区存储,并且无法取消。
● App targetSdkVersion < 29,默认为分区存储,可通过requestLegacyExternalStorage更改
应用可以通过AndroidManifest.xml设置requestLegacyExternalStorage, 选择对应的方式:
● App targetSdkVersion < 29,声明了READ_EXTERNAL_STORAGE,默认Legacy Mode ● App在下列条件都成立时 ■ 声明 MANAGE_EXTERNAL_STORAGE 权限。 ■ 使用 ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION intent 操作将用户引导至一个系统设置页面,在该页面上,用户可以为您的应用启用以下选项:授予所有文件的管理权限。 App拥有外置存储空间Read、Write权限。但是通过Environment.isExternalStorageLegacy接口判断,返回不一定是Legacy Mode。
1.3.1.2. 判断当前App运行模式 判断当前App运行什么模式,可以通过这个API判断: Environment.isExternalStorageLegacy() (added in api 29);
1.3.2. 读写公共目录 App启动分区存储后,只能直接访问自身专属目录,所以Android 11,提供了两种访问公共目录的方法(特殊直接路径访问参考1.3.8. 直接路径访问):
1.3.2.1. 通过MediaStore定义的Uri MediaStore提供了下列几种类型的访问Uri,通过查找对应Uri数据,达到访问的目的。 下列每种类型又分为三种Uri,Internal、External、可移动存储:
●Audio ■ Internal: MediaStore.Audio.Media.INTERNAL_CONTENT_URI
content://media/internal/audio/media。
■ External: MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
content://media/external/audio/media。
■ 可移动存储: MediaStore.Audio.Media.getContentUri
content://media/<volumeName>/audio/media。 ● Video ■ Internal: MediaStore.Video.Media.INTERNAL_CONTENT_URI content://media/internal/video/media。 ■ External: MediaStore.Video.Media.EXTERNAL_CONTENT_URI content://media/external/video/media。 ■ 可移动存储: MediaStore.Video.Media.getContentUri content://media/<volumeName>/video/media。 ● Image ■ Internal: MediaStore.Images.Media.INTERNAL_CONTENT_URI content://media/internal/images/media。 ■ External: MediaStore.Images.Media.EXTERNAL_CONTENT_URI content://media/external/images/media。 ■ 可移动存储: MediaStore.Images.Media.getContentUri content://media/<volumeName>/images/media。 ● File ■ MediaStore. Files.Media.getContentUri content://media/<volumeName>/file。 ● Downloads ■ Internal: MediaStore.Downloads.INTERNAL_CONTENT_URI content://media/internal/downloads。 ■ External: MediaStore.Downloads.EXTERNAL_CONTENT_URI content://media/external/downloads。 ■ 可移动存储: MediaStore.Downloads.getContentUri content://media/<volumeName>/downloads。
1.3.2.1.1. 获取所有的Volume 对于前面描述的Uri中,getContentUri如何获取所有<volumeName>,可以通过下述方式:
1.3.2.1.2.Uri跟公共目录关系 MediaProvider对于App存放到公共目录文件,通过ContentResolver insert方法中Uri来确定,其中下表中<Uri路径>为相对路径,完整为: content://media/<volumeName>/<Uri路径>。
Mine Type | Uri路径 | 一级目录 |
---|---|---|
audio/* | images/media images/media/# | Environment.DIRECTORY_ALARMS |
Environment.DIRECTORY_MUSIC | ||
Environment.DIRECTORY_NOTIFICATIONS | ||
Environment.DIRECTORY_PODCASTS | ||
Environment.DIRECTORY_RINGTONES | ||
image/* | audio/albumart audio/albumart/# | Environment.DIRECTORY_MUSIC |
audio/playlists audio/playlists/# | Environment.DIRECTORY_MUSIC | |
video/* | video/media video/media/# | Environment.DIRECTORY_DCIM |
Environment.DIRECTORY_MOVIES | ||
image/* | images/media images/media/# | Environment.DIRECTORY_DCIM |
Environment.DIRECTORY_PICTURES | ||
image/* | video/thumbnails video/thumbnails/# | Environment.DIRECTORY_MOVIES |
image/* | images/thumbnails images/thumbnails/# | Environment.DIRECTORY_PICTURES |
downloads downloads/# | Environment.DIRECTORY_DOWNLOADS | |
file file/# | Environment.DIRECTORY_DOWNLOADS | |
Environment.DIRECTORY_DOCUMENTS |
通过ContentResolver,根据不同的Uri查询不同的内容:1.3.2.1.4. 查询文件1.3.2.1.3. 权限 MediaStore通过不同Uri,为用户提供了增、删、改。 App对应的权限如下:
Audio | Image | Video | File | Downloads | |
---|---|---|---|---|---|
WRITE_EXTERNAL_STORAGE | no-op | ||||
READ_EXTERNAL_STORAGE | 能读取所有App的多媒体文件 | 不能读取非多媒体文件 | |||
无 | 只能读取、修改自己新建的文件 |
1.3.2.1.4. 查询文件
通过ContentResolver,根据不同的Uri查询不同的内容:
1.3.2.1.5.读取文件 通过ContentResolver query接口,查找出来文件后如何读取,可以通过下面的方式: ● 通过ContentResolver openFileDescriptor接口,选择对应的打开方式
例如”r”表示读,”w”表示写,返回ParcelFileDescriptor类型FD。
● 访问Thumbnail,通过ContentResolver loadThumbnail接口
通过传递大小,MediaProvider返回指定大小的Thumbnail。 ● Native代码访问文件 如果Native代码需要访问文件,可以参考下面方式:
■ 通过openFileDescriptor返回ParcelFileDescriptor
■ 通过ParcelFileDescriptor.detachFd()读取FD ■ 将FD传递给Native层代码 ■ App需要负责通过close接口关闭FD
1.3.2.1.6.新建文件 如果需要新建文件存放到公共目录,需要通过ContentResolver insert接口,使用不同的Uri,选择存储到不同的目录。
1.3.2.1.7.修改文件
如果需要修改多媒体文件,需要通过ContentResolver query接口查找出来对应文件的Uri。 如果不是自己新建的文件,需要注意1.3.2.1.3. 权限中描述,需要catch RecoverableSecurityException,弹框给用户选择。
通过下列接口,获取需要修改文件的FD或者OutputStream: ● getContentResolver().openOutputStream(contentUri) 获取对应文件的OutputStream。 ● getContentResolver().openFile或者getContentResolver().openFileDescriptor 通过openFile或者openFileDescriptor打开文件,需要选择Mode为”w”,表示写权限。这些接口返回一个ParcelFileDescriptor。 getContentResolver().openFileDescriptor(contentUri,”w”); getContentResolver().openFile(contentUri,”w”,null);
1.3.2.1.8.删除文件 通过ContentResolver接口删除文件,Uri为query出来的Uri: getContentResolver().delete(contentUri,null,null);
1.3.2.1.9.文件批量申请
Android 11提供了批量文件授权申请:
● createWriteRequest
● createFavoriteRequest
● createTrashRequest
● createDeleteRequest
具体可以参考https://developer.android.google.cn/reference/android/provider/MediaStore#createDeleteRequest(android.content.ContentResolver, java.util.Collection
1.3.2.2.通过SAF接口 SAF,即Storage Access Framework,通过选择不同的DocumentsProvider,提供给用户打开、浏览文件。
Android默认提供了下列DocumentsProvider: MediaDocumentsProvider、ExternalStorageProvider、 DownloadStorageProvider。 他们之间差异是:
MediaDocumentsProvider | ExternalStorageProvider | DownloadStorageProvider | |
---|---|---|---|
读 | 只能读取视频、音频、图片 | 全部内置、外置存储 | 读取Download目录 |
删除 | 可以删除 | ||
修改 | 无法修改 | 可以修改 |
这个图片上,有三个区域,分别是: ● MediaDocumentsProvide,DownloadStorageProvider ● ExternalStorageProvider ● 第三方DocumentsProvider 如何使用,具体参考: https://developer.android.google.cn/guide/topics/providers/document-provider 大致方法如下: ● 选择单个文件
● 选择目录
文件管理程序,清理程序,可以通过这个方法获取对应目录以及子目录的全部管理权限。 ● 新建文件
● 删除 DocumentsContract.deleteDocument(getContentResolver(),uri); ● 修改 ■ 获取OutputStream getContentResolver().openOutputStream(uri); ■ 获取可写ParcelFileDescriptor getContentResolver().openFileDescriptor(contentUri,”w”); getContentResolver().openFile (contentUri,”w”,null); 具体Demo参考:https://github.com/android/storage
在Android 11上,无法通过SAF选择External Storage根目录、Downloads目录以及App专属目录(Android/data、Android/obb)。
1.3.3.访问应用的专属目录 访问应用专属目录分为两种情况,第一是访问App自身专属目录,第二是访问其他App的专属目录。
1.3.3.1.App自身专属目录 Android 11获取应用专属目录 ■ 获取Media接口:getExternalMediaDirs ■ 获取Cache接口:getExternalCacheDirs ■ 获取Obb接口:getObbDirs ■ 获取Data接口:getExternalFilesDirs 应用专属目录App本身可以直接访问。
1.3.3.2.其他App的专属目录 Android 11,App无法访问其他App的专属目录(Android/data)。如果需要访问其他应用专属目录数据,需要被访问者按照下列方法来提供: 1.3.3.2.1.通过SAF文件 ● 共享App自定义DocumentsProvider App自定义DocumentsProvider需要做以下步骤: a)指定DocumentsProvider
b)DocumentsProvider实现基本接口:
● 访问App通过ACTION_OPEN_DOCUMENT,启动浏览
1.3.3.2.2.共享App实现FileProvider
FileProvider具体使用参考: https://developer.android.google.cn/training/secure-file-sharing/setup-sharing 这边总结一下大概步骤: ● 指定App FileProvider
● 指定文件路径,配置文件必须要放到res/xml中
● 获取分享Uri
● 设置权限,并且发送Uri
● 接收App,设置接受的inter-filter
● 接收并处理Uri
1.3.3.2.3.App自定义私有Provider App可以实现自定义ContentProvider,尤其是内部文件共享,但是不希望UI交互。
1.3.4.MediaStore文件Pending状态
MediaStore中添加了一个IS_PENDING Flag,用于标记当前文件时Pending状态。其他App通过MediaStore查询文件,如果没有设置setIncludePending接口,查询不到设置为Pending状态的文件,这就给App专享访问此文件。在一些情况下使用,例如在下载的时候:下载中,文件是Pending状态→下载完成,文件Pending状态置为0。
1.3.5. MediaColumns.RELATIVE_PATH设置存储路径 Android Q上,通过MediaStore存储到公共目录的文件,除了1.3.2.1.2节Uri跟公共目录关系中规定的每一个存储空间的一级目录外,可以通过MediaColumns.RELATIVE_PATH来指定存储的次级目录,这个目录可以使多级,具体代码如下: ● ContentResolver insert方法 通过values.put(Media.RELATIVE_PATH,”Pictures/album/family “)指定存储目录。其中,Pictures是一级目录,album/family是子目录。 ● ContentResolver update方法 通过values.put(Media.RELATIVE_PATH,”Pictures/album/family “)指定存储目录。通过update方法,可以移动存储地方。
1.3.6.访问图片Exif Metadata Android Q上, App如果需要访问图片上的Exif Metadata,需要做下列事情: ● 申请ACCESS_MEDIA_LOCATION权限 ● 通过MediaStore.setRequireOriginal返回新Uri Demo Code如下:
1.3.7.App Scopted Storage,访问权限总结
App访问不同目录的权限总结如下:
文件位置 | 需要权限 | 访问方式 | App卸载是否保存 |
---|---|---|---|
应用专属目录 | 无 | getExternalFilesDir() | 不保留 |
Media文件(photos, videos, audio) | 访问其他app文件,需要READ_EXTERNAL_STORAGE修改其他App,需要弹框用户确认 | MediaStore | 保留 |
Downloads | 无 | SAF | 保留 |
1.3.9.宽泛权限 1.3.8.直接路径访问 Android 11上,App可以直接通过路径访问拥有权限的文件。例如,可以通过路径访问自己通过MediaStore新建的Images。
因为现在分区存储公共区域,是基于FUSE来实现,通过直接路径访问会经过下列路程:访问者 → FUSE → KERNEL—->MediaProvider(得到真实数据)—>KERNEL → FUSE→访问者,比之前SDCARDFS多了几个步骤,所以会导致一些性能问题。建议通过MediaStore访问。
Android 11,提供了两种宽泛权限,需要注意的是这两种宽泛权限是无法访问其他应用的专属目录: ● MANAGE_EXTERNAL_STORAGE App拥有此权限,能够读写公共区域内所有文件,并且可以访问MediaStore.Files里面的所有文件。此权限能够满足清理、手机搬家、杀毒、文件管理这些类型应用需求。 App可以通过下列方式申请:
● System Gallery Role Gallery Role只能是预装的系统应用,通过系统配置才能成为Gallery Role。拥有Gallery Role,通过MediaStore读写多媒体文件不用弹框用户交互。
1.3.10.应用卸载 ● 如果App在AndroidManifest.xml中声明:android:hasFragileUserData=”true” 卸载应用会有提示是否保留App数据:
● App存放到公共目录下的文件,卸载后,如果需要修改,需要用户重新授予权限
1.3.11.App数据迁移 App打开分区存储,会涉及到数据的迁移,不然会导致旧数据无法使用。可以从下面几方面着手数据迁移: ● App对于可以存放到公共目录的文件,可以通过MediaStore接口存放到对应类型的公共目录中。 ● 对于私有数据,可以存放到App私有目录。 ● 迁移后数据的共享访问 ■ 对于存放到公共目录的文件,其他App可以通过MediaStore访问。 ■ 对于无法存放在公共目录文件,可以放置在私有目录,通过Uri共享给其他App访问。
1.3.12.MediaStore Queries 在使用MediaStore进行query动作的时候,使用Projection时,Column Name要在MediaStore中定义好的。
1.3.13.新建测试使用可移动存储 如果一个设备没有可移动的存储,可以使用下面的方法新建虚拟存储设备: ● adb shell sm set-virtual-disk true ● 在设置 -> 存储 -> Virtual SD,进行初始化
1.4.规范愿景
我们希望三方应用,尤其是TOP应用,能够按照分区存储的规范,将用户数据(例如图片、视频、音频等)保存在公共目录,把应用数据保存在SDCARD私有目录,以更好地保护外部存储上的应用和用户数据。而Google正在更新 Google Play 政策,以确保应用只在其真正需要获取位置信息时才请求授权。
2.1.2应用缓存
1 背景 在Android 11上,应用默认不能删除其他应用的缓存文件,即使申请了MANAGE_EXTERNAL_STORAGE权限。 Google官网特性介绍: https://developer.android.google.cn/preview/privacy/storage#manage-device-storage 2 兼容性影响 文件管理类,清理类或其他具有缓存清理功能应用,清除其他应用缓存功能失效。 3 适配指导 1 使用intent action – ACTION_MANAGE_STORAGE 检查可用存储空间大小。 2 如果可用的存储空间不足,使用 intent action —ACTION_CLEAR_APP_CACHE 呈现UI界面让用户确认后,触发所有应用的缓存清理。 注意:执行 ACTION_CLEAR_APP_CACHE 触发的缓存清理,会清理所有应用的缓存,同时大量的IO操作也会加剧电量消耗,如非必要,请不要使用。
2.1.3文件访问限制 1 背景 如果您的应用以 Android 11 为目标平台并使用存储访问框架 (SAF),则您无法再使用ACTION_OPEN_DOCUMENT和ACTION_OPEN_DOCUMENT_TREE操作访问某些目录,具体限制如下: 1 访问目录 您无法再使用ACTION_OPEN_DOCUMENT_TREE 操作来请求访问以下目录: Downloads根目录。 设备制造商认为可靠的各个 SD 卡根目录,无论该卡是模拟卡还是可移除的卡。 内部存储根目录 2 访问文件 您无法再使用 ACTION_OPEN_DOCUMENT_TREE 或 ACTION_OPEN_DOCUMENT操作来请求用户从以下目录中选择单独的文件: Android/data/ 目录及其所有子目录。 Android/obb/ 目录及其所有子目录。 2 兼容性影响 如果应用指定AndroidR为运行平台,则不再能使用SAF访问上述指出的目录,可能导致您的业务逻辑异常。 3 适配指导 执行以下操作来确认行为变更是否已对应用生效: 1 将targetSdkVersion指向Android 11 2 确保已经打开RESTRICT_STORAGE_ACCESS_FRAMEWORK 兼容性开关(使用方法见文档兼容性调试工具部分) 。 3 使用 intent action – ACTION_OPEN_DOCUMENT_TREE ,检查Downloads目录是否显示并呈灰显状态。 4 使用intent action – ACTION_OPEN_DOCUMENT检查Android/data/和Android/obb/目录是否都不显示。
2.1.4存储权限变更 1 背景 Android 11 引入了与存储权限相关的以下变更。 1 不管应用的目标 SDK 版本是什么,以下变更均会在 Android 11 中生效: ● 存储运行时权限已重命名为文件和媒体。 ● 如果应用未选择停用分区存储,并且请求 READ_EXTERNAL_STORAGE 权限,则用户会看到不同于 Android 10 的对话框。该对话框会指示应用正在请求访问相册和多媒体。如下图所示:
在系统设置的设置 > 隐私 > 权限管理器 > 文件和媒体 页面中,用户可以查看已授予权限READ_EXTERNAL_STORAGE应用,应用会列在允许存储所有文件下。 注意:如果您的应用以Android 11 为目标运行平台,上述允许存储所有文件代表的是对文件的只读权限。
2 以 Android 11 为目标平台 如果应用以 Android 11 为目标平台,则WRITE_EXTERNAL_STORAGE 权限和 WRITE_MEDIA_STORAGE 特许权限将不再提供任何其他访问权限。 2 兼容性影响 1 存储运行时权限UI发生变更。 2 WRITE_EXTERNAL_STORAGE 权限和 WRITE_MEDIA_STORAGE 在targetSdkVersion 指定为30时,发生变更。
2.1.5所有文件访问 1背景 有些应用主要功能就是访问手机存储文件,例如文件管理器、备份&恢复出厂操作。在Android 11 版本上,需要通过声明MANAGE_EXTERNAL_STORAGE权限来获取“Allowed for all files(允许存储所有文件)”权限,进行功能实现。 此权限被授予后,拥有以下权限: 1.“共享存储”上的所有文件的读写权限 共享存储说明: https://developer.android.google.cn/training/data-storage/shared 2. MediaStore.Files表内容 注意:即便授予了所有文件访问权限,应用也不能获取其他app的应用专属的文件。 应用专属目录: https://developer.android.google.cn/training/data-storage/app-specific 2兼容性影响 文件管理类应用或其他需要对较多存储文件进行扫描和处理的应用,可能会功能失效。 3 适配指导 Google适配指导: https://developer.android.google.cn/preview/privacy/storage#all-files-access
1 AndroidManifest.xml中声明MANAGE_EXTERNAL_STORAGE权限。 2 使用intent action – ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 跳转到系统设置页面,引导用户开启“Allowed for all files(允许存储所有文件)”权限。
2.2 权限 2.2.1单次权限 1.1 背景 对于最敏感的数据类型,包括位置信息、设备的麦克风和摄像头,在 Android 11中,用户可以授予单次的临时访问权限。
如右图所示。
如果用户选择了该选项,应用将获得临时的一次性权限。应用至少需要满足以下条件中一条时才能访问相关的数据:
(1)应用的Activity在用户授予一次性权限之后一直可见。 (2)应用在用户授予权限时可见,并且从那之后一直运行着前台服务,即使用户将应用切到后台,应用也会保留权限。 (3)应用短时间退至后台。 如果以上三个条件都不满足,无论应用的targetSdkVersion是什么,都需要再次请求该权限,才能访问相关数据。 Google官网特性介绍: https://developer.android.google.cn/preview/privacy/permissions#dialog-visibility 1.2兼容性影响 对于应用请求位置信息、麦克风或摄像头相关的权限时,用户可能授予“仅限这一次”权限,当应用被切换到后台(既无前台Activity,又无前台服务),该权限会被系统收回,并且应用进程会被杀掉。 1.3适配指导 应用申请位置信息、麦克风或摄像头相关的权限后,如在后台访问相关的敏感数据,需要确保权限不会被系统撤销的情况下执行相应的功能。 如果您的应用已经遵循Google权限申请最佳实践则不受变更影响,否则请按照最佳实践进行适配: https://developer.android.google.cn/privacy/best-practices#permissions 2.2.2权限对话框可见性 1背景 Android 11 不建议重复请求特定权限组中的权限。在应用安装到设备上后,如果用户在使用过程中对某个特定权限拒绝了两次,则表示其希望“不再询问”相应权限组的权限。 系统对于是否算作“拒绝”选项,做出了如下两个定义: (1)如果用户按返回按钮关闭权限对话框,此操作不算“拒绝”操作。 (2)应用使用requestPermissions()转到系统设置,然后点返回按钮,此操作就算是“拒绝”操作。 2兼容性影响 应用若对运行时权限使用不规范,可能出现权限被关闭后无法正确引导用户打开权限、闪退的现象。 3适配指导 1 应用尽量不要申请与功能不相关的权限。 2 如果功能必须使用到被用户拒绝的权限,应用可以在权限拒绝的回调中弹窗提示用户,说明申请该权限的意图,引导用户跳转到应用权限设置页面,授予该权限。 请按照Google权限申请最佳实践适配: https://developer.android.google.cn/privacy/best-practices#permissions 2.2.3读取手机号码权限 1 背景 Android 11 更新了对手机号码读取的权限管理。如果您的应用targetSdkVersion为30,通过TelephonyManager和 TelecomManager的getLine1Number()方法,或者TelephonyManager的getMsisdn()方法读取号码时,需要申请READ_PHONE_NUMBERS权限,即便申请了READ_PHONE_STATE权限。 Google官网特性介绍: https://developer.android.google.cn/preview/privacy/permissions#phone-numbers 2 兼容性影响 如果您的应用升级到targetSdkVersion到30,并且应用通过TelephonyManager和TelecomManager的getLine1Number()方法,或TelephonyManager的getMsisdn()方法获取电话号码,若没有申请READ_PHONE_NUMBERS权限,则无法获取电话号码。 3 适配指导 1 如果您的应用使用READ_PHONE_STATE权限读取电话号码,但是没有使用上文中所提到的getLine1Number()或getMsisdn()方法,则不受影响。 2 如果您的应用升级到R,配置了READ_PHONE_STATE权限同时使用了getLine1Number()或getMsisdn()方法,则按照如下方法进行适配: a.调整READ_PHONE_STATE权限申明的最大Sdk版本 b.增加READ_PHONE_NUMBERS权限。 代码如下:
2.2.4 闲置应用权限自动重置 1 背景 如果您的应用以Android11为目标平台并且数月未使用,系统会通过自动重置用户已授予应用的敏感权限来保护用户数据。此操作与用户在系统设置中查看权限并将应用的访问权限级别更改为拒绝的做法效果一样。 2 兼容性影响 如果您的应用以Android11为目标平台,若用户长时间不使用,当用户再次使用时,若应用没有权限校验逻辑则会导致与回收权限相关的业务失效。 3 适配指导 Goolge适配指导:https://developer.android.google.cn/preview/privacy/permissions#auto-reset
如果您的应用遵循有关在运行时请求权限的最佳实践,那么您不必对应用进行任何更改。这是因为,当用户与应用中的功能互动时,您应该会验证相关功能是否具有所需权限。 权限申请的最佳实践:https://developer.android.google.cn/training/permissions/requesting
2.3后台位置访问 1背景 Android 11移除了来自应用程序中提示允许运行后台定位访问。 如下图所示
上述应用内的权限弹窗中没有了“Allow all the time(一直允许)”的选项。 Google官网特性介绍: https://developer.android.google.cn/preview/privacy/location#background-location 2 兼容性影响 1 获取定位权限的应用程序对话框不再包括“一直允许”的选项 2 用户deny掉应用定位访问请求两次,任何进一步请求相同权限都会被系统忽略掉。 3 如果尝试请求ACCESS_BACKGROUND_LOCATION,同时请求任何其他权限,系统会抛出一个异常。(只针对sdk为android 11),具体表现为闪退。 4 调用requestPermissions()请求后台定位权限会跳转至系统设置界面。
3 适配指导
(1)Target Android 11 使用自定义的UI界面向用户展示申请ACCESS_BACKGROUND_LOCATION权限原因和过程。 自定义UI界面的方法,请参考Google适配指导 https://developer.android.google.cn/preview/privacy/location#create-custom-ui (2) Target Android 10 or lower 使用系统提供的UI界面向用户展示申请ACCESS_BACKGROUND_LOCATION权限原因和过程。 系统UI界面使用方法,请参考Google适配指导 https://developer.android.google.cn/preview/privacy/location#use-system-provided-ui
2.4应用包可见性 1 背景 应用包可见性(Package Visibility),是Android 11上提升系统隐私安全性的一个新特性。它的作用是限制app随意获取其他app的信息和安装状态。 此前,Android系统提供了多种SDK接口(主要在PMS中,如getInstalledPackages(0)等),使app能轻易获取其他app的信息。这些接口容易被病毒软件、间谍软件利用,引发网络钓鱼、用户安装信息泄露等安全事件,而同时此类接口的合法应用场景也很多,仅靠软件商店的扫描检测监控很难识别这类接口的滥用。 因此,Android 11上引入了Package Visibility新特性。它改造了获取app信息的接口,且封堵了SDK接口以外变相获取app安装状态的途径。如果app一定需要获取某些app的信息,必须在清单文件中声明“需要交互”的app,或者声明特定的权限以越过拦截。 Package Visibility特性符合“最低优先级原则”:仅允许app获取“它需要交互”的app的信息。 Google官网特性介绍: https://developer.android.google.cn/preview/privacy/package-visibility 1.2 兼容性影响 1 SDK接口行为变更 app默认不再能通过SDK接口获取其他app的信息,例如: getPackageInfo(getPackageName(), 0) 仍能正常返回应用自身的PackageInfo,但getPackageInfo(“com.another.app”,0)将抛出NameNotFoundException,即使com.another.app已经被安装到设备上。getInstalledPackages(0),只能返回应用自身,以及少数的核心AOSP应用的信息。
2 其他变更 app不能感知/data/data/com.another.app等目录的存在。即使com.another.app已经被安装到设备上,app尝试访问/data/data/com.another.app也将提示“File not found”,而不是“Permission denied”。 类似的目录还有/data/misc/profiles/cur/${userId}/com.another.app。此变更,可以防止app通过访问目录文件返回的错误不同,来判断特定app是否已被安装。 3 适配指导 如果仍想要通过SDK接口获取其他app的信息,需要在清单文件中声明自己“需要交互的app”。有三种方式: ● 声明要交互特定的app <manifest> …… <queries> <package android:name=”com.another.app”/> </queries> …… </manifest> ● 声明要交互能响应特定intent的apps <manifest> …… <queries> <intent> <action android:name=”android.intent.action.SEND” /> <data android:type=”image/jpeg” /> </intent> </queries> …… </manifest> ● 声明要交互所有的app 申请新权限:android.permission.QUERY_ALL_PACKAGES。 QUERY_ALL_PACKAGES权限等级为normal,app申请即可获得。但是,Google Play上架应用时会检测并限制该权限的使用。同理,在queries中声明“宽泛的intent”也将受到Google Play的检测和限制。 Google适配指导: https://developer.android.google.cn/preview/privacy/package-visibility
2.5前台服务类型 1 背景 从Android 9 开始,应用被限制只有在前台时才能使用camara和microphone。Android 11 为了进一步保护用户隐私权限,通过引入变更来限制前台服务访问camara和microphone相关的数据。如果你的应用targetSdkVersion指定为Android 11,并且在前台服务中访问这些数据,你需要在Manifest中注册前台服务组件时,指定foregroundServiceType为camara和microphone。 2兼容性影响 1 targetSdkVersion为Android 11的应用在前台服务中访问camera或microphone相关的数据,不指定相应的foregroundServiceType,将无法正常访问到相关数据。 2 targetSdkVersion为Android 11的应用,在后台启动前台服务,即使应用声明了对应的foregroundServiceType,也无法正常访问相关数据。 3适配指导 Google适配指导: https://developer.android.google.cn/preview/privacy/foreground-service-types
1 在AndroidManifest.xml文件中配置foregroundServiceType 如果你的应用运行的前台服务需要访问location和camera相关的数据,需要在Manifest中申明服务时按照如下方式指定foregroundServiceType: <manifest> … <service … android:foregroundServiceType=”location|camera” /> </manifest> 如果您的应用运行的前台服务需要访问location,camera和microphone相关的数据,需要在Manifest中声明服务时按照如下方式指定foregroundServiceType: <manifest> … <service … android:foregroundServiceType=”location|camera|microphone”/> </manifest> (2)需要在前台服务中获取location、camera、microphone相关数据时,应用必须在前台时启动前台服务。
三、行为变更
3.1 Firebase JobDispatcher 和 GCMNetworkManager 停用 1 背景 如果您应用的目标API级别是R或者更高,运行在Android 6.0 或更高版本上,Firebase JobDispatcher和GcmNetworkManager API已经失效。 2 兼容性影响 如果应用的目标API级别是R或者更高,以Firebase JobDispatcher和GcmNetworkManager实现的功能将在Android6.0及其后续版本将失效。 3 适配指导 将Firebase JobDispatcher和GcmNetworkManager迁移成WorkManager进行代替。 Firebase JobDispatcher迁移指南: https://developer.android.google.cn/topic/libraries/architecture/workmanager/migrating-fb GcmNetworkManager迁移指南: https://developer.android.google.cn/topic/libraries/architecture/workmanager/migrating-gcm
3.2自定义view的Toast屏蔽 1 背景 出于安全方面的考虑,同时也为了保持良好的用户体验,如果包含自定义视图的toast消息是以 Android 11 为目标平台的应用从后台发送的,则系统会屏蔽这些消息框。请注意,仍允许使用文本消息框;此类消息是使用Toast.makeText()创建的,并不调用setView()。 如果您的应用仍尝试从后台发布包含自定义视图的toast消息,系统会在 logcat 中记录以下消息: W/NotificationService: Blocking custom toast from package <package> due to package not in the foreground。 2 兼容性影响 如果您的应用以Android 11为目标运行平台时,后台使用自定义view的toast消息将不能显示,可能会影响用户交互的完整性。 3 适配指导 Google适配指导: https://developer.android.google.cn/preview/features/toasts
1 使用兼容性框架测试变更 adb shell am compat enable (128611929|CHANGE_BACKGROUND_CUSTOM_TOAST_BLOCK) PACKAGE_NAME adb shell am compat disable (128611929|CHANGE_BACKGROUND_CUSTOM_TOAST_BLOCK) PACKAGE_NAME 兼容性框架的相关介绍,请参考 https://developer.android.google.cn/preview/behavior-changes-11
2 使用snackbar替换toast信息提示,使用方法请参考: https://developer.android.google.cn/training/snackbar/showing#display
3 如果snackbar不适用业务场景,仍然需要在后台显示toast,可以使用纯文本的toast,即不设置自定义的view使用系统提供的toast默认样式即可,不调用setView()方法。
4 以Android 11 为目标平台时,调用如下接口将发生变更: getView()方法将返回 null getHorizontalMargin(),getVerticalMargin(),getGravity() ,getXOffset(),getYOffset() 方法不能返回实际值,不要在业务逻辑中依赖接口的返回值。 setMargin(),setGravity()方法将会失效。 上述接口变更的具体说明,请查看 https://developer.android.google.cn/reference/android/widget/Toast 3.3堆指针标记 1背景 Android 11上,堆指针在最高有效字节 (MSB) 中有一个非零标记。错误地使用指针的应用(包括修改 MSB 的应用)会崩溃或遇到其他问题。这是支持未来启用了ARM内存标记扩展 (MTE) 的硬件所必需的变更。 2 兼容性影响 如果您的应用目标Sdk为R则堆指针标记默认开启,目标SDK低于R时,默认关闭。 使用如下命令开启或关闭此特性,查看您的应用是否有错误的使用指针场景。 adb shell am compat enable (135754954|NATIVE_HEAP_POINTER_TAGGING) PACKAGE_NAME adb shell am compat disable (135754954|NATIVE_HEAP_POINTER_TAGGING) PACKAGE_NAME 3 适配指导 应用可以在AndroidManifest.xml文件中进行如下配置,显式关闭此特性。
<application android:allowNativeHeapPointerTagging=”false”> … </application> 不过若您的应用有指针使用上的问题,关闭并不能帮助解决问题,建议如果有相关问题,直接解决。同时以上的规避方案在后续的Android版本中,将会移除。 Google特性介绍和适配指导: https://source.android.google.cn/devices/tech/debug/tagged-pointers
3.4 Netlink MAC 地址限制 1 背景 在以 API 级别“30”及更高版本为目标平台的应用中,非特权应用(预置或系统应用)将无法访问设备的 MAC 地址;只有具有 IPv4 地址的网络接口可见。 2 兼容性影响 getifaddrs() 返回 -1。 NetworkInterface.getHardwareAddress() 返回 null。 应用无法对NETLINK_ROUTE套接字使用bind()函数。 ip命令不会返回有关接口的信息。 3 适配指导 在相关业务中使用级别较高的ConnectivityManager API 而不是级别较低的NetworkInterface/getifaddrs() API。 参考Google获取设备标识符的最佳实践进行适配: https://developer.android.google.cn/training/articles/user-data-ids#mac-addresses 3.5 MAC地址随机分配 1 背景 为了进一步保护用户的隐私,Android Q在连接Wi-Fi时,默认启用了Mac地址随机化的特性,如果 APP不进行适配,使用原来方式获取到的Mac地址可能是随机生成的,并不是真实的Mac地址。在Android 11上,Passpoint网络会根据每个profile/FQDN,生成一个持久可用的随机Mac地址,每次wifi网络关联都会使用新生成的随机Mac地址(AndroidR根据每个SSID生成一个Mac地址,Android 11是在此基础上的加强模式)。 2 兼容性影响 如果您的APP需要使用Mac地址作为设备的标识,无论您的Target SDK是否设置为R,只要运行在Android 11上,您就需要进行适配。 3 适配指导 请参考谷歌适配指导: https://developer.android.google.cn/preview/privacy/data-identifiers#randomized-mac-addresses 3.6限制对APN数据库的读取访问 1背景 以Android 11 为目标平台的应用现在必须具备Manifest.permission.WRITE_APN_SETTINGS特权才能Telephony provider APN 数据库。如果在不具备此权限的情况下尝试访问 APN 数据库,会生成安全异常。 2 兼容性影响 如果您的应用业务涉及到APN数据库读取,并且targetSdkVersion指向30,会发生访问APN数据库的安全异常。 3 适配指导 1 使用以下 ADB 命令来开启或关闭此变更,测试变更的影响: adb shell am compat enable (124107808|APN_READING_PERMISSION_CHANGE_ID) PACKAGE_NAME adb shell am compat disable (124107808|APN_READING_PERMISSION_CHANGE_ID) PACKAGE_NAME
2 在AndroidManifest中申请Manifest.permission.WRITE_APN_SETTINGS权限。
3.7压缩的资源文件 1 背景 以Android 11 为目标运行平台的应用,如果安装包中的resources.arsc进行了压缩或没有进行4字节对齐,则该应用将无法安装。 上述变更的原因是,压缩或未对齐的resources.arsc文件,系统无法直接通过内存映射(mmap)加载,而只能通过buffer读入内存,这会增加系统内存压力和内存占用。 2 兼容性影响 如果您的应用targetSdkVersion为30,安装包文件中的resources.arsc文件有压缩或者未进行4字节对齐,则您的应用将无法在运行Android 11设备上进行安装。 3 适配指导 不对resources.arsc文件压缩,同时检查是否进行了4字节对齐。如果没有,请使用zipalign工具进行对齐: https://developer.android.google.cndeveloper.android.com/studio/command-line/zipalign
3.8文件描述符排错程序 (fdsan) 1背景 fdsan(FdSanitizer)是Android在Android 10.0里开始引入的一个避免重复操作已关闭文件描述符的保险机制。在Android Q中,当进程出现重复操作已关闭文件描述符动作后,会打印错误信息帮助开发者定位问题。Android 11上,除打印异常信息外,进程会终止执行,更加严格。 关于fdsan原理的详细介绍可参考博客: https://android.googlesource.com/platform/bionic/ /master/docs/fdsan.md 2兼容性影响 Android 11中,当出现对已关闭描述符的重复操作(use-after-close、double-close)时,进程会终止执行。 3适配指导 应用需关注自己的代码写法,使用Android推荐的unique_fd,尽量避免自己直接操作fd。 不推荐:int fd = dup(STDOUT_FILENO); 推荐: android::base::unique_fd fd(dup(STDOUT_FILENO));
3.9无障碍服务按钮 1背景 从Android 11开始,应用无障碍服务在运行时不能申明与系统的无障碍按钮的关联。即使应用添加了AccessibilityServiceInfo.FLAG_REQUEST_ ACCESSIBILITY_BUTTON到AccessibilityServiceInfo对象的flag属性,系统也不会传递无障碍按钮回调事件到自定义的AccessibilityService中。取而代之的是,通过在无障碍服务注册的metadata文件中来申明无障碍服务与无障碍按钮的关联。 2 兼容性影响 对于使用到无障碍服务的应用,如果仅仅在代码中添加了AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON,则系统不会回调障碍按钮操作事件到自定义的AccessibilityService中,应用以此来实现的功能将失效。 3 适配指导
// Manifest文件中注册自定义无障碍服务 <service android:name=”.r.accessibilityservice.MyAccessibilityService” android:label=”@string/app_name” android:permission=”android.permission.BIND_ACCESSIBILITY_SERVICE”>
<intent-filter> <action android:name=”android.accessibilityservice.AccessibilityService” /> </intent-filter> <meta-data android:name=”android.accessibilityservice” android:resource=”@xml/accessibility_config”> </meta-data> </service>
//accessibility_config.xml文件中配置flagRequestAccessibilityButton <?xml version=”1.0″ encoding=”utf-8″?> <accessibility-service xmlns:android=”http://schemas.android.com/apk/res/android” android:accessibilityEventTypes=”typeAllMask” android:accessibilityFeedbackType=”feedbackAllMask” android:accessibilityFlags=”flagDefault| flagRequestAccessibilityButton” android:canRetrieveWindowContent=”true” android:description=”@string/accessibility_service_description” android:notificationTimeout=”100″ /> 应用需要指定 android:accessibilityFlags属性必须包含flagRequestAccessibilityButton的flag。
3.10 SYSTEM_ALERT_WINDOW权限授予 1背景 在Android 11上,SYSTEM_ALERT_WINDOW权限的授权过程有了如下调整: 1 拥有ROLE_CALL_SCREENING角色的应用在申请SYSTEM_ALERT_WINDOW 权限时即授予权限,不需要再使用intent ACTION_MANAGE_OVERLAY_PERMISSION 引导用户进行授权处理,一旦应用不再拥有ROLE_CALL_SCREENING角色属性,将失去权限。 2 MANAGE_OVERLAY_PERMISSION intent 始终会跳转到系统设置界面 ACTION_MANAGE_OVERLAY_PERMISSION intent 始终会将用户转至设置页面,用户可在其中授予或撤消应用的 SYSTEM_ALERT_WINDOW 权限;同时intent 中的任何package:数据都会被忽略。 在更低版本的 Android中,ACTION_MANAGE_OVERLAY_PERMISSION intent 可以指定一个应用包,它会将用户转至应用设置页面来管理权限。Android 11 不再支持此功能,而是必须由用户先选择要对其授予或撤消权限的应用。此变更可以让权限的授予更有目的性,从而达到保护用户的目的。 2兼容性影响 三方应用跳转到悬浮窗管理界面逻辑发生变化。 3 适配指导 此部分变更主要是悬浮窗权限管理界面跳转逻辑的变化,如果您的应用在这部分有做特殊的适配处理,如使用辅助服务进行自动授权的操作,请根据UI界面的变化进行适配。
3.11 限制非SDK接口 1 背景 Android 11 包含更新后的受限制非 SDK 接口列表,在限制使用非 SDK 接口之前,Google尽可能确保提供公开替代方案。具体调整如下: 1 非 SDK 测试 API 现在受到限制 从 Android 11 开始,默认情况下,非 SDK 测试 API(即 AOSP 中使用 @TestApi 注释的 API)现在受到限制。这些非 SDK 接口用于在 Android 平台上执行内部测试。应用可以继续使用灰名单中的测试 API,但任何新的测试 API 都会包含在黑名单中。 2 目前在 Android 11 中受限的灰名单中的非 SDK 接口 https://developer.android.google.cn/preview/non-sdk-11#greylist-now-restricted 上述列出了 Android 10(API 级别 29)中列入灰名单而目前在 Android 11 中受限的所有非 SDK 接口。只要有可能,接口名称后面的注释中都会提供建议的替代 API。每个接口占一行。 3 Android 11 中已列入白名单的非 SDK 接口(这些接口原本列在灰名单中) https://developer.android.google.cn/preview/non-sdk-11#greylist-now-public 2 兼容性影响 如果您的应用并非以 Android 11 为目标平台,那么其中一些变更可能不会立即对您产生影响。虽然您目前仍然可以使用灰名单中的一些非 SDK 接口(取决于您的应用的目标 API 级别),但如果您使用任何非 SDK 方法或字段,则应用在将来系统版本或安全补丁升级后无法运行的风险终归较高。 3 适配指导 non-SDK接口介绍 https://developer.android.google.cn/distribute/best-practices/develop/restrictions-non-sdk-interfaces 查看最新non-SDK的api列表 https://developer.android.google.cn/preview/non-sdk-11#r-list-changes 1 测试应用是否使用非 SDK 接口 https://developer.android.google.cn/distribute/best-practices/develop/restrictions-non-sdk-interfaces#test-for-non-sdk 2 如果您的应用依赖于非 SDK 接口,则应该开始计划迁移到 SDK 替代方案。替代的API会在名单中以注释的形式给出,如: Landroid/app/AppOpsManager;->noteOpNoThrow(IILjava/lang/String;)I # Use #noteOpNoThrow(java.lang.String, int, java.lang.String, java.lang.String, java.lang.String) instead. 3 请求新的公共 API 如果您无法为应用中的某项功能找到使用非 SDK 接口的替代方案,则应该请求新的公共 API。 https://developer.android.google.cn/distribute/best-practices/develop/restrictions-non-sdk-interfaces#feature-request
3.12 V1版Google 地图共享库移除
1 背景 Android 11中已完全移除V1版Google地图共享库。此库之前已被弃用,并已停止在Android 10中的应用中运行。 2 兼容性影响 如果您的应用依赖了V1版本的Google地图共享库,运行在搭载Android 11设备上时,应用中使用Google地图共享库的相关功能将失效。 3 适配指导 如果您的应用之前依赖V1版Google地图共享库,现在需要切换为接入Google地图SDK。具体的接入方式,请参考https://developers.google.cn/maps/documentation/android-sdk/intro。 当您完成切换后,请务必从应用的清单文件的<uses-library>元素中移除对V1版Google地图共享库的引用,因为现在应用无法再将Google Play过滤与V1版Google地图共享库和<uses-library>元素一起使用。
3.13 APK签名方案v2要求 1 背景 如果您的应用以Android 11(API级别30)为目标平台,且目前仅使用APK签名方案v1签名,现在需要在v1签名的基础上还必须使用APK签名方案v2或更高版本进行签名。 2 兼容性影响 用户无法在搭载Android 11的设备上安装或更新仅通过APK签名方案v1 签名的应用。 3 适配指导 1 APK签名方案验证 您可以在命令行中使用AndroidStudio或 apksigner工具,验证您的应用是否已使用APK签名方案v2或更高版本进行签名。 Apk签名方案v2介绍: https://source.android.google.cn/security/apksigning/v2 Apksigner工具介绍: https://developer.android.google.cn/studio/command-line/apksigner AndroidStudio签名介绍: https://developer.android.google.cn/studio/publish/app-signing#sign_release 2 Android旧版本兼容 为支持运行旧版Android的设备,除了使用APK签名方案v2或更高版本为您的APK签名之外,您还应继续使用APK签名方案v1进行签名。
四、Google Android 11适配信息汇总
隐私更新: https://developer.android.google.cn/preview/privacy
行为变更: https://developer.android.google.cn/preview/behavior-changes-all https://developer.android.google.cn/preview/behavior-changes-11
Android 11 版本发布时间线: https://developer.android.google.cn/preview/overview
应用适配重要时间点: 2020.6 Beta1 最终API,开放Google Play发布。 ● 建议开发者开始对应用,SDK和库进行最终的兼容性测试。发布兼容版本,留意Android Beta 版用户反馈,继续针对Android 11的工作。使用正式API进行构建和测试。
2020.7 Beta2 平台稳定性里程碑 ● 公开API,SDK不发生改变 ● 私有API和系统内部实现逻辑开启回滚策略,即此阶段出现的问题会回滚相对应代码改动的来保证兼容性和稳定性。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/158479.html原文链接:https://javaforall.cn