AndroidP适配技术指导
1 背景说明
Android P版本已经在2018-8-7正式发布,P版本中伴随很多机制和新增特性的改变,对第三方应用带来了很多兼容性问题。本文档主要介绍了谷歌P版本新增的特性和变更,以及三方如何适配。
1.1 P版本适配步骤建议
- 下载手机镜像,刷机
- 开发环境和SDK:https://developer.android.com/studio/
- 设置应用的targetSdkVersion和compileSDKVersion:
- compileSdkVersion 28
- targetSdkVersion 28
- 阅读P的应用行为变更,在应用中进行需要的调整
- 阅读P的新功能,在应用中合理使用
1.2 刷机指导
1.2.1 版本下载
参考:https://developers.google.com/android/images,在该页面可以下载对应支持P版本的手机版本;
1.2.2 刷机方法
刷手机的方法(刷机前需要退出之前登录的谷歌账号):
- 下载对应设备的压缩包并解压;
- 手机连上USB后,执行adb reboot bootloader命令;
- 等手机出现小机器人以后,并且显示‘unlock’字样时,执行压缩包中的bat脚本,脚本执行完成即可。
- 如果是“lock”字样,请参考:https://source.android.com/source/running#unlocking-the-bootloader,在刷机之前先对手机进行解锁操作:
- 在设备上启用 OEM 解锁:
- 在“设置”中,点按关于手机,然后点按版本号七 (7) 次。
- 当看到“您已处于开发者模式”这条消息后,点按返回按钮。
- 点按开发者选项,然后启用 OEM 解锁和 USB 调试。(如果“OEM 解锁”处于停用状态,请连接到互联网,以便设备可以至少签到一次。如果“OEM 解锁”仍处于停用状态,则说明您的设备可能已被运营商锁定 SIM 卡,系统无法解锁引导加载程序。)
- 重新启动进入引导加载程序,然后使用 fastboot 解锁。
- 对于新款设备(2015 年及之后发布的设备):
fastboot flashing unlock
- 对于老款设备(2014 年及之前发布的设备):
fastboot oem unlock
- 在屏幕上确认解锁。
1.3 华为终端开放实验室Android P 版本兼容性测试
开发者如果没有Pixel真机并且不喜欢谷歌模拟器调试,也可以选择华为终端开放实验室提供的远程真机来进行P版本测试和调试。请参考链接:华为终端开放实验室Android P 版本兼容性测试上线。
2 非SDK接口管控
2.1 背景介绍
2.1.1 非SDK接口的定义
- SDK接口:https://developer.android.com/reference/packages,谷歌这个网站能查到的接口都是SDK接口;
- 非SDK接口:除了谷歌开放的SDK接口之外的其他JAVA接口都是非SDK接口
2.1.2 应用滥用非SDK接口的危害
这些非SDK接口在大版本之间的变化可能很频繁,带来兼容性问题。
2.1.3 非SDK接口管控名单:
- 名单说明:
|
- 名单查看:
https://android.googlesource.com/platform/frameworks/base/+/master/config/
2.2.1 影响范围2.2 兼容性影响
所有三方应用都可能会受到影响
2.2.2 具体影响
在Developer Preview的后续版本中,访问non-SDK接口的各种方法都会产生错误或其他不良结果:
2.3 适配指导
2.3.1 获取应用使用的所有非SDK接口列表
- 方法1:动态运行,通过日志分析扫描获取
通过自动化,或者是人工测试遍历应用每一个界面和所有功能,然后抓日志分析调用的所有非SDK接口,关键日志:
- Accessing hidden field Landroid/os/Message;->flags:I (light greylist, JNI)
- Accessing hidden method Landroid/app/ActivityThread;->currentActivityThread()Landroid/app/ActivityThread; (dark greylist, reflection)
- Accessing hidden method Landroid/app/ActivityThread;->currentActivityThread()Landroid/app/ActivityThread; (blacklist, reflection)
- 方法2:静态扫描,通过谷歌提供的veridex扫描工具扫描获取
- 工具路径:art/tools/veridex/
- 工具编译:make appcompat
- 工具执行:./art/tools/veridex/appcompat.sh --dex-file=test.apk
- 输出扫描结果:
备注:使用了加固的应用,需要使用加固前的apk进行扫描
- 工具获取
途径1:谷歌开放了veridex静态扫描工具:
https://android.googlesource.com/platform/prebuilts/runtime/+/master/appcompat/
途径2:下载源码编译生成工具
https://source.android.google.cn/setup/downloading
2.3.2 非SDK接口整改
浅灰名单的非SDK接口目前还不需要整改,P版本可以正常使用,三方应用需要重点关注深灰名单和黑名单的非SDK接口,需要找可替代的SDK接口进行适配。
2.3.3 无法整改的接口给谷歌提单申请加浅灰名单
如果三方发现调用了无法替代的非SDK接口在深灰名单或者黑名单中,可向谷歌提供必需使用此接口的详细原因,申请重新评估该接口,问题反馈链接:https://partnerissuetracker.corp.google.com/issues/new?component=328403&template=102726
问题反馈邮件模板:
3 电源管理
3.1 背景介绍
3.1.1 应用待机群组
Android 9 引入了一项新的电池管理功能,即应用待机群组。 应用待机群组可以基于应用最近使用时间和使用频率,帮助系统排定应用请求资源的优先级。 根据使用模式,每个应用都会归类到五个优先级群组之一中。系统将根据应用所属的群组限制每个应用可以访问的设备资源。
- 五个群组按照以下特性将应用分组:
|
系统会动态地将每个应用归类到某个优先级群组,并根据需要重新归类。 系统可能会依靠某个使用机器学习的预加载应用确定每个应用的使用可能性,并将应用归类到合适的群组。 如果设备上不存在系统应用,系统默认将基于应用的最近使用时间对它们进行排序。 更为活跃的应用将被归类到为应用提供更高优先级的群组,从而让应用可以使用更多系统资源。 具体而言,群组决定应用运行作业的频率,应用可以触发报警的频率,以及应用可以接收高优先级 Firebase 云信息传递 (FCM) 消息的频率。 这些限制仅在设备使用电池电量时适用,如果设备正在充电,系统不会对应用施加这些限制。分类规则:
- 每个制造商都可以设定自己的标准来归类非活跃应用。
- 具体分类管控规则:
|
3.1.2 省电模式改进
Android 9 对省电模式进行了多处改进。 设备制造商可以决定施加的确切限制。 例如,在 AOSP 构建中,系统会应用以下限制:
- 系统会更积极地将应用置于应用待机模式,而不是等待应用空闲。
- 后台执行限制适用于所有应用,无论它们的目标 API 级别如何。
- 当屏幕关闭时,位置服务可能会被停用。
- 后台应用没有网络访问权限。
3.1.3 P版本新增FAS管控
用户设置限制后台活动之后的限制:
- 应用退后台,1分钟就会被停止Service(包括正在执行的前台任务,前台服务)
- 限制访问网络
- 限制Alarm触发
- 限制JobScheduler执行
3.1.4 后台执行限制(非P特性)
谷歌海外TargetSdkVersion策略要求,应用必须升级TargetSdkVersion到26+才允许上架google play,所以后台执行限制这个针对TargetSdkVersion>=26生效的功耗管控特性需要应用在P版本手机重点关注:
https://developer.android.com/about/versions/oreo/background。
- 后台执行限制管控内容:
- TargetSdkVersion>=26的应用将受到后台服务管控:应用处于前台时,可以自由创建和运行前台服务与后台服务。 进入后台时,在一个持续数分钟的时间窗内,应用仍可以创建和使用服务。在该时间窗结束后,应用将被视为处于空闲 (idle)状态。 此时,系统将停止应用的后台服务,就像应用已经调用服务的“stopSelf()”方法,另外应用处于空闲(idle)状态将不再允许应用再启动服,可以参考下面的日志:
08-25 14:13:23.377 1482 17299 W ActivityManager: Background start not allowed: service Intent {…
08-25 14:13:24.246 1482 3255 W ActivityManager: Service.startForeground() not allowed due to bg restriction:
- TargetSdkVersion>=26的应用将受到广播管控:应用无法收到通过静态注册的隐式广播,动态注册方式不受影响,显示广播不受影响
- 判断应用是否处于前台的条件:
- 具有可见 Activity(不管该 Activity 已启动还是已暂停)。
- 具有前台服务。
- 另一个前台应用已关联到该应用(不管是通过绑定到其中一个服务,还是通过使用其中一个内容提供程序)。 例如,如果另一个应用绑定到该应用的服务,那么该应用处于前台:
- IME
- 壁纸服务
- 通知侦听器
- 语音或文本服务
3.1.5 应用待机模式(非P特性)
应用待机模式允许系统判定应用在用户未主动使用它时处于空闲状态(idle)。当用户有一段时间未触摸应用时,系统便会作出此判定。如果拔下了设备电源插头,系统会为其视为空闲的应用停用网络访问以及暂停同步和作业。
3.1.6 低电耗模式(非P特性)
我们在P版本测试发现还是有很多应用没有适配谷歌的低电耗模式,导致手机进入低电耗模式之后,应用的某些后台功能受到影响,比如后台下载、播放音乐和收消息等等,并且P版本进入低电耗模式的时间由原来的15分钟缩短到5分钟,没有适配的应用更容易出现问题。
参考链接:https://developer.android.com/training/monitoring-device-state/doze-standby
- 进入低电耗模式条件:用户设备未插接电源、处于静止状态一段时间且屏幕关闭,设备会进入低电耗模式,O版本进入低电耗模式时间是15分钟左右,P版本减少到5分钟左右;
- 低电耗模式限制:
|
3.2 兼容性影响
除了后台执行限制管控是对TargetSdkVersion>=26生效以外,上面介绍的其他变化适用于所有应用,无论它们是否以 Android 9 为目标。
3.2.1 具体影响
影响应用后台活动包括:应用后台下载、播放音乐,收消息等等功能
3.3 适配指导
3.3.1 前台服务
应用如果拥有前台服务的话,可以豁免低电耗、应用待机群组以及O版本新增的后台执行限制一系列功耗方案的管控,但是在P版本如果用户开启了应用的后台限制,有前台服务的应用也是会受到影响的:
在 Android 8.0 之前,创建前台服务的方式通常是先创建一个后台服务,然后将该服务推到前台。Android 8.0 有一项复杂功能;系统不允许后台应用创建后台服务。 因此,Android 8.0 引入了一种全新的方法,即 Context.startForegroundService(),以在前台启动新服务。在系统创建服务后,应用有五秒的时间来调用该服务的 startForeground() 方法以显示新服务的用户可见通知。如果应用在此时间限制内未调用startForeground(),则系统将停止服务并声明此应用为 ANR。
3.3.2 Doze白名单
通过Doze白名单豁免所有的功耗方案管控:
- 加入doze白名单:
用户可以在 Settings > Battery > Battery Optimization 中手动配置该白名单。或者,系统会为应用提供请求用户将应用加入白名单的方式。
- 应用可以触发 ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS Intent,让用户直接进入 Battery Optimization,他们可以在其中添加应用。
- 具有 REQUEST_IGNORE_BATTERY_OPTIMIZATIONS 权限的应用可以触发系统对话框,让用户无需转到“设置”即可直接将应用添加到白名单。应用将通过触发 ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS Intent 来触发该对话框。
- 用户可以根据需要手动从白名单中移除应用。
- 用户授权加入doze白名单参考实现代码:
- 申请使用权限:
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
- 弹出用户授权对话框代码:
/** * 忽略电池优化 */ public void ignoreBatteryOptimization(Activity activity) { PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); boolean hasIgnored = powerManager.isIgnoringBatteryOptimizations(activity.getPackageName()); // 判断当前APP是否有加入电池优化的白名单,如果没有,弹出加入电池优化的白名单的设置对话框。 if (!hasIgnored) { Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); intent.setData(Uri.parse("package:" + activity.getPackageName())); startActivity(intent); } }
3.3.3 功耗调试命令,多多测试验证
- Doze调试
- 查看服务是否是前台服务
adb shell dumpsys activity processes 应用包名
- 应用待机群组调试
https://developer.android.com/about/versions/pie/power#adb-commands
4 刘海屏
4.1 背景介绍
谷歌对于刘海屏的设计约束:
- 刘海区域不能超出系统状态栏的显示区域,刘海高度<=系统状态栏高度
- 允许屏幕上有多个刘海,但不支持单边有多个刘海
- 刘海位置可以在屏幕的短边、角上、或者长边。如果刘海在角上,系统认为是在短边
- 支持应用界面延伸到短边的刘海区域内。不支持延伸到长边的刘海区域内,长边刘海区留黑
4.2 刘海屏的API介绍
4.2.1 窗口对刘海区域使用申明API:layoutInDisplayCutoutMode
- 默认值:LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
- 说明:应用不使用 API 或使用默认 API,系统将根据窗口类型自动决策应用是否使用刘海区域,如应用为全屏应用(设置了FLAG_FULLSCRREN 或 SYSTEMUI_FLAG_FULLSCREEN)将不使用,否则使用。
- 系统默认处理规则图示:
全屏界面不使用刘海区 有状态栏非全屏界面使用刘海区显示
横屏界面统一不使用刘海区显示
- 应用申明使用刘海区显示:LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
- 说明:窗口申明使用短边刘海区域
注:P版本初期提供 LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS 标记,但 DP2 升级时,增加了对多个刘海的支持,Google 只允许应用使用短边刘海区,不支持应用使用长边刘海区。
- 参考代码:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { WindowManager.LayoutParams lp = getWindow().getAttributes(); lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; getWindow().setAttributes(lp); }
- 设置使用刘海区显示的影响:对于所有全屏页面和所有横屏页面是有影响的,设置之后这些页面才能使用刘海区显示
设置默认值 设置使用值
设置默认值 设置使用值
- 应用申明不使用刘海区显示:
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
- 说明:窗口声明永远不使用刘海区域
- 设置不使用刘海区显示的影响:主要影响使用沉浸式状态栏和导航栏的页面有影响,这种场景应用的布局也是不使用刘海区显示的
默认设置 设置不使用刘海区显示
默认和设置不使用刘海应用布局可显示区域大小是不一样的
4.2.2 刘海设备及刘海区域API
- 判断是否为刘海设备方法
DisplayCutout cutout = windowInsets.getDisplayCutout(); if (cutout == null) { Log.e(TAG, "cutout==null, is not notch screen"); } else { Log.e(TAG, "cutout!=null, is notch screen"); }
- 获取刘海尺寸接口和安全显示区域接口
|
备注:刘海个数可以是多个:
Line 6291: 05-24 11:27:04.517 11036 11036 E Cutout_test: rect size:2
Line 6292: 05-24 11:27:04.517 11036 11036 E Cutout_test: cutout.getSafeInsetTop():84, cutout.getSafeInsetBottom():84, cutout.getSafeInsetLeft():0, cutout.getSafeInsetRight():0, cutout.rects:Rect(351, 0 - 729, 84),cutout.rects:Rect(351, 1836 - 729, 1920)
4.3 适配指导
4.3.1 使用非刘海屏手机模拟调试
Pixel非刘海屏手机或者模拟器,通过开发者选项开启模拟刘海屏进行调试:
- 在开发人员选项屏幕中,向下滚动到绘图部分,然后选择模拟“刘海屏”。
- 选择刘海尺寸信息
4.3.2 应用界面需要使用刘海区显示适配指导
- 页面设置全屏属性SYSTEM_UI_FLAG_FULLSCREEN和SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN,并设置LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES使用刘海区显示
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { WindowManager.LayoutParams lp = getWindow().getAttributes(); lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; getWindow().setAttributes(lp); }
备注:应用如果想让布局延伸到刘海区显示,除了设置LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES属性使用刘海区显示,还需要设置SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
没有设置SYSTEM_UI_FLAG_LAYOUT_FULLSCREE
设置SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- 获取布局安全区域,并调整布局,除背景外的其他布局信息放在安全区域内显示:
contentView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { @Override public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { DisplayCutout cutout = windowInsets.getDisplayCutout(); if (cutout == null) { Log.e(TAG, "cutout==null, is not notch screen"); } else { List<Rect> rects = cutout.getBoundingRects(); if (rects == null || rects.size() == 0) { Log.e(TAG, "rects==null || rects.size()==0, is not notch screen"); } else { contentView.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); } } } return windowInsets; } });
布局原则:页面背景可以延伸到刘海区危险区域显示,其他布局建议全部放在安全区域布局显示,因为刘海的位置不是固定的,从下图可以看到只有布局放在安全区域(橙色边框标示的区域)显示才能保证不被刘海遮挡。
- 适配使用刘海区显示,考虑华为独有隐藏刘海的需求
- 隐藏开关打开之后,显示规格:
- 读取开关状态调用范例:
public static final String DISPLAY_NOTCH_STATUS = "display_notch_status"; int mIsNotchSwitchOpen = Settings.Secure.getInt(getContentResolver(),DISPLAY_NOTCH_STATUS, 0);//0表示“默认”,1表示“隐藏显示区域”
- 华为手机横屏页面适配和谷歌原生差异的说明:
在华为刘海屏手机应用适配使用刘海区显示,竖屏页面和谷歌原生适配没有任何区别,但是应用的横屏页面判断是否需要设置布局显示在安全区域判断条件要修改为:手机为刘海屏手机并且隐藏开关未打开,参考代码:
mIsNotchSwitchOpen = Settings.Secure.getInt(getContentResolver(),DISPLAY_NOTCH_STATUS, 0); contentView = findViewById(R.id.safearea); contentView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { @Override public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { DisplayCutout cutout = windowInsets.getDisplayCutout(); if(cutout != null && mIsNotchSwitchOpen == 0) {//手机是刘海屏&&隐藏刘海开关关闭 contentView.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(), cutout.getSafeInsetRight(), cutout.getSafeInsetBottom()); } } return windowInsets; } });
非刘海屏布局 刘海屏布局
非刘海屏布局在隐藏刘海功能开启之后的效果 刘海屏布局在隐藏刘海功能开启之后的效果
- 适配使用刘海区显示,应用页面如果支持屏幕旋转,需要考虑横竖屏0度、90度、180度、270度的场景,请旋转手机在这四个场景都测试一下看看布局是否存在遮挡问题;
4.3.3 应用界面不想使用刘海区显示适配指导
应用布局如果不需要延伸到刘海区域显示也是需要适配的,需要考虑非刘海屏和刘海屏手机的差异,否则很容易出现各种UI或者功能问题。
- 应用全屏页面window坐标原点的差异
- 非刘海屏,应用全屏页面的window坐标系的坐标原点和整个手机屏幕的坐标系一致,都是(0,0)
(0,0) |
(0,y) |
刘海屏,应用全屏页面的window坐标系的坐标原点因为系统特殊下移处理发生了变化,已经不再是(0,0),而是(0,y)---y表示偏移量:状态栏高度- 适配指导:在刘海屏手机处理坐标位置的时候,需要考虑坐标系统一,要么都用window的坐标系,要么都用屏幕坐标系,否则计算结果可能存在一个状态栏高度的偏差。
|
- 状态栏高度的差异
不能再假设系统状态栏是固定高度,刘海屏的手机系统状态栏的高度和刘海的高度是有关系的,需要满足:系统状态栏高度>=刘海高度。所以应用在使用沉浸式状态栏的时候,不能把状态栏高度写死,否则就会出现一些UI问题:
计算系统状态栏高度代码:
public static int getStatusBarHeight(Context context) { int result = 0; int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { result = context.getResources().getDimensionPixelSize(resourceId); } return result; }
- 计算屏幕分辨率的差异
DisplayMetrics dm = getResources().getDisplayMetrics(); int w_screen = dm.widthPixels; int h_screen = dm.heightPixels;
|
5 支持Heif格式图片
5.1 背景介绍
- P版本支持Heif格式的软解码和软编码,系统图库支持Heif格式图片的显示,但是P版本相机不支持拍照生成Heif格式的照片
- 目前Heif图片的主要来源于苹果iPhone手机,通过手机克隆,备份恢复软件(网盘)等途径转移到安卓手机
- Heif和JPG等其他传统格式最主要的可感知的体验差别:Heif文件大小更小,同等图片质量压缩率是JPEG的39倍,可节省约50%空间,节省网络传输流量
5.2 兼容性影响
如果应用没有适配支持Heif,就会出现下面的这些问题,特别是社交类应用:
- Heif文件提示打开失败,显示异常
- 无法直接通过三方社交软件发送本地Heif图片给好友
- P版本和P之前的版本Heif图片的兼容问题
5.3 适配指导
- 判断手机是否支持Heif
public static boolean isSupportHeif() { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { return true; } return false; }
- 读取和显示Heif图片文件(通过P版本新增的ImageDecoder)
public static Drawable getHeifImageFromSdcardUseImageDecoder(String path) throws IOException { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { File file = new File(path); ImageDecoder.Source source = ImageDecoder.createSource(file); return ImageDecoder.decodeDrawable(source); } else { return null; } }
- 读取和显示Heif图片文件(通过BitmapFactory)
public static Bitmap getHeifImageFromSdcardUseBitmapFactory(String path) { return BitmapFactory.decodeFile(path); }
- 图片扫描推荐方式:ContentProvider
通过Media的ContentProvider获取手机本地图片列表方法的好处是,不需要判断手机是否支持Heif,支持Heif的手机会返回Heif图片列表,不支持Heif的手机不会返回Heif图片列表:
Cursor cursor = context.getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null); while (cursor.moveToNext()) { String path = cursor.getString(cursor .getColumnIndex(MediaStore.Images.Media.DATA)); Log.e(TAG, "image path:" + path); }
- Heif文件后缀:.heif和.heic
- Heif转JPG:需要用到该转换的场景就是对于不支持Heif的手机的兼容处理
public static void convertHeifToJpg(String path, String jpg_name){ try { Bitmap bmp = HeifUtils.getHeifImageFromSdcardUseBitmapFactory(path); File file = new File("/sdcard/" + jpg_name); FileOutputStream out = new FileOutputStream(file); bmp.compress(Bitmap.CompressFormat.JPEG, 100, out); out.flush(); out.close(); } catch (IOException e) { e.printStackTrace(); } }
6 Apache HTTP 客户端弃用
6.1 背景介绍
在 Android 6.0 中,谷歌取消了对 Apache HTTP 客户端的支持。
此变更对大多数不以 Android 9 或更高版本为目标的应用没有任何影响。 不过,此变更会影响使用非标准 ClassLoader结构的某些应用,即使这些应用不以 Android 9 或更高版本为目标平台。
如果应用使用显式委托到系统 ClassLoader 的非标准 ClassLoader,则应用会受到影响。 在 org.apache.http.* 中查找类时,这些应用需要委托给应用 ClassLoader。 如果它们委托给系统 ClassLoader,则应用在 Android 9 或更高版本上将失败并显示 NoClassDefFoundError,因为系统 ClassLoader 不再识别这些类。 为防止将来出现类似问题,一般情况下,应用应通过应用 ClassLoader 加载类,而不是直接访问系统 ClassLoader。
6.2 兼容性影响
- 所有的targetSdkVersion>=P的应用不适配的话,继续按照之前的方式使用apache http客户端会导致应用因为找不到apache http类抛异常崩溃;
- 小部分targetSdkVersion<P的应用,如果应用使用了非标准的classloader,不适配的话也是会导致闪退的问题。
异常日志:
08-24 10:47:10.455 4364 4364 E AndroidRuntime: java.lang.NoClassDefFoundError: Failed resolution of: Lorg/apache/http/client/methods/HttpGet;
6.3 适配指导
6.3.1 继续使用apache http客户端
- targetSdkVersion<p的应用,如果测试发现有该问题,可能是显示指定了系统的ClassLoader去查找apache-http类,但是系统的ClassLoader已经找不到apache的类了,所以报错。所以建议应用如果还需要继续使用apache-http的类,不要显示指定系统的ClassLoader去加载apache-http的类,通过应用的ClassLoader去加载是没有问题的。
- targetSdkVersion>=P的应用:
对于targetSdkVersion>=P的应用如果想继续使用apache-http客户端:
- 为了能编译通过需要在gradle 文件中声明以下编译时依赖项:
android { useLibrary 'org.apache.http.legacy' }
- 需要在应用的xml文件中添加:
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
注意:对于最低SDK为23或更低的应用程序,android:required =“false”属性是必需的,因为在API等级低于24的设备上,org.apache.http.legacy库不可用。 (在这些设备上,Apache HTTP类在bootclasspath上可用。)
6.3.2 不再使用apache-http客户端
使用HttpURLConnection替代apache-http
7 蓝牙后台扫描管控
7.1 背景介绍
减少功耗,减少后台占用CPU,提供更加智能的扫描策略来降低功耗,对后台应用、灭屏场景下的扫描作限制,提供更加低功耗的扫描间隔和窗口。
7.2 兼容性影响
- 影响APP范围:没有设置过滤条件的所有应用
private void startScan(List<ScanFilter> filters, ScanSettings settings, final ScanCallback callback, List<List<ResultStorageDescriptor>> resultStorages)
第一个参数(ScanFilter):筛选条件,可以通过设置过滤器的mDeviceName、mDeviceAddress、mServiceUuid等作为过滤条件进行过滤。也就是如果第一个参数传入的是null,该应用会受该特性影响。
- 管控措施:
后台:降低扫描的空占比;
灭屏:禁止蓝牙扫描。
7.3 适配指导
发起蓝牙扫描的时候添加过滤条件。
8 Art的CDEX新特性
8.1 背景介绍
- 新增dex2oat参数:--compact-dex-level=<none/fast>,其中:
- none:不生成CompactDex
- fast:生成CompactDex,并会做一些优化
需要注意的是:
为了不影响应用安装时间,PMS在应用安装的时候传递的是--compact-dex-level=none,即不做cdex编译
- 使用CompactDex编译的场景:
只要dex2oat时没有指定--compact-dex-level=none,就都会采用cdex编译,目前看到的只有在前台job中(比如应用安装/BOOT_COMPLETE时触发的编译JOB/hota升级)才会指定 --compact-dex-level=none参数。
当前已知的有会做Compact dex编译场景:
- 后台优化(充电灭屏71分钟)
- 动态加载
- 实际压缩效果:
8.2 兼容性影响
对加固应用有影响,加固动态加载以及后台优化之后都会触发cdex,加固方案不适配cdex,可能出现兼容性问题
8.3 测试建议
- 强制触发后台优化:
- adb shell cmd package compile -m speed -f my-package
- 触发后台优化会做cdex,需要多多测试
9 限制内联方法不能跨dex
9.1 背景介绍
Google在P版本新增检测:在resolve inline method的时候,如果检测到caller 与 callee处于不同的dex file, 就会主动发起abort(inline不允许跨dex文件),导致应用crash。
- 谷歌具体修改:
commit 7c947996ca9426a8ae3abf9feb110f166d8f11da
修改文件:art/runtime/entrypoints/entrypoint_utils-inl.h
if (UNLIKELY(inlined_method->GetDexFile() != method->GetDexFile())) { // TODO: We could permit inlining within a multi-dex oat file and the boot image, // even going back from boot image methods to the same oat file. However, this is // not currently implemented in the compiler. Therefore crossing dex file boundary // indicates that the inlined definition is not the same as the one used at runtime. LOG(FATAL) << "Inlined method resolution crossed dex file boundary: from " << method->PrettyMethod() << " in " << method->GetDexFile()->GetLocation() << "/" << static_cast<const void*>(method->GetDexFile()) << " to " << inlined_method->PrettyMethod() << " in " << inlined_method->GetDexFile()->GetLocation() << "/" << static_cast<const void*>(inlined_method->GetDexFile()) << ". " << "This must be due to duplicate classes or playing wrongly with class loaders"; UNREACHABLE(); }
- 问题定位关键日志:This must be due to duplicate classes or playing wrongly with class loaders
9.2 兼容性影响
测试发现主要影响使用了热修复框架的三方应用,目前已经发现下面几类问题:
1. 应用原始apk中的dex A和从应用服务端下载的热修复dex B有重复类,并且dex A调用了dex B中的类的内联方法就会导致该问题,导致应用闪退,参考日志:
06-20 19:07:24.597 30376 30376 F ***: entrypoint_utils-inl.h:94] Inlined method resolution crossed dex file boundary: from *** in /data/app/***-YPDeV7WbuyZckOfy-5AuKw==/base.apk!classes3.dex/0xece238f0 to *** in /data/user/0/***/files/storage/***/dexpatch/1111/***.zip!classes4.dex/0xebda4320. This must be due to duplicate classes or playing wrongly with class loaders
2. 由classloader A加载的class 1调用了一个由classloader B加载的class2里的某个inline方法06-28 14:11:29.136 14361 14361 F pluginInstalle: entrypoint_utils-inl.h:94] Inlined method resolution crossed dex file boundary: from ***in /data/app/***-aofMHvlB2CdneHGpA-hNzA==/base.apk/0xee4a3ab0 to *** in /data/app/***-aofMHvlB2CdneHGpA-hNzA==/base.apk/0xe82541f0. This must be due to duplicate classes or playing wrongly with class loaders3. 应用原始apk的源文件中调用了系统apache-http接口,应用运行之后动态下载的dex中也集成了apache jar包,源文件中的apache类调用了热修复apache类的inline方法,导致了该问题:
07-09 19:52:32.938 12797 12876 F ***: entrypoint_utils-inl.h:94] Inlined method resolution crossed dex file boundary: from void org.apache.http.client.methods.HttpRequestBase.<init>() in /system/framework/org.apache.http.legacy.boot.jar/0xed1a5c60 to void org.apache.http.message.AbstractHttpMessage.<init>() in /data/user/0/***/***/patch-9cd507d8/dex/***_classN.apk!classes3.dex/0xed1a32d0. This must be due to duplicate classes or playing wrongly with class loaders
9.3 适配指导
- 先在P版本测试验证应用是否有该问题:
- 启动应用,构造热修复场景,在app侧触发热修复
- adb shell cmd package compile -m speed -f my-package 应用包名 (inline编译)
- 重启应用,看看会不会出现闪退问题
- 通过关键日志分析确认是不是这个问题:This must be due to duplicate classes or playing wrongly with class loaders
- 如果确认应用有这个问题,修复建议:
- 不要将ROM中预置的jar包打包进apk
- 不要用相同的class loader加载重复类
- 如果一定要有重复类的话,避免内联(比如在不期望被inline的函数里面加个try catch,这样compiler就不会将这个函数inline)
10 Camera、sensor和麦克风后台管控
10.1 背景介绍
增加Camera、sensor和麦克风的background限制,对所有targetSdkVersion的应用都生效;主要是考虑隐私安全的原因,功耗不是主要动机;对应用来说是很大的变化。
10.2 兼容性影响
- 影响范围:
所有三方应用处于idle的状态就会被管控,切换到后台一分钟应用就会进入idle状态,但是有前台服务的不会进入idle状态;
- 影响三方功能:后台录音,拍照、摄像、定位和计步。
Android 9 限制后台应用访问用户输入和传感器数据的能力。 如果您的应用在运行 Android 9 设备的后台运行,系统将对您的应用采取以下限制:
- 您的应用不能访问麦克风或摄像头。
- 使用连续报告模式的传感器(例如加速度计和陀螺仪)不会接收事件。
- 使用变化或一次性报告模式的传感器不会接收事件。
- 异常日志:
- 06-13 10:38:25.757 636 10129 E CameraService: Access Denial: can't use the camera from an idle UID pid=2787, uid=10164
- 异常日志:
10.3 适配指导
不要再假设应用在idle状态还可以使用麦克风、sensor和camera,如果需要后台(idle状态)访问麦克风、sensor和camera的时候需要在应用进入idle状态之前增加前台服务。前台服务:https://developer.android.com/guide/components/services.html#Foreground
11 targetSdkVersion政策
11.1 Google Play的TargetSdkVersion要求
- 2018年8月 新发布应用-必现为26或更高
- 2018年11月 升级现有应用-必须为26或者更高
- 2019年之后 新发布或升级应用-必须为一年内发布的Android版本
11.2 国内TargetSdkVersion政策
谷歌要求P版本上面应用设置的targetSdkVersion需要>=17,否则会弹出警告对话框提示用户,并且谷歌有明确的CDD要求,该提示无法去掉。
11.3 TargetSdkVersion升级指南
https://developer.android.google.cn/distribute/best-practices/develop/target-sdk
12 其他对所有targetSdkVersion生效的变更
参考文档:https://developer.android.com/about/versions/pie/android-9.0-changes-all
12.1 FLAG_ACTIVITY_NEW_TASK被强制要求
在P版本,如果不在Intent添加FLAG_ACTIVITY_NEW_TASK,将无法通过非Activity的Context启动一个Activity,并且会抛异常。
比如在Service中启动Activity,如果Intent不添加FLAG_ACTIVITY_NEW_TASK,就会抛异常:
@Override public void onCreate() { Log.v(TAG, "ServiceDemo onCreate"); super.onCreate(); Intent intent = new Intent(this, Main2Activity.class); // intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); }
12.2 Crypto provider在P版本去掉了
从P版本开始,Crypto JCA provider被去掉了,调用SecureRandom.getInstance(“SHA1PRNG”, “Crypto”) 将会报NoSuchProviderException。
12.3 限制访问 Wi-Fi 位置和连接信息
- Wi-Fi位置信息
在 Android 9 中,应用进行 Wi-Fi 扫描的权限要求比之前的版本更严格。 除了需要下面权限外:
还需要用户开启位置开关:
- 获取Wifi的SSID 和 BSSID 值
类似的限制也适用于 getConnectionInfo() 函数,该函数返回描述当前 Wi-Fi 连接的 WifiInfo 对象。 如果调用应用具有以下权限,则只能使用该对象的函数来检索 SSID 和 BSSID 值,也是和通过Wifi位置信息一样,增加了位置开关必须打开的要求
- 兼容性影响
影响应用通过wifi获取位置信息以及获取wifi的SSID 和 BSSID 值。
- 适配指导
- 通过wifi获取位置信息和wifi的SSID 和 BSSID值之前,需要先判断一下位置开关是否打开,如果位置开关关闭,需要提示用户主动开启:
public static boolean isLocationEnabled(Context context){ return Settings.Secure.getInt(context.getContentResolver(), Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF) != Settings.Secure.LOCATION_MODE_OFF; }
- 如果定位开关未打开,跳转到位置开关设置页面:
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); startActivity(intent);
13 只对targetSdkVersion>=28生效的变更
参考文档:https://developer.android.com/about/versions/pie/android-9.0-changes-28
13.1 前台服务权限
使用前台服务的应用必须请求 FOREGROUND_SERVICE 权限。
- 只需要在manifest中声明,系统自动授予,无需动态授权
- 若无此权限运行前台服务,会发生SecurityException
13.2 隐私权变更
对设备序列信息和 DNS 信息进行的这些更新可增强用户隐私保护。
- 去除通过SERIAL获取设备序列号的方法
在 Android 9 中,Build.SERIAL 始终设置为 "UNKNOWN" 以保护用户的隐私。
如果您的应用需要访问设备的硬件序列号,您应改为请求 READ_PHONE_STATE 权限,然后调用 getSerial()。
- DNS 隐私
以 Android 9 为目标平台的应用应采用私有 DNS API。 具体而言,当系统解析程序正在执行 DNS-over-TLS 时,应用应确保任何内置 DNS 客户端均使用加密的 DNS 查找与系统相同的主机名,或停用它而改用系统解析程序。
13.3 框架安全性变更
- 默认情况下启用网络传输层安全协议 (TLS)
如果您的应用以 Android 9 或更高版本为目标平台,则默认情况下 isCleartextTrafficPermitted() 函数返回 false。 如果您的应用需要为特定域名启用明文,您必须在应用的网络安全性配置中针对这些域名将 cleartextTrafficPermitted 显式设置为 true。
- WebView数据目录不允许共享
- 即使是同一个应用的不同进程也不行
- 建议:只在一个进程中调用WebView,其他进程disableWebView()
- 不同进程的WebView数据目录名必须使用不同的后缀
- 如果应用中的多个进程需要访问同一WebView数据,您需要自行在这些进程之间复制数据。 例如,您可以调用 getCookie() 和 setCookie(),在不同进程之间手动传输 Cookie 数据。
- 应用间不可以使用全局的Unix权限共享数据
- 每个应用数据目录都由selinux保护
- 应用可以继续使用ContentProvider或者外部存储共享数据
14 参考文献
- 华为终端开放实验室Android 9.0测试:
- 安卓绿色联盟《深度解读安卓P版本新特性》技术沙龙视频链接:
http://www.itdks.com/dakashuo/detail/15276?userId=2605919
http://www.itdks.com/dakashuo/detail/15275?userId=2605919
- Google P版本系统镜像下载:
https://developers.google.com/android/images
- 刷机指导:
https://source.android.com/source/running#unlocking-the-bootloader
- Google android code下载方法:
https://source.android.com/source/downloading.html
- Google android code线上查看地址:
https://android.googlesource.com/platform/frameworks/base/+/android-cts-9.0_r1
- Android 9 功能和 API:
https://developer.android.com/about/versions/pie/android-9.0
- 行为变更:所有应用
https://developer.android.com/about/versions/pie/android-9.0-changes-all
- 行为变更:以 API 级别 28+ 为目标的应用
https://developer.android.com/about/versions/pie/android-9.0-changes-28
- P版本适配流程:
https://developer.android.com/about/versions/pie/android-9.0-migration
- 刘海屏:
https://android-developers.googleblog.com/2018/07/supporting-display-cutouts-on-edge-to.html