总结系列-Android10适配

Posted ZhangQiang-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了总结系列-Android10适配相关的知识,希望对你有一定的参考价值。

android10,即TargetSDK29于2019 年 9 月上线正式版, google play store要求TargetSDK29的适配,要求新产品在8月1号前完成,已有产品在11月1号前完成适配,记录. 在Android 10 版本中,某些改动较大,有一定的开发适配成本,本文主要记录一些相关适配点及部分调研情况  
  • 关于Androidx
  版本 28.0.0 是Android Support 库的最后一个版本。官方将不再发布 android.support 库版本。所有新功能都将在 AndroidX命名空间中开发。建议在升级Android10前进行Androidx的升级(非必须).Androidx升级相关此处不再赘述    

相关目录:

限制非 SDK 接口 ( 官方指导) Android 10 中的隐私权变更 ( google文档)      存储权限      定位权限     从后台启动 Activity     设备唯一标识符 深色主题 设置面板 其他相关 Android10升级可能的问题  

限制非 SDK 接口 (官方指导)

目前,开发者对于非SDK API的调用,一般是采取反射或JNI间接调用的方法进行的。由于Android是开源的,所以开发者对非公有SDK的调用十分混乱, Google为了进一步防止碎片化,规范开发者的API使用行为,于是开始限制开发者 通过反射或JNI间接调用非Android SDK提供的api。 这个变更会影响所有运行在9.0及以上的Android系统的APP,无论该APP是否已经升级了Target SDK。 几乎所有第三方SDK(包括Android support库!!)都有大量通过反射调用非SDK api的情况。通常反射调用时都有try-catch,能够保证应用不崩,但实际功能受影响的程度有待评估。 针对非 SDK 接口的限制       /    Android 10 的名单更改
名单类型 影响
greylist 灰名单,即当前版本仍能使用的非SDK接口,但在下一版本中可能变成被限制的非SDK接口 , 不用关注
greylist-max-o 受限制的灰名单。APP运行在 版本<=8.0的系统里 可以正常访问,targetSDK>8.0且运行在>8.0的手机会抛出异常。
greylist-max-p 受限制的灰名单。APP运行在 版本<=9.0的系统里 可以正常访问,targetSDK>9.0且运行在>9.0的手机会抛出异常。
blacklist 所有三方应用不允许调用。 黑名单,使用了就会报错。项目中必须解决的非SDK接口
下表说明了当您的应用尝试访问黑名单或者在高版本手机上访问受限制的灰名单中的非 SDK 接口时可能会出现的异常:
访问方式 结果
Dalvik 指令引用某个字段 抛出 NoSuchFieldError
Dalvik 指令引用某个方法 抛出 NoSuchMethodError
通过 Class.getDeclaredField() 或 Class.getField() 进行反射 抛出 NoSuchFieldException
通过 Class.getDeclaredMethod()、Class.getMethod() 进行反射 抛出 NoSuchMethodException
通过 Class.getDeclaredFields()、Class.getFields() 进行反射 结果中未获取到非 SDK 成员
通过 Class.getDeclaredMethods()、Class.getMethods() 进行反射 结果中未获取到非 SDK 成员
通过 env->GetFieldID() 进行 JNI 调用 返回 NULL,抛出 NoSuchFieldError
通过 env->GetMethodID() 进行 JNI 调用 返回 NULL,抛出 NoSuchMethodError

怎么检测出非SDK接口的使用?

方法一 veridex 工具扫描 APK

google官方也提供了官方检查器veridex用来检测一个apk中哪里使用了非SDK接口  使用  veridex 工具命令:appcompat.sh --dex-file=**.apk --imprecise ( 目前该工具推荐支持mac和linux运行,Windows须先安装WLS) 扫描示例如下:
#51: Reflection greylist-max-o Landroid/animation/TimeAnimator;->mListener potential use(s):
       Landroid/support/v4/app/FragmentManagerImpl;->getAnimationListener(Landroid/view/animation/Animation;)Landroid/view/animation/Animation$AnimationListener;
