*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
前言
HI,欢迎来到裴智飞的《每周一博》。今天是十月第三周,我给大家分享一下安卓各系统版本的新特性和开发过程中的适配.
安卓每次升级都会带来一些重大的改变,在性能,安全上都会有所提升,每个版本也都有其各自的特点,比如L引入的MaterialDesign设计,M增加对权限和耗电的控制,N支持的多窗口和VR,O对后台进程的限制,P对流海屏的支持等。
当然,每个版本都需要开发者去适配,开发者明显可以感到系统对权限的收紧和安全上的重视,过去很多粗放的代码都要变更,典型的比如M的权限适配,N的FileProvider,O的后台限制,P的安全上的配配等,这里我将针对LMNOP这5个版本中重大的适配做一个简单总结。
安卓各版本新特性
安卓L
- 引入Material Design设计,增加UI控件和动画效果, V7支持库中增加新控件以便向下兼容;
- 系统由以往的Dalvik模式改为采用ART(Android Runtime)模式,实现ahead-of-time (AOT)静态编译与just-in-time (JIT)动态编译交互进行;
- 支持64位系统;
安卓M
- 新增运行时权限概念:系统默认给app授权部分基础权限,其他敏感权限,需要运行时手动请求系统授予权限;
- 新增Doze模式:可以自动识别手机使用状态,并在闲时主动关闭部分后台进程以节省能耗;
- 新增待机模式: 针对很少使用的应用,将不再消耗电量
- 新增指纹解锁API;
安卓N:
- 通知中心变的便捷且更强大,下拉通知栏中最上方加入了快捷按键控制开关,同时通知中心能显示更多的信息,用户可以在通知中心内快速回复;
- 支持多窗口功能;
- 支持VR,以使开发者能为用户打造高质量移动 VR 体验;
- 引入全新的JIT编译器,使得App安装速度快了75%,编译代码的规模减少了50%;
- App快捷菜单,长按APP图标可以快速进入某些功能界面;
安卓O:
- 支持画中画模式;
- 自动填充,对于用户设备上最常用的应用,Android O将会帮助用户进行快速登录,而不用每次都填写账户名和密码;
- 自适应图标,为开发者提供了适应其显示设备的每个图标的多个形状模板,来解决Android中APP图标形状不一致的问题;
- 通知提醒,当通知栏有未读信息时,会出现一个小点,这时候长按应用程序图标,就会以类似气泡的形式快速预览。
- 后台进程限制,当应用被置入后台后,将自动智能限制后台应用活动,主要会限制应用的广播,后台运行和位置,但应用的整体进程并没有被杀掉。
- 运行时权限策略变化,系统只会授予应用明确请求的权限,而一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准。
安卓P:
- 支持流海屏,系统会管理状态栏的高度从而将内容与裁切区域分开,如果拥有重要的沉浸式内容,则还可以使用新的API查看裁切形状并创建全屏布局;
- 多摄像头API,可以通过两个或更多实体摄像头同时访问视频流;
- 新增了ImageDecoder类,为解码图像提供了一种更优的方法;
- 增加神经网络API-1.1版本;
安卓各版本开发适配
安卓L-5.0
- Service服务必须采用显示方式启动
解决办法有两种,一种是设置Action和packageName,这也是推荐的方案,另一种是通过PackageManager遍历所有符合的ComponentName;
Intent intent = new Intent();
// service定义的action
intent.setAction("XXX.XXX.XXX");
// service所在的包名
intent.setPackage(getPackageName());
context.startService(mIntent);
- 通知栏适配
5.0以上需要通过NotificationCompat.Builder来创建通知,而不是直接new Notification或new Notification.Builder,另外在8.0之上还需要适配,后面再说;
Notification notification = new NotificationCompat.Builder(mContext)
.setContentTitle(name)
.setContentText(contentText)
.setSmallIcon(R.drawable.stat)
.setContentIntent(pi)
.build();
- 关闭通知权限不弹Toast
其实看过Toast源码就知道它是通知栏服务NotificationManagerService维护的一个队列,通过调用 WindowManager来添加view,所以关闭通知权限后无法显示Toast。
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
}
}
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
解决办法可以自己去实现一个Toast,使用队列来维护,用WindowManager来addView,或者直接使用第三方库。
- View的高度与阴影
View新增加了z轴属性,来体现MaterialDesign中的层次,影响的因素有translationZ和elevation。
计算View高度= elevation + translationZ
(1) transtionZ属性表示view在Z方向移动的距离,一般用于属性动画中;
(2) elevation表示view的阴影,可以在xml中直接使用属性, 也可以在代码中使用view.setEvelvation();elevation也能起到权重的作用,因为有阴影的必然是在普通层级的上面,具体的关于elevation和transtionZ的说明可以参考这篇文章。
高度会影响View的绘制顺序,以前是按View添加顺序绘制,现在高度小的先绘制,因为层级低,在下面, 高度相同的,按添加顺序绘制,需要注意的是如果View的背景色为透明,则不会显示出阴影效果,另外只有子View的大小比父View小时,阴影才能显示出来;
安卓M-6.0
-
动态权限申请
当targetSdkVersion>=23的时候, 需要用checkSelfPermission()用来检测App是否被授予了权限,比如定位的时候需要先去检查是否拥有相关权限,如果没有,可以使用requestPermissions()用来请求权限 ,这是Activity的方法,同时在回调方法onRequestPermissionsResult里面判断结果。 -
弃用HttpClient
Android6.0版本移除了对Httpclient的支持,建议我们使用HttpUrlConnection,如果仍要坚持使用,需要在build.gradle中添加如下代码:
android {
useLibrary 'org.apache.http.legacy'
}
-
Doze模式对定时器的影响
在Doze模式下,标准 AlarmManager闹铃,包括 setExact() 和 setWindow()将推迟到下一维护时段,如果需要设置在低电耗模式下触发的闹铃,可以使用setAndAllowWhileIdle() 或setExactAndAllowWhileIdle()方法,这2个方法只能在15分钟唤醒一次,如果你的广播需要1分钟广播很多次,也只能15分钟一次。setAlarmClock()将不会受Doze模式影响,但是它会耗电, 所以必须在节约电量和业务之间做个取舍。 -
获取硬件标识符需要权限
WifiInfo.getMacAddress()和BluetoothAdapter.getAddress()将始终返回02:00:00:00:00:00,为了在M上可以扫描WiFi或蓝牙,需具有ACCESS_FINE_LOCATION和 ACCESS_COARSE_LOCATION权限
安卓N-7.0
-
私有文件权限收紧
私有文件的文件权限不再放权给全部的应用,使用 MODE_WORLD_READABLE 或 MODE_WORLD_WRITEABLE 进行的操作将触发 SecurityException。 -
文件共享使用FileProvider
在N上Android 框架强制运行了StrictMode,禁止对外公开file:// URI,也就是说不能发起包含文件file:// URI类型的Intent ,这样会出现FileUriExposedException异常,比如调用系统相机拍照或相册。
(1) 先在清单文件里面配置FileProvider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
其中exported必须为false,true会报安全异常,grantUriPermissions为true表示授予URI访问权限。
(2) 指定共享的文件夹
在res文件夹下创建一个xml文件夹,然后创建一个名为“file_paths”(和注册的Provider所引用的resource保持一致就可以)的资源文件,内容如下;
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-path path="." name="files_root"/>
</paths>
</resources>
代码中path=".",表示根文件夹,也就是说你能够向其它的应用共享根文件夹及其子文件夹下的任何文件,假设你把path设为path="pictures", 那么它表示根文件夹下的pictures文件夹,如/storage/emulated/0/pictures,那么向其它应用分享pictures文件夹之外的文件是不行的。
path下必须包含一到多个子元素,这些子元素用于指定共享文件的目录路径,必须是这些元素之一:
- <files-path>:内部存储空间应用私有目录下的 files/ 目录,等同于 Context.getFilesDir() 所获取的目录路径;
- <cache-path>:内部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getCacheDir() 所获取的目录路径;
- <external-path>:外部存储空间根目录,等同于 Environment.getExternalStorageDirectory() 所获取的目录路径;
- <external-files-path>:外部存储空间应用私有目录下的 files/ 目录,等同于 Context.getExternalFilesDir(null) 所获取的目录路径;
- <external-cache-path>:外部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir();
可以看出,这五种子元素基本涵盖内外存储空间所有目录路径,包含应用私有目录。每个子元素都拥有 name 和 path 两个属性,path 属性用于指定当前子元素所代表目录下需要共享的子目录名称。注意path属性值不能使用具体的独立文件名,只能是目录名,而name属性用于给 path 属性所指定的子目录名称取一个别名。后续生成content:// URI 时,会使用这个别名代替真实目录名。这样做的目的,很显然是为了提高安全性。
如果我们需要分享的文件位于同级别目录下不同的子目录中,就需要添加多个子元素逐一指定要分享的文件目录,或者共享他们通用的父目录也行。
(3) 使用FileProvider,比如调用相机
String filePath = Environment.getExternalStorageDirectory() +
"/images/"+System.currentTimeMillis()+".jpg";
File outputFile = new File(filePath);
if (!outputFile.getParentFile().exists()) {
outputFile.getParentFile().mkdir();
}
// 这里的uri要和manifest里面的对应上
Uri contentUri = FileProvider.getUriForFile(this,BuildConfig.APPLICATION_ID + ".myprovider", outputFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
startActivityForResult(intent, REQUEST_TAKE_PICTURE);
安卓O-8.0
-
通知栏增加渠道
安卓O增加了渠道,使得用户可以选择接受哪些渠道的通知,上一张图比较直观。
这里我上一份最终的通知栏适配代码,经历各版本的;
public void showNotification(){
final int NOTIFICATION_ID = 12234;
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
Intent intent = new Intent();
String action = "com.tamic.myapp.action";
intent.setAction(action);
Notification notification = null;
String contentText;
PendingIntent pi = PendingIntent.getActivity(mContext, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
notification = new Notification();
notification.icon = android.R.drawable.stat_sys_download_done;
notification.flags |= Notification.FLAG_AUTO_CANCEL;
notification.setLatestEventInfo(mContext, aInfo.mFilename, contentText, pi);
// 5.0适配
} else if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O && Build.VERSION.SDK_INT >= LOLLIPOP_MR1) {
notification = new NotificationCompat.Builder(mContext)
.setContentTitle("Title")
.setContentText(contentText)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentIntent(pi).build();
// 4.4适配
} else if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && Build.VERSION.SDK_INT <= LOLLIPOP_MR1) {
notification = new Notification.Builder(mContext)
.setAutoCancel(false)
.setContentIntent(pi)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setWhen(System.currentTimeMillis())
.build();
// 8.0适配
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
String CHANNEL_ID = "my_channel_01";
CharSequence name = "my_channel";
String Description = "This is my channel";
int importance = NotificationManager.IMPORTANCE_HIGH;
NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, name, importance);
mChannel.setDescription(Description);
mChannel.enableLights(true);
mChannel.setLightColor(Color.RED);
mChannel.enableVibration(true);
mChannel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
mChannel.setShowBadge(false);
// 这里配置了渠道
notificationManager.createNotificationChannel(mChannel);
notification = new NotificationCompat.Builder(ctx, CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentTitle("Title").build();
}
notificationManager.notify(NOTIFICATION_ID, notification);
}
- 安装APK
Android)去除了“允许未知来源”选项,所以如果我们的App有安装App的功能,比如检查更新之类的,那么会无法正常安装。
首先在AndroidManifest文件中添加安装未知来源应用的权限:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
这样系统会自动询问用户完成授权,当然也可以先使用 canRequestPackageInstalls()查询是否有此权限,如果没有的话使用Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES这个action将用户引导至安装未知应用权限界面去授权。
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();
}
}
- 悬浮窗适配
使用 SYSTEM_ALERT_WINDOW 权限的应用必须使用名为TYPE_APPLICATION_OVERLAY 的新窗口类型,以下窗口类型将无法显示:
TYPE_PHONE
TYPE_PRIORITY_PHONE
TYPE_SYSTEM_ALERT
TYPE_SYSTEM_OVERLAY
TYPE_SYSTEM_ERROR
需要在之前的基础上判断一下:
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" />
安卓P-9.0
-
前台Service权限
在 Android P中,如果 targeSdkVersion 升级到 28,使用前台 Service 必须要申请FOREGROUND_SERVICE权限,如果没有申请该权限,系统会抛出SecurityException,该权限为普通权限,申请自动授予应用。 -
序列号弃用
在 Android P中,Build.SERIAL 始终设置为 "UNKNOWN" ,来保护用户的隐私。如果需要访问设备的硬件序列号,需要请求 READ_PHONE_STATE 权限,然后调用 getSerial(); -
默认启用网络传输层安全协议 (TLS)
在 Android P 中,默认情况下 isCleartextTrafficPermitted() 函数返回 false。 如果应用需要为特定域名启用明文,必须在应用的网络安全性配置中针对这些域名将 cleartextTrafficPermitted 显式设置为 true。所以无法及时把服务器变更为 https 的应用,应该通过配置文件针对特定域名允许使用明文传输,也就是http服务。
先定义配置文件 res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">secure.example.com</domain>
</domain-config>
</network-security-config>
然后在manifest.xml中使用
<application android:networkSecurityConfig="@xml/network_security_config">
- WiFi相关权限变更
(1)在Android 8.0和Android 8.1上:三选一
调用 WifiManager.getScanResults() 需要以下任何一项权限,如果调用应用程序没有任何这些权限,则调用将失败并显示 SecurityException。
ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
CHANGE_WIFI_STATE
(2)Android 9及更高版本:全部满足
调用 WifiManager.startScan() 需要满足以下所有条件:
具有 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限;
具有 CHANGE_WIFI_STATE 权限;
调用 WifiManager.getScanResults() 需要满足以下所有条件:
具有 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限;
具有 ACCESS_WIFI_STATE 权限;
设备上启用了位置服务;
这个限制条件也适用于getConnectionInfo() 函数,该函数返回描述当前WiFi连接的WifiInfo 对象。 如果调用应用具有以下权限,则只能使用该对象的函数来检索 SSID 和 BSSID 值:
具有 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限;
具有 ACCESS_WIFI_STATE 权限;
检索 SSID 或 BSSID 还需要在设备上启用位置服务;
结尾:
本周给大家简单介绍了各系统版本的新特性和开发适配,具体更多的内容可以参考其他文章。感谢大家的阅读,我们下周再见。
网友评论