Android 版本适配问题
一、基本概念
在平时的开发中,我们经常会遇到两个问题:
- 升级了
build.gradle
的targetSdkVersion
之后发生了莫名其妙的问题。 - 在不同
Android
版本上的行为不同。
那么这是为什么呢?其核心原因是没有做好版本适配工作,这和targetSdkVersion
有很大的关系,所以我们需要了解一下它的基本概念。
1.1 什么是 targetSdkVersion
targetSdkVersion
的属性值表示创建的Android
项目使用哪个API
版本。
1.2 targetSdkVersion 有什么用
每个Android
版本都会对应一个API
数字,例如Android 7.0
对应的是API 24
,当手机的Android
系统版本升级的时候,会出现两种情况:
- 提供了新的接口。如果开发者想要在
APP
中使用Android 7.0
提供的新功能,除了需要使用Android 7.0
手机,还需要保证targetSdkVersion
升级到至少24
,从这个角度来说,升级 targetSdkVersion 的目的是为了使用新版本的功能。 - 旧接口的行为发生了变化。为了保证旧的
APK
的行为还是和以前兼容,在源码当中,有许多类似于ctx.getApplicationInfo().targetSdkVersion
的判断。因此只要APK
的targetSdkVersion
不变,即使这个APK
安装在新的Android
系统上,其行为也不会发生变化,从这个角度来说,targetSdkVersion 保证了系统对旧应用的向前兼容性。
1.3 compileSDKVersion
compileSDKVersion
定义应用程序编译选择哪个Android SDK
版本,通常设置为最新的API
版本,它的属性值不会影响Android
系统运行行为,仅仅是Android
编译项目时其中的一项配置,不会打包到APK
中,其目的是为了 在编译的时候检查代码的错误的警告,提示开发者修改和优化。
1.4 minSdkVersion
minSdkVersion
定义应用支持安装的最低Android
版本,这个数值有两个作用:
- 告诉
Google Play Store
哪些Android
版本的手机可以安装该APK
。 - 默认情况下,
lint
会对代码中的API
调用做出提示,假如你调用的API
是在minSdkVersion
之后才提供的,那么它会告诉你,虽然可以编译通过,但是会在运行的时候抛出异常。
如果调用的API
是在minSdkVersion
之后才提供的,解决的方案有两种:
- 运行时判断
API Level
,仅在足够高,有此方法的API Level
系统中调用此方法。
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//处理逻辑。
}
- 保证功能完整性,通过低版本的
API
实现功能。
二、Android 6.0 适配
2.1 在运行时请求权限
从Android 6.0 (API >= 23)
开始,用户开始在运行时向其授予权限,而不是在应用安装时授予。系统权限分为两类:
- 正常权限。如果在
AndroidManifest.xml
列出了正常权限,系统将自动授予该权限。 - 危险权限。如果在
AndroidManifest.xml
中列出了危险权限,用户必须明确批准您的应用使用这些权限。
这篇是关于如何进行权限适配的文章 在运行时请求权限。
三、Android 7.0 适配
3.1 应用间共享文件限制
在Android 7.0
系统上,Android
框架强制执行了StrictMode API
政策禁止向应用外公开file://URI
,如果一项包含文件file://URI
类型的的Intent
离开你的应用,即调用Uri.from(file)
传递文件路径给第三方应用,会出现FileUriExposedException
异常,如调用系统相机拍照、裁切照片、打开APK
安装界面等。
如果要 在应用间共享文件,可以发送content://URI
类型的Uri
,并授予URI
临时访问权限,进行此授权的最简单方式是使用FileProvider
类。
步骤如下:
- 在
AndroidManiest.xml
清单中注册provider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.demo.lizejun.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths"/>
</provider>
exported
为false
,grantUriPermissions
表示授予URI
临时访问权限。
- 指定共享目录
上文中的android:resource="@xml/file_provider_paths"
指定了共享的目录,其配置如下:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path
name="captured_media"
path="captured_media/" />
<external-path
name="data"
path="Android" />
<cache-path
name="cache"
path="appCache" />
<external-path
name="external"
path="" />
</paths>
- 通过
FileProvider
,下面是打开下载完的APK
的实例
public static Intent getOpenFileIntent(Context context, DownloadResponse downloadResponse) {
File file = new File(downloadResponse.getParentPath(), downloadResponse.getFileName());
if (!file.exists()) {
return null;
}
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri contentUri = FileProvider.getUriForFile(context, "com.demo.lizejun.provider", file);
intent.setDataAndType(contentUri, downloadResponse.getMimeType());
} else {
intent.setDataAndType(Uri.fromFile(file), downloadResponse.getMimeType());
}
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
return intent;
}
3.2 系统广播删除
Android N
关闭了三项系统广播:网络状态变更广播、拍照广播及录像广播。
只有在通过 动态注册 的方式才能收到网络变化的广播,在AndroidManifest.xml
中静态的注册的无法收到。
四、Android 8.0 适配
4.1 通知渠道
在Android 8.0
中所有的通知都需要提供通知渠道,否则,所有通知在8.0
系统上都不能正常显示。
DownloadNotifier(Context context) {
mContext = context;
mManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@SuppressWarnings("all") final NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_HIGH);
mManager.createNotificationChannel(channel);
}
}
4.2 悬浮窗
8.0
中新增了一种悬浮窗的窗口类型,TYPE_APPLICATION_OVERLAY
,如果应用使用SYSTEM_ALERT_WINDOW
权限并且尝试使用以下窗口类型之一在其它应用和系统窗口上方显示提醒窗口,都会显示在TYPE_APPLICATION_OVERLAY
窗口类型的下方。
TYPE_PHONE
TYPE_PRIORITY_PHONE
TYPE_SYSTEM_ALERT
TYPE_SYSTEM_OVERLAY
TYPE_SYSTEM_ERROR
TYPE_TOAST
如果该应用的targetSdkVersion >= 26
,则应用只能使用TYPE_APPLICATION_OVERLAY
窗口类型来创建悬浮窗。
4.3 透明窗口不允许锁定屏幕旋转
之前应用中的侧滑返回方案需要将窗口设为透明,但是由于没有适配横屏,因此将其屏幕方法锁定为竖屏。
<activity
android:name=".circle.activity.CircleListActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:screenOrientation="portrait"
android:theme="@style/Base.Theme.CirclePage" />
窗口透明 + 固定屏幕方向 会抛出下面的异常:
Caused by: java.lang.IllegalStateException: Only fullscreen opaque activities can request orientation
解决方案有两种:
- 适配横屏,去掉固定屏幕方向的限制。
- 仅在滑动开始的时候设置窗口透明,具体实现方案 SWipeBack。
五、Android 9.0 适配
5.1 明文流量的网络请求
Android 9.0
限制了明文流量的网络请求,非加密的流量请求都会被系统禁止。
- 在
res/xml
文件夹下新建network_security_config.xml
:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
- 在
AndroidManifest.xml
的<application
标签下进行配置:
<application
android:networkSecurityConfig="@xml/network_security_config">
</application>