#94: Reflection greylist Lcom/android/internal/R$dimen;->status_bar_height use(s):
       Lcom/demo/common/utils/SystemUtils;->getStatusBarHeight(Landroid/app/Activity;)I
#1340: Reflection blacklist Lcom/android/org/conscrypt/SSLParametersImpl;->setApplicationProtocols potential use(s):
       Lokhttp3/internal/platform/Jdk9Platform;->buildIfSupported()Lokhttp3/internal/platform/Jdk9Platform;

107 hidden API(s) used: 24 linked against, 83 through reflection
       91 in greylist
       0 in blacklist
       1 in greylist-max-o
       15 in greylist-max-p

use(s):左边是被调用的非SDK的api,并非常详细地列出了包名、类名、函数名(or变量名),以及其所属的名单级别(greylist or blacklist)。

use(s):右边就是调用了非SDK api的项目代码的位置。 比如上面扫描结果告诉我们:
  1. #51:谷歌的Android-support库中的FragmentManagerImpl通过反射调用了greylist-max-o灰名单中的android/animation/TimeAnimator的mListener变量。 很明显,我们无法修改support库中的源码, 只能等官方自己修复
  2. #94:项目里的demo/common/utils/SystemUtils的getStatusBarHeight方法 通过反射获取了com.android.internal.R$dimen类中的status_bar_height变量,属于通过反射调用灰名单的非SDK api, 暂时是没有问题
  3. #1340:项目里的okhttp3/internal/platform/Jdk9Platform的buildIfSupported方法通过反射调用了com/android/org/conscrypt/SSLParametersImpl的setApplicationProtocols方法,属于通过反射调用 黑名单里的非SDK api。 很明显,这里我们要注意评估项目中的okhttp通过反射调用黑名单api的行为,需要评估是否会引起崩溃(有无try-catch),是否在9.0以上机型造成功能异常,这个就需要开发者 根据具体的业务情况去做具体分析了。
  扫描结果较多,但结果中重复且包含源码,三方SDK,Lint 库中黑名单API调用信息,所以只关注自己应用包名下的信息即可 几乎所有第三方SDK(包括Android support库!)都有大量通过反射调用非SDK api的情况。通常反射调用时都有try-catch,能够保证应用不崩,但实际功能受影响的程度无法评估。 所以对第三方库的适配方法是查看官网更新日志,并对第三方库进行升级(建议)。 更改限制API的调用, 如果一定使用限制API使用,做try catch 处理,并做调用失败的业务逻辑处理  

方法二 全局搜索业务代码

在veridex扫描结果中排查项目业务代码对非SDK接口的调用情况比较困难。所以可以通过最原始的办法————全局搜索 JAVA反射相关的api(搜索上面的表格里的getField、getMethod等关键字),与官方给出的hidden api list(上面提到的hiddenapi-flags.csv)做比对,就很容易排查中业务代码里对非SDK接口的调用情况。 全局搜索getField,发现项目通过反射找到对应类的对应成员变量,在官方提供的 hiddenapi-flags.csv中可以发现该成员变量是否非SDK接口,且属于受限制的灰名单,由于业务代码对反射调用进行了try-catch,所以实际业务中拿到的是一个null,这个时候我们就可以感受到谷歌官方对非SDK接口的限制,并评估具体的业务影响。 谷歌官方的建议:
如果您的应用依赖于非 SDK 接口,则应该开始计划迁移到 SDK 接口或其他替代方案。如果您无法为应用中的功能找到使用非 SDK 接口的替代方案,可以向谷歌官方 申请新的公共 API
   

Android 10 中的隐私权变更 (google文档)

 

存储权限

  Android 10 在外部存储设备中为每个应用提供了一个“隔离存储沙盒”,其访问权限范围限定为外部存储,即分区存储。 此类应用可以查看外部存储设备内* 特定于应用的目录中的文件(使用 getExternalFilesDir() 访问)。 * 应用创建的照片、视频和音频片段(通过 媒体库访问),无需请求任何与存储相关的用户权限。 任何其他应用都无法直接访问您应用的沙盒文件。此变更可让您更轻松地保证用户文件的隐私性,并有助于减少应用所需的权限数量。   简单而言就是应用专属文件夹,并且 访问这个文件夹无需权限。 谷歌官方推荐应用在沙盒内存储文件的地址为Context. getExternalFilesDir()下的文件夹。比如要存储一张图片,则应放在Context. getExternalFilesDir(Environment.DIRECTORY_PICTURES)中。 target=29时,谷歌临时允许使用老版本的权限来解决储存分区问题 在manifest application下添加 android:requestLegacyExternalStorage ="true" (注:在Android11 即targetSdkVersion = 30中不行了, 强制开启分区存储。) 处理外部存储中的媒体文件 将文件保存到外部存储 本次适配升级暂用兼容模式 :(若需具体适配看另一篇分区存储适配)   android10的时候在 targetSdkVersion = 29 应用中,设置 android:requestLegacyExternalStorage="true" ,就可以不启动分区存储,让以前的文件读取正常使用。但是 targetSdkVersion = 30 中不行了, 强制开启分区存储 当然,如果是覆盖安装呢,可以增加 android:preserveLegacyExternalStorage="true" ,暂时关闭分区存储,好让开发者完成数据迁移的工作。为什么是暂时呢?因为只要 卸载重装 ,就会失效了。 以下是关于分区存储会遇到的 所有情况 分情况运行: 1) targetSdkVersion = 28,运行后正常读写。 2) targetSdkVersion = 29,不删除应用,targetSdkVersion 由28修改到29,覆盖安装,运行后正常读写。 3) targetSdkVersion = 29,删除应用,重新运行,读写报错,程序崩溃(open failed: EACCES (Permission denied)) 4) targetSdkVersion = 29,添加android:requestLegacyExternalStorage="true"(不启用分区存储),读写正常不报错 5) targetSdkVersion = 30,不删除应用,targetSdkVersion 由29修改到30,读写报错,程序崩溃(open failed: EACCES (Permission denied)) 6) targetSdkVersion = 30,不删除应用,targetSdkVersion 由29修改到30,增加android:preserveLegacyExternalStorage="true",读写正常不报错 7) targetSdkVersion = 30,删除应用,重新运行,读写报错,程序崩溃(open failed: EACCES (Permission denied))  

定位权限

  为了让用户更好地控制应用对位置信息的访问权限  Android 10 引入了  ACCESS_BACKGROUND_LOCATION  权限(危险权限)。 与现有的 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 权限不同,新权限仅会影响应用在 后台运行时对位置信息的访问权。除非应用的某个 Activity 可见或应用正在运行前台服务(类型为 location),否则应用将被视为在后台运行。 如果应用需要在后台时也获得用户位置(比如滴滴),就需要动态申请ACCESS_BACKGROUND_LOCATION权限。   该权限允许应用程序在后台访问位置。如果请求此权限,则还必须请求 ACCESS_FINE_LOCATION    ACCESS_COARSE_LOCATION 权限。只请求此权限无效果。 当然如果不需要的话,应用就无需任何改动,且谷歌会按照应用的targetSDK作出不同处理: 在Android 10的设备上,如果你的应用的  targetSdkVersion  < 29,则在请求 ACCESS_FINE_LOCATION  ACCESS_COARSE_LOCATION 权限时, 系统会自动同时请求 ACCESS_BACKGROUND_LOCATION 。在请求弹框中,选择“始终允许”表示同意后台获取位置信息,选择“仅在应用使用过程中允许”或"拒绝"选项表示拒绝授权 如果你的应用的  targetSdkVersion  >= 29,则请求 ACCESS_FINE_LOCATION    ACCESS_COARSE_LOCATION 权限表示在前台时拥有访问设备位置信息的权限。 在请求弹框中,选择“始终允许”表示前后台都可以获取位置信息,选择“仅在应用使用过程中允许”只表示拥有前台的权限。 对应关系:  
目标平台版本 是否授予了 粗略或精确位置信息使用权限? 清单中是否 定义了后台权限? 更新后的默认权限状态
Android 10 前台和后台访问权
Android 10 仅前台访问权
Android 10 (被系统忽略) 无访问权
Android 9 或更低版本 在设备升级时由系统自动添加 前台和后台访问权
Android 9 或更低版本 (被系统忽略) 无访问权
官方 不推荐你使用申请后台访问权的方式, 推荐使用 前台服务 来实现,在前台服务中获取位置信息。 首先在清单中对应的 service 中添加  android:foregroundServiceType="location" 启动前台服务前检查是否具有前台的访问权限: boolean permissionApproved = ActivityCompat.checkSelfPermission( this , Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED);  

一些电话、蓝牙和WLAN的API需要精确位置权限

下面列举了Android 10中必须具有 ACCESS_FINE_LOCATION 权限才能使用类和方法: 电话 * TelephonyManager     * getCellLocation()     * getAllCellInfo()     * requestNetworkScan()     * requestCellInfoUpdate()     * getAvailableNetworks()     * getServiceState() * TelephonyScanManager     * requestNetworkScan() * TelephonyScanManager.NetworkScanCallback     * onResults() * PhoneStateListener     * onCellLocationChanged()     * onCellInfoChanged()     * onServiceStateChanged() WLAN * WifiManager     * startScan()     * getScanResults()     * getConnectionInfo()     * getConfiguredNetworks() * WifiAwareManager * WifiP2pManager * WifiRttManager 蓝牙 * BluetoothAdapter     * startDiscovery()     * startLeScan() * BluetoothAdapter.LeScanCallback * BluetoothLeScanner     * startScan()  

从后台启动 Activity

  从 Android 10 开始,系统会增加针对从后台启动 Activity 的限制, 简单解释就是 应用处于后台时,无法启动Activity 。比如点开一个应用会进入启动页或者广告页,一般会有几秒的延时再跳转至首页。如果这期间你退到后台,那么你将无法看到跳转过程。   而在之前的版本中,会强制弹出页面至前台。但下情景除外 * 应用具有可见窗口,例如前台 Activity。 * 应用在前台任务的返回栈中已有的 Activity。 * 应用在 Recents 上现有任务的返回栈中已有的 Activity。Recents 就是我们的任务管理列表。 * 应用收到系统的 PendingIntent 通知。 * 应用收到它应该在其中启动界面的系统广播。示例包括 ACTION_NEW_OUTGOING_CALL 和 SECRET_CODE_ACTION。应用可在广播发送几秒钟后启动 Activity。 * 用户已向应用授予 SYSTEM_ALERT_WINDOW 权限,或是在应用权限页开启后台弹出页面的开关。 因为此项行为变更适用于在 Android 10 上运行的所有应用,所以这一限制导致最明显的问题就是点击推送信息时,有些应用无法进行正常的跳转(具体的实现问题导致) 针对这类问题,可以采取 PendingIntent 的方式,发送通知时使用 setContentIntent 方法。   对于全屏 intent ,注意设置最高优先级和添加 USE_FULL_SCREEN_INTENT 权限,这是一个普通权限。比如微信来语音或者视频通话时,弹出的接听页面就是使用这一功能。 权限需清单文件注册,系统自动授权,调用全屏通知会有以下两种场景: 1.如果用户设备被锁定,会显示全屏 Activity,覆盖锁屏。 2.如果用户设备处于解锁状态,通知以展开形式显示,其中包含用于处理或关闭通知的选项。  

设备唯一标识符

从Android 10开始已经无法完全标识一个设备,曾经用mac地址、IMEI等设备信息标识设备的方法,从Android 10开始统统失效。而且无论你的APP是否适配过Android 10。 从Android10开 始普通应用不再允许请求权限 android.permission.READ_PHONE_STATE。而且,无论你的App是否适配过Android 10(既targetSdkVersion是否大于等于29),均无法再获取到设备IMEI等设备信息。 受影响的API:

IMEI等设备信息

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地址)

如何标识设备唯一性 

1 Google解决方案:(google- 唯一标识符最佳做法 ) 如果您的应用有追踪非登录用户的需求,可用ANDROID_ID来标识设备。
  • ANDROID_ID生成规则:签名+设备信息+设备用户
  • ANDROID_ID重置规则:设备恢复出厂设置时,ANDROID_ID将被重置
String androidId = Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID); 信通院统一SDK(OAID)( 通过以上方法获取到OAID等设备标识之后,即可作为唯一标识使用 ) Android 10 应用无法访问 /proc/net,不可重置标识符。如果需要IMEI 和序列号 作为唯一标,应用必须具有 READ_PRIVILEGED_PHONE_STATE 签名权限才能访问设备的不可重置标识符(包含 IMEI 和序列号)。 许多用例不需要不可重置的设备标识符。如果您的应用没有该权限,但您仍尝试查询标识符的相关信息。会返回空值或报错。
  • 如果应用以 Android 10 或更高版本为目标平台,则会发生  SecurityException
  • 如果应用以 Android 9(API 级别 28)或更低版本为目标平台,则相应方法会返回  null  或占位符数据(如果应用具有  READ_PHONE_STATE  权限)。否则,会发生  SecurityException
  设备唯一标识符需要特别注意,原来的READ_PHONE_STATE权限已经不能获得IMEI和序列号,如果想在10设备上通过 ((TelephonyManager) getActivity()      .getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId() 获得设备ID,会返回空值(targetSDK<=P)或者报错(targetSDK==Q)。且官方所说的READ_PRIVILEGED_PHONE_STATE权限 只提供给系统app从 Google Play 商店安装的第三方应用无法声明特许权限 所以这个方法算是废了。 谷歌官方给予了 设备唯一ID最佳做法,但是此方法给出的ID可变,可以按照具体需求具体解决。      

无线扫描权限

启用和停用 WLAN   :   

无法启用或停用 WLAN  , WifiManager.setWifiEnabled()  在Android10 始终返回false ,如果需要操作,需引导开启 设置界面 直接访问已配置的 WLAN 网络实施了限制 :  应用不是系统应用或 DPC,则下列方法不会返回有用数据: * getConfiguredNetworks() 方法始终返回空列表。 注意:如果运营商应用调用 getConfiguredNetworks(),则系统返回的列表仅包含运营商配置的网络。 * 每个返回整数值的网络操作方法(addNetwork() 和 updateNetwork())始终返回 -1。 * 每个返回布尔值的网络操作(removeNetwork()、reassociate()、enableNetwork()、disableNetwork()、reconnect() 和 disconnect())始终返回 false。    

深色主题

Android 10 新增了一个系统级的深色主题(在系统设置中开启)。虽然深色主题并 不是强制适配项 ,但是它可以带给用户更好的体验:

1.手动适配(资源替换)

官方文档中提到的继承Theme.AppCompat.DayNight 或者 Theme.MaterialComponents.DayNight的方法,但这只是将我们使用的各种View的 默认样式进行了适配,并不太适用于实际项目的适配。 配的方法很简单,类似屏幕适配、国际化的操作,并不需要继承上面的主题。比如你要修改颜色,就在 res  下新建  values-night 目录,创建对应的 colors.xml 文件 。将具体要修改的色值定义在里面。图标之类的也是一个思路,创建对应的  drawable-night 目录。

2.自动适配(Force Dark)

应用必须选择启用 Force Dark,方法是在其主题背景中设置  android:forceDarkAllowed="true" 您可以通过  android:forceDarkAllowed  布局属性或  setForceDarkAllowed(boolean)  在特定视图上控制 Force Dark。( 如果使用的是  DayNight    Dark Theme  主题,则设置 forceDarkAllowed  不生效 ) 设置的色值会自动取反。但也因此颜色不受控制,能否达到预期效果是个需要注意的问题。追求快速适配可以采取此方案。 判断深色主题是否开启 int currentNightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;  

设置面板

Android 10  引入了“设置面板”,让应用能够在自身环境中向用户显示设置。这样一来,用户无需通过转到 设置 来更改  NFC  或 移动数据 等设置,而能继续留在原来的应用中。 要显示设置面板,请发出具有以下一个  Settings.Panel  操作的 intent: Intent panelIntent = new Intent(Settings.Panel.settings_panel_type); startActivityForResult(panelIntent); ACTION_INTERNET_CONNECTIVITY  显示与互联网连接相关的设置,例如飞行模式、WLAN 和移动数据。 ACTION_WIFI  显示 WLAN 设置,但不显示其他连接设置。这对于需要 WLAN 连接以执行大容量上传或下载的应用非常有用。 ACTION_NFC  显示与近距离无线通信 (NFC) 相关的所有设置。 ACTION_VOLUME  显示所有音频流的音量设置。  

其他相关

webview相关  webview loadData不展示,通过loadUrl的方式可以,10以后 loadData需要base64数据处理 Glide 加载本地图片   直接加载file出问题,须转换为URI方式 明文HTTP限制 当SDK版本大于API 28时,默认限制了HTTP请求 在AndroidManifest.xml中Application节点添加如下代码  <application android:usesCleartextTraffic="true"> 限制了对剪贴板数据的访问权限  :  除非您的应用是默认输入法 (IME) 或是目前处于焦点的应用,否则它无法访问 Android 10 或更高版本平台上的剪贴板数据。暂无解决方案 手势导航:       您需要将应用内容扩展到屏幕边缘,并适当地处理存在冲突的手势( 屏幕边缘向内-返回)    手势导航 文档 应用使用:       灰度模式 ; 干扰模式(禁止显示其通知,并且不会将其显示为推荐的应用) 启用和停用 WLAN   :   无法启用或停用 WLAN  ,若需要 请使用 设置面板 WLAN 直连广播不生效 Android10上对折叠屏设备有了更好的支持, 有折叠屏适配的需求,可以参看 为可折叠设备构建应用    华为折叠屏应用开发指导 暂停和播放:      在 Android 10 中,暂停的应用无法播放音频。    

Android10升级可能的问题(收集)

  问题:一个页面通过webview展示的图片不展示 解决过程:发现通过loadData不展示,通过loadUrl的方式可以,后来发现10以后loadData需要base64数据处理 if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.Q)             String newhtml_code = Base64.encodeToString(htmlStr.getBytes(), Base64.NO_PADDING);             webview.loadData(newhtml_code,"text/html", "base64"); else            webview.loadData(htmlStr, "text/html; charset=UTF-8", null);   Activity透明相关,windowIsTranslucent属性 如果你要显示一个半透明的Activity,这在android10之前普通样式Activity只需要设置windowIsTranslucent=true即可,但是到了Android10,它没有效果了,而且如果动态设置View.setVisibility(),界面还会出现残影... 解决办法:使用Dialog样式Activity,且设置windowIsFloating=true,此时问题又来了,如果Activity根布局没有设置fitsSystemWindow=true,默认是没有侵入状态栏的,使界面看上去正常。   第三方分享图片等操作 直接使用文件路径的,图片分享,都需要注意,这是不可行的,都只能通过MediaStore等API,拿到Uri来操作    
 

相关参考链接:

Google 文档 Android10 行为变更  /  迁移指南 Android 10 适配攻略 Android10 文件存储相关 记录项目升级androidX+API29的各种坑 SAF(Storage Access Framework)使用攻略     携程Android 10适配踩坑指南  / 简书   // 华为文档 Android 10分区存储介绍及 百度APP适配实践 Android10填坑适配指南,实际经验代码 Android 10 更新内容与适配( 关于Android 10.0适配,看这篇就够了 适配Android P之非SDK接口限制的排查方法 拖不得了,Android11 最全 适配实践 Android11(30)/Android10(29)分区存储-适配方案

以上是关于总结系列-Android10适配的主要内容,如果未能解决你的问题,请参考以下文章

总结系列-Android10适配-分区存储

总结系列-Android10适配

总结系列-Android10适配

Android深色模式适配-想法1.0

Android 10 更新内容与适配

Android 10 更新内容与适配