Android 10适配指南
2019 年 9 月 3 日,Google 发布了 Android 10(API =29) 正式版。Android 10 聚焦移动创新、安全隐私和数字健康三大主题,全面打造最佳用户体验。
在Android 10 版本中,官方的改动较大,相应的开发者适配成本还是很高的。基于前期调研,我们主要基于以下几方面进行Android 10的适配:
1.Android X
2.分区存储
3.设备ID
4.明文HTTP限制
AndroidX
什么是AndroidX
随着Android系统版本不断地迭代更新,每个版本中都会加入很多新的API进去,但是新增的API在老版系统中并不存在,因此这就出现了一个向下兼容的问题。
于是Android团队推出了一个鼎鼎大名的Android Support Library,用于提供向下兼容的功能。比如我们熟知的support-v4库,appcompat-v7库都是属于Android Support Library的。4在这里指的是Android API版本号,对应的系统版本是1.6。support-v4的意思就是这个库中提供的API会向下兼容到Android 1.6系统。类似地,appcompat-v7指的是将库中提供的API向下兼容至API 7,也就是Android 2.1系统。
随着时间的推移,Android1.6、2.1系统早已被淘汰了,现在Android官方支持的最低系统版本已经是4.0.1,对应的API版本号是15。support-v4、appcompat-v7库也不再支持那么久远的系统了,但是它们的名字却一直保留了下来,虽然它们现在的实际作用已经对不上当初命名的原因了。
Android团队也意识到这种命名已经非常不合适了,于是对这些API的架构进行了一次重新的划分,推出了AndroidX。因此,AndroidX本质上其实就是对Android Support Library进行的一次升级。
为什么要升级AndroidX
版本 28.0.0 是Android Support 库的最后一个版本。官方将不再发布 android.support 库版本。所有新功能都将在 AndroidX命名空间中开发。
长远来看。AndroidX重新设计了包结构,旨在鼓励库的小型化,支持库和架构组件包的名字进行了简化。而且这也是减轻Android生态系统碎片化的有效方式。
与Android Support库不同,AndroidX软件包是单独维护和更新的。这些AndroidX包使用严格的语义版本控制,从版本1.0.0开始,您可以单独更新项目中的AndroidX库。
分区存储
背景介绍
为了更好的保护用户数据并限制设备冗余文件增加,以 Android 10(API 级别 29)及更高版本为目标平台的应用在默认情况下被赋予了对外部存储设备的分区访问权限(即分区存储), 对外部存储文件访问方式重新设计,便于用户更好的管理外部存储文件。
应用只能看到本应用专有的目录(通过 Context.getExternalFilesDir() 访问)以及特定类型的媒体。除非您的应用需要访问存放在应用的专有目录以及 MediaStore 之外的文件,否则最好使用分区存储。
要点:
1.Android Q文件存储机制修改成了沙盒模式
2.APP只能访问自己目录下的文件和公共媒体文件
3.Android Q版本以下机型,还是使用老的文件存储方式
4.Android Q及以上版本机型,所有应用均需要分区存储, 所以应用需要提前确保支持分区存储
需要注意:在适配AndroidQ的时候还要兼容Q系统版本以下的,使用SDK_VERSION区分
外部存储:被分为应用私有目录以及共享目录两个部分
应用私有目录:存储应用私有数据,外部存储应用私有目录对应
1.Android/data/packagename,内部存储应用私有目录对应data/data/packagename。
2.应用私有目录文件访问方式与之前Android版本一致,可以通过File path获取资源。
共享目录:
1.存储其他应用可访问文件, 包含媒体文件、文档文件以及其他文件,对应设备DCIM、Pictures、Alarms, Music, Notifications,Podcasts, Ringtones、Movies、Download等目录。
2.共享目录文件需要通过MediaStore API或者Storage Access Framework方式访问。
3.MediaStore API在共享目录指定目录下创建文件或者访问应用自己创建文件,不需要申请存储权限;
4.MediaStore API访问其他应用在共享目录创建的媒体文件(图片、音频、视频), 需要申请存储权限,未申请存储权限,通过ContentResolver查询不到文件Uri,即使通过其他方式获取到文件Uri,读取或创建文件会抛出异常;
5.MediaStore API不能够访问其他应用创建的非媒体文件(pdf、office、doc、txt等), 只能够通过Storage Access Framework方式访问;
受影响的变更
图片位置信息
一些图片会包含位置信息,因为位置对于用户属于敏感信息, Android 10应用在分区存储模式下图片位置信息默认获取不到,应用通过以下两项设置可以获取图片位置信息:
1.1在manifest中申请ACCESS_MEDIA_LOCATION
1.2调用MediaStore setRequireOriginal(Uri uri)接口更新图片Uri
访问数据
1.MediaStore.Files应用分区存储模式下,MediaStore.Files 集合只能够获取媒体文件信息(图片、音频、视频), 获取不到非media(pdf、office、doc、txt等)文件。
File Path路径访问受影响接口
开启分区存储新特性, Andrioid 10不能够通过File Path路径直接访问共享目录下资源,以下接口通过File 路径操作文件资源,功能会受到影响,应用需要使用MediaStore或者SAF方式访问。

存储特性Android版本差异概览

兼容模式
应用未完成外部存储适配工作,可以临时以兼容模式运行, 兼容模式下应用申请存储权限,即可拥有外部存储完整目录访问权限,通过Android10之前文件访问方式运行,以下设置应用以兼容模式运行。
tagretSDK 大于等于Android 10(API level 29), 在manifest中设置requestLegacyExternalStorage属性为true。
<manifest ...>
...
<application android:requestLegacyExternalStorage="true" ... >
...
</manifest>
判断兼容模式接口
//返回值
//true : 应用以兼容模式运行
//false:应用以分区存储特性运行
Environment.isExternalStorageLegacy();
备注:应用已完成存储适配工作且已打开分区存储开关,如果当前应用以兼容模式运行,覆盖安装后应用仍然会以兼容模式运行,卸载重新安装应用才会以分区存储模式运行
适配方案
分区存储适配包含文件迁移以及文件访问兼容性适配两个部分:
文件迁移是将应用共享目录文件迁移到应用私有目录或者Android10要求的media集合目录。
1.1针对只有应用自己访问并且应用卸载后允许删除的文件,需要迁移文件到应用私有目录文件,可以通过File path方式访问文件资源,降低适配成本。
1.2允许其他应用访问,并且应用卸载后不允许删除的文件,文件需要存储在共享目录,应用可以选择是否进行目录整改,将文件迁移到Android10要求的media集合目录。
共享目录文件不能够通过File path方式读取,需要使用MediaStore API或者Storage Access Framework框架进行访问。
1.1AndroidQ中使用ContentResolver进行文件的增删改查。
//获取(创建)私有目录下的文件夹
//在自身目录下创建apk文件夹
File apkFile = context.getExternalFilesDir("apk");
//创建私有目录文件
//生成需要下载的路径,通过输入输出流读取写入
String apkFilePath = context.getExternalFilesDir("apk").getAbsolutePath();
File newFile = new File(apkFilePath + File.separator + "demo.apk");
OutputStream os = null;
try {
os = new FileOutputStream(newFile);
if (os != null) {
os.write("file is created".getBytes(StandardCharsets.UTF_8));
os.flush();
}
} catch (IOException e) {
} finally {
try {
if (os != null) {
os.close();
}catch (IOException e1) {
}
}
//创建共享目录文件夹
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentResolver resolver = context.getContentResolver();
ContentValues values = new ContentValues();
values.put(MediaStore.Downloads.DISPLAY_NAME, fileName);
values.put(MediaStore.Downloads.DESCRIPTION, fileName);
//设置文件类型
values.put(MediaStore.Downloads.MIME_TYPE, "application/vnd.android.package-archive");
//注意MediaStore.Downloads.RELATIVE_PATH需要targetVersion=29,
//故该方法只可在Android10的手机上执行
values.put(MediaStore.Downloads.RELATIVE_PATH, "Download" + File.separator + "apk");
Uri external = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
Uri insertUri = resolver.insert(external, values);
return insertUri;
}else{
...
}
//在共享目录指定文件夹下创建文件
//主要是在公共目录下创建文件或文件夹拿到本地路径uri,不同的Uri,可以保存到不同的公共目录中。接下来使用输入输出流就可以写入文件。
//重点:AndroidQ中不支持file://类型访问文件,只能通过uri方式访问。
// 创建图片地址uri,用于保存拍照后的照片 Android 10以后使用这种方法
private Uri createImageUri() {
String status = Environment.getExternalStorageState();
// 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储
if (status.equals(Environment.MEDIA_MOUNTED)) {
return getContext().getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new ContentValues());
} else {
return getContext().getContentResolver().insert(MediaStore.Images.Media.INTERNAL_CONTENT_URI, new ContentValues());
}
}
//通过MediaStore API读取公共目录下的文件
if (cursor != null && cursor.moveToFirst()) {
do {
...
int _id = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));
Uri imageUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, _id);
...
} while (!cursor.isLast() && cursor.moveToNext());
} else {
...
}
// 通过uri获取bitmap
public Bitmap getBitmapFromUri(Context context, Uri uri) {
ParcelFileDescriptor parcelFileDescriptor = null;
FileDescriptor fileDescriptor = null;
Bitmap bitmap = null;
try {
parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r");
if (parcelFileDescriptor != null && parcelFileDescriptor.getFileDescriptor() != null) {
fileDescriptor = parcelFileDescriptor.getFileDescriptor();
//转换uri为bitmap类型
bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
if (parcelFileDescriptor != null) {
parcelFileDescriptor.close();
}catch (IOException e) {
}
}
return bitmap;
}
//使用MediaStore删除文件
context.getContentResolver().delete(fileUri, null, null);
设备ID
IMEI等设备信息
从Android10开始普通应用不再允许请求权限android.permission.READ_PHONE_STATE。而且,无论你的App是否适配过Android Q(既targetSdkVersion是否大于等于29),均无法再获取到设备IMEI等设备信息。
//受影响的API
Build.getSerial();
TelephonyManager.getImei();
TelephonyManager.getMeid()
TelephonyManager.getDeviceId();
TelephonyManager.getSubscriberId();
TelephonyManager.getSimSerialNumber();
- targetSdkVersion<29 的应用,其在获取设备ID时,会直接返回null
- targetSdkVersion>=29 的应用,其在获取设备ID时,会直接抛出异常SecurityException
如果您的App希望在Android 10以下的设备中仍然获取设备IMEI等信息,可按以下方式进行适配:
<uses-permission android:name="android.permission.READ_PHONE_STATE"
android:maxSdkVersion="28"/>
Mac地址随机分配
从Android10开始,默认情况下,在搭载 Android 10 或更高版本的设备上,系统会传输随机分配的 MAC 地址。(即从Android 10开始,普通应用已经无法获取设备的真正mac地址,标识设备已经无法使用mac地址)
如何标识设备唯一性
Google解决方案:如果您的应用有追踪非登录用户的需求,可用ANDROID_ID来标识设备。
1.ANDROID_ID生成规则:签名+设备信息+设备用户
2.ANDROID_ID重置规则:设备恢复出厂设置时,ANDROID_ID将被重置
String androidId = Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID);
信通院统一SDK(OAID)
MSA 统一 SDK 下载地址:
移动安全联盟官网,http://www.msa-alliance.cn/
明文HTTP限制
当SDK版本大于API 28时,默认限制了HTTP请求,并出现相关日志“java.net.UnknownServiceException: CLEARTEXT communication to xxx not permitted by network security policy“。
该问题有两种解决方案:
1.在AndroidManifest.xml中Application节点添加如下代码
<application android:usesCleartextTraffic="true">
2.在res目录新建xml目录,已建的跳过 在xml目录新建一个xml文件network_security_config.xml,然后在AndroidManifest.xml中Application添加如下节点代码。
//network_config.xml(命名随机)
android:networkSecurityConfig="@xml/network_config"
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
展望
2020年2月20号,Google提前发布了Android 11预览版,通过 5G、折叠屏、内置机器学习等新技术,照亮了移动设备的未来。Android 11 依然致力于让用户畅享最新科技,并始终确保将安全和隐私放在首位,帮助用户管理敏感数据和文件的访问权限。此外还对平台的关键区域做出了强化,以保持操作系统的弹性和安全性。
对于像Android这样的开放性OS来说,占有的市场份额越大,整个Android生态系统的发展会越好。随着Android对于碎片化的整理、用户隐私和安全性的重视、5G和机器学习等新技术的引入,已逐步抓住快速增长的中产阶级用户,未来的市场份额增长量将是不可预估的。