android开发中,除了屏幕适配外,新版本的推出会带来一些版本的差异,我们需要对相应版本来做适配。
Android 6 权限适配
系统权限主要分为两类,正常权限和危险权限。不管哪个版本的android,你应用中所用到的所有权限,不管是正常权限还是危险权限,都需要在应用的Manifest中申明。
你的目标SDK是23以及23以上版本:应用必须在Manifest
中罗列出所有的权限,并且在程序运行时,它必须请求用户授予每一个危险权限,此时用户可以授予或者拒绝每一个权限,并且应用程序可以继续运行有限的功能,即使用户拒绝了权限请 。
适配步骤:
1.在需要用到权限时调用requestPermissions(needRequestPermissonArr,PERMISSON_REQUESTCODE);
来请求
2.重写权限申请的回调方法
/**
* 申请权限结果的回调方法
*/
@TargetApi(23)
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] paramArrayOfInt) {
if (requestCode == PERMISSON_REQUESTCODE) {
//这里通过permissions及paramArrayOfInt来判断是否给予了相应的权限,paramArrayOfInt里对应值为PackageManager.PERMISSION_GRANTED,则表示有此权限
}
}
现在有rxPermissions,MPermissions这些库可以使用。
Android 7适配
FileProvider
在官方7.0的以上的系统中,尝试传递 file://URI可能会触发FileUriExposedException。
比如我们的拍照功能以前都是这样写
private static final int REQUEST_CODE_TAKE_PHOTO = 0x110;
private String mCurrentPhotoPath;
public void takePhotoNoCompress(View view) {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
.format(new Date()) + ".png";
File file = new File(Environment.getExternalStorageDirectory(), filename);
mCurrentPhotoPath = file.getAbsolutePath();
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_TAKE_PHOTO) {
mIvPhoto.setImageBitmap(BitmapFactory.decodeFile(mCurrentPhotoPath));
}
// else tip?
}
android7.0上运行就会报FileUriExposedException
,适配步骤如下:
- 声明provider
2.配置resource xml file
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="root" path="" />
<files-path name="files" path="" />
<cache-path name="cache" path="" />
<external-path name="external" path="" />
<external-files-path name="name" path="path" />
<external-cache-path name="name" path="path" />
</paths>
在paths节点内部支持以下几个子节点,分别为:
<root-path/>
代表设备的根目录new File("/");
<files-path/>
代表context.getFilesDir()
<cache-path/>
代表context.getCacheDir()
<external-path/>
代表Environment.getExternalStorageDirectory()
<external-files-path>
代表context.getExternalFilesDirs()
<external-cache-path>
代表getExternalCacheDirs()
每个节点都支持两个属性:
name
path
path即为代表目录下的子目录,比如:
<external-path name="external" path="pics" />
代表的目录即为:Environment.getExternalStorageDirectory()/pics
,其他同理。
开源库实现 https://github.com/hongyangAndroid/FitAndroid7
多窗口支持
如果您不想支持多窗口模式,只需配置
<application
android:resizeableActivity="false"
其中,true 表示应用支持多窗口模式,false 表示应用不支持多窗口模式,如果不配置这个属性,那么默认值为true
给wm 应用配置分屏模式
在清单文件的 或 节点中设置该属性,启用或禁用多窗口显示:
//设置为true ,该activity支持分屏,默认为true
android:resizeableActivity="true"
在AndroidManifest中的布局属性
对于 Android N,<layout> 清单文件元素支持以下几种属性,这些属性影响 Activity 在多窗口模式中的行为,我们先看看下面的代码.
<!--android:defaultWidth
以自由形状模式启动时 Activity 的默认宽度。
android:defaultHeight
以自由形状模式启动时 Activity 的默认高度。
android:gravity
以自由形状模式启动时 Activity 的初始位置。
android:minimalSize
分屏和自由形状模式中 Activity 的最小高度和最小宽度。
如果用户在分屏模式中移动分界线,使 Activity 尺寸低于指定的最小值,系统会将 Activity 裁剪为用户请求的尺寸。
例如,以下节点显示了如何指定 Activity 在自由形状模式中显示时 Activity 的默认大小、位置和最小尺寸:
-->
<activity android:name=".MyActivity">
<layout android:defaultHeight="500dp"
android:defaultWidth="600dp"
android:gravity="top|end"
android:minimalSize="450dp" />
</activity>
隐式广播
在Android 7.0中删除了三项隐式广播,以帮助优化内存使用和电量消耗。
Android 7.0 应用了以下优化措施:
- 在 Android 7.0上 应用不会收到
CONNECTIVITY_ACTION
广播,即使你在manifest清单文件中设置了请求接受这些事件的通知。 但,在前台运行的应用如果使用BroadcastReceiver 请求接收通知,则仍可以在主线程中侦听 CONNECTIVITY_CHANGE。 - 在 Android 7.0上应用无法发送或接收
ACTION_NEW_PICTURE
或ACTION_NEW_VIDEO
类型的广播。
应对策略:Android 框架提供多个解决方案来缓解对这些隐式广播的需求。 例如,JobScheduler API 提供了一个稳健可靠的机制来安排满足指定条件(例如连入无限流量网络)时所执行的网络操作。 您甚至可以使用 JobScheduler API 来适应内容提供程序变化。
另外,大家如果想了解更多关于后台的优化可查阅后台优化。
移动设备会经历频繁的连接变更,例如在 Wi-Fi 和移动数据之间切换时。 目前,可以通过在应用清单中注册一个接收器来侦听隐式 CONNECTIVITY_ACTION
广播, 让应用能够监控这些变更。 由于很多应用会注册接收此广播,因此单次网络切换即会导致所有应用被唤醒并同时处理此广播。
7.1的3D Touch的支持
Android 8.0 适配
1. 自适应启动图标
之前的启动图标都是mipmap中的静态图片ic_launcher。到后来7.1的时候谷歌开始推广圆形图标,在原来android:icon的基础上又添加了android:roundIcon属性来让你的app支持圆形图标
到了8.0,情况又变了,我们来创建一个新项目看看发生了什么变化
多了一个mipmap-anydpi-v26文件夹,里面也是启动图,但是不是一张图片,而是xml文件
文件是这样的
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
原来是两张SVG图,8.0通过定义背景和前景这2层视图来自适应启动器图标的外观。这个功劳归属于<adaptive-icon>
元素。我们可以使用该元素为图标定义前景层和背景层的绘图,其中的<foreground>
和<background>
内部属性都支持android:drawable
属性
注意图标图层的大小,两层的尺寸必须为108x108dp,前景图层中间的72x72dp图层就是在手机界面上展示的应用图标范围。这样系统在四面各留出18dp以产生有趣的视觉效果,如视差或脉冲(动画视觉效果由受支持的启动器生成,视觉效果可能因发射器而异)
如果要将图标应用于快捷方式中,我们可以通过以下两种方式去使用:
对于静态快捷方式,使用该xml
对于动态快捷方式,使用createWithAdaptiveBitmap()创建相应的Bitmap
安装APK
Android 8.0去除了“允许未知来源”选项,所以如果我们的App有安装App的功能(检查更新之类的),那么会无法正常安装。
首先在AndroidManifest文件中添加安装未知来源应用的权限:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
这样系统会自动询问用户完成授权。当然你也可以先使用 canRequestPackageInstalls()
查询是否有此权限,如果没有的话使用Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
这个action将用户引导至安装未知应用权限界面去授权。
private static final int REQUEST_CODE_UNKNOWN_APP = 100;
private void installAPK(){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean hasInstallPermission = getPackageManager().canRequestPackageInstalls();
if (hasInstallPermission) {
//安装应用
} else {
//跳转至“安装未知应用”权限界面,引导用户开启权限
Uri selfPackageUri = Uri.parse("package:" + this.getPackageName());
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, selfPackageUri);
startActivityForResult(intent, REQUEST_CODE_UNKNOWN_APP);
}
}else {
//安装应用
}
}
//接收“安装未知应用”权限的开启结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_UNKNOWN_APP) {
installAPK();
}
}
集合的处理
现在,AbstractCollection.removeAll(null)
和 AbstractCollection.retainAll(null)
始终引发NullPointerException
;之前,当集合为空时不会引发 NullPointerException
。所以我们需要做判空处理。
通知适配
8.0在通知变化挺多的,比如通知渠道、通知标志、通知超时、背景颜色的等
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
//分组(可选)
//groupId要唯一
String groupId = "group_001";
NotificationChannelGroup group = new NotificationChannelGroup(groupId, "广告");
//创建group
notificationManager.createNotificationChannelGroup(group);
//channelId要唯一
String channelId = "channel_001";
NotificationChannel adChannel = new NotificationChannel(channelId,
"推广信息", NotificationManager.IMPORTANCE_DEFAULT);
//补充channel的含义(可选)
adChannel.setDescription("推广信息");
//将渠道添加进组(先创建组才能添加)
adChannel.setGroup(groupId);
//创建channel
notificationManager.createNotificationChannel(adChannel);
//创建通知时,标记你的渠道id
Notification notification = new Notification.Builder(MainActivity.this, channelId)
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentTitle("一条新通知")
.setContentText("这是一条测试消息")
.setAutoCancel(true)
.build();
notificationManager.notify(1, notification);
}
}
注意:当Channel已经存在时,后面的createNotificationChannel方法仅能更新其name/description,以及对importance进行降级,其余配置均无法更新。所以如果有必要的修改只能创建新的渠道,删除旧渠道。
删除旧渠道代码:
private void deleteNotificationChannel(String channelId){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager mNotificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.deleteNotificationChannel(channelId);
}
}
悬浮窗适配
使用 SYSTEM_ALERT_WINDOW
权限的应用无法再使用以下窗口类型来在其他应用和系统窗口上方显示提醒窗口:
TYPE_PHONE
TYPE_PRIORITY_PHONE
TYPE_SYSTEM_ALERT
TYPE_SYSTEM_OVERLAY
TYPE_SYSTEM_ERROR
相反,应用必须使用名为 TYPE_APPLICATION_OVERLAY
的新窗口类型。
也就是说需要在之前的基础上判断一下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mWindowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}else {
mWindowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
}
当然记得需要有权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
Android9.0适配
刘海屏API支持
Android 9 支持最新的全面屏,其中包含为摄像头和扬声器预留空间的屏幕缺口。 通过 DisplayCutout类可确定非功能区域的位置和形状,这些区域不应显示内容。 要确定这些屏幕缺口区域是否存在及其位置,使用 getDisplayCutout()函数。
//取区域位置及位置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
View decorView = getWindow().getDecorView();
WindowInsets rootWindowInsets = decorView.getRootWindowInsets();
if (rootWindowInsets != null) {
DisplayCutout cutout = rootWindowInsets.getDisplayCutout();
List<Rect> boundingRects = cutout.getBoundingRects();
if (boundingRects != null && boundingRects.size() > 0) {
String msg;
for (Rect rect : boundingRects) {
msg = s+"left-" + rect.left;
Log.d(TAG, msg);
}
}
}
}
- 新窗口布局模式,允许应用程序请求是否在挖孔区域布局:
class WindowManager.LayoutParams {
int layoutInDisplayCutoutMode;
final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
}
layoutInDisplayCutoutMode
值说明:
1.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
:默认情况下,全屏窗口不会使用到挖孔区域,非全屏窗口可正常使用挖孔区域。
-
LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
:窗口声明使用挖孔区域
3.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
:窗口声明不使用挖孔区域
设置代码
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
getWindow().setAttributes(lp);
前台服务需要添加权限
在安卓9.0版本之后,必须要授予FOREGROUND_SERVICE权限,才能够使用前台服务,否则会抛出异常。
例如:
@Override
public void onCreate() {
super.onCreate();
String channelID = "1";
String channelName = "channel_name";
NotificationChannel channel = new NotificationChannel(channelID, channelName, NotificationManager.IMPORTANCE_HIGH);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.createNotificationChannel(channel);
Intent intent = new Intent(this, ForegroundActivity.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("前台服务测试")
.setContentText("前台服务需要增加 FOREGROUND_SERVICE 权限")
.setChannelId(channelID)
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
.setContentIntent(pi)
.build();
startForeground(1, notification);
}
这是一个带Notification
的简单前台服务, 如果我们没有在AndroidManifest
中注册FOREGROUND_SERVICE
权限,在Service启动的时候会抛出SecurityException
异常。
对此,我们只需要在AndroidManifest添加对应的权限即可,这个权限是普通权限,不需要动态申请。
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
限制静态广播的接收
升级安卓9.0之后,隐式广播将会被全面禁止,在AndroidManifest中注册的Receiver将不能够生效,如果你的清单文件中有如下的监听器:
<receiver android:name="com.yanghaoyi.receiver.UpdateReceiver">
<intent-filter>
<action android:name="com.yanghaoyi.action.ACTION_UPDATE" />
</intent-filter>
</receiver>
你需要移除上面的代码,并在应用中进行动态注册,例如:
private void registerReceiver(){
myReceiver = new MyReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(TOAST_ACTION);
registerReceiver(myReceiver, intentFilter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(myReceiver);
}
非全屏透明Activity禁用设置orientation
非全屏透明页面不允许设置方向,否则会抛Caused by: java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation异常,解决方案:android:windowIsTranslucent设置为false。
非 SDK 接口访问限制
在 Android 9.0 版本中,谷歌加入了非 SDK 接口使用限制,无论是通过调用、反射还是JNI等方式,开发者都无法对非 SDK 接口进行访问,此接口的滥用将会带来严重的系统兼容性问题。 在开发过程中,开发者如果调用了非 SDK 接口,会导致应用出现crash,无法启动;或在运行过程中出现崩溃、闪退等现象;也可能导致应用功能不可用等严重兼容性问题,其影响范围波及所有调用此接口的应用。
那么什么是非SDK接口呢,所谓非SDK接口就是所有不能够在谷歌官网上查询到的接口,谷歌提供了查询接口的网站。
非SDK接口的类型,分为三类
(1)Light grey list: targetSDK>=P时,警告;
(2)Dark grey list: targetSDK<P时,警告;>=p时,不允许调用;
(3)Black list:三方应用不允许调用。
例如我们通过反射修改Dialog窗体的颜色:
try {
//通过反射的方式来更改dialog中文字大小、颜色
Field mAlert = AlertDialog.class.getDeclaredField("mAlert");
mAlert.setAccessible(true);
Object mAlertController = mAlert.get(normalDialog);
Field mMessage = mAlertController.getClass().getDeclaredField("mMessageView");
mMessage.setAccessible(true);
TextView mMessageView = (TextView) mMessage.get(mAlertController);
mMessageView.setTextSize(23);
mMessageView.setTextColor(Color.RED);
Field mTitle = mAlertController.getClass().getDeclaredField("mTitleView");
mTitle.setAccessible(true);
TextView mTitleView = (TextView) mTitle.get(mAlertController);
mTitleView.setTextSize(20);
mTitleView.setTextColor(Color.RED);
}catch (Exception e){
Toast.makeText(NotSDKInterfaceActivity. this,e.getLocalizedMessage(),Toast.LENGTH_LONG).show();
}
此方法在安卓9.0版本将不能够正常运行,会抛出NoSuchFieldException
,对于诸如此类的调用官方private方法或者@hide方法,都将不能使用。
非Activity-Context启动Activity,现在强制执行 FLAG_ACTIVITY_NEW_TASK要求
Apache HTTP 客户端弃用,影响采用非标准 ClassLoader 的应用
将 compileSdkVersion 升级到 28 之后,如果在项目中用到了 Apache HTTP client 的相关类,就会抛出找不到这些类的错误。这是因为官方已经在 Android P 的启动类加载器中将其移除,如果仍然需要使用 Apache HTTP client.
在 Manifest 文件中加入:
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
或者也可以直接将 Apache HTTP client 的相关类打包进 APK 中。
如果它们委托给 系统 ClassLoader,则应用在 Android 9 或更高版本上将失败并显示 NoClassDefFoundError,因为 系统 ClassLoader不再识别这些类。 为防止将来出现类似问题,一般情况下,应用应通过 应用 ClassLoader加载类,而不是直接访问系统 ClassLoader
前台服务
针对 Android 9 或更高版本并使用前台服务的应用必须请求 FOREGROUND_SERVICE 权限。 这是普通权限,因此,系统会自动为请求权限的应用授予此权限。
如果针对 Android 9 或更高版本的应用尝试创建一个前台服务且未请求 FOREGROUND_SERVICE,则系统会引发 SecurityException。
设备识别码
通过Build.SERIAL不再能够获取到真实数据,Build.serial:unknown,需要通过Build.getSerial()获取。同时需要用户授权READ_PHONE_STATE权限。
http网络请求
在9.0中默认情况下启用网络传输层安全协议 (TLS),默认情况下已停用明文支持。也就是不允许使用http请求,要求使用https。
比如我使用的是okhttp,会报错:
java.net.UnknownServiceException: CLEARTEXT communication toxxxx notpermitted bynetwork security policy
解决方案
Android 10适配
Android 10 适配攻略,你适配了吗
主要就是存储权限发生了变化,分区存储完美适配这篇讲得比较细,
关于获取文件存储目录的api与对应的真实目录,可以看Android文件存储目录。
其它一些问题可以看Android10填坑指南
Android 11
Android11强制执行分区存储,也就是沙盒模式。这次真的没有关闭功能了,离Android11出来也有一段时间了,还是抓紧适配把。
修改电话权限,改动了两个API:getLine1Number()和 getMsisdn() ,需要加上READ_PHONE_NUMBERS权限
不允许自定义toast从后台显示了
必须加上v2签名
增加5g相关API
后台位置访问权限再次限制
适配请参考拖不得了,Android11真的要来了,最全适配实践指南奉上