Android版本差异适配方案(5.0-9.0)

Android版本差异适配方案(5.0-9.0)

一个好的APP最好支持90%设备,由于不同版本系统提供的API可能不同,所以了解不同版本间系统差异很重要,这样才能更好的适配更多的智能设备。你的应用足不足够健壮要看你的应用在主流版本运行是否流畅。这篇文章记录开发过程中遇到的相对重要以及常用的适配方案,希望对读者有所帮助。

Android 版本号及对应的版本名

版本号 版本名 中文名
API Q android Q
API 28 android 9.0 Pie 馅饼
API 27 android 8.1 Oreo 奥利奥
API 26 android 8.0 Oreo 奥利奥
API 25 android 7.1 Nougat 牛轧糖
API 24 android 7.0 Nougat 牛轧糖
API 23 android 6.0 Marshmallow 棉花糖
API 22 android 5.1 Lollipop 棒棒糖
API 21 android 5.0 Lollipop 棒棒糖
API 20 android 4.4W KitKat 奇巧巧克力棒
API 19 android 4.4 KitKat 奇巧巧克力棒
API 18 android 4.3 Jelly Bean 果冻豆
API 17 android 4.2 Jelly Bean 果冻豆
API 16 android 4.1 Jelly Bean 果冻豆
API 15 android 4.0.3 ~4.0.4 Ice Cream Sandwich 冰淇淋三明治
API 14 android 4.0 ~ 4.0.2 Ice Cream Sandwich 冰淇淋三明治
API 13 android 3.2 Honeycomb 蜂巢
API 12 android 3.1 Honeycomb 蜂巢
API 11 android 3.0 Honeycomb 蜂巢
API 10 android 2.3.3 ~ 2.3.7 Gingerbread 姜饼
API 9 android 2.3~ 2.3.2 Gingerbread 姜饼
API 8 android 2.2~ 2.2.3 Froyo 冻酸奶
API 7 android 2.1 Éclair 闪电泡芙
API 6 android 2.0.1 Éclair 闪电泡芙
API 5 android2.0 Éclair 闪电泡芙
API 4 android 1.6 Donut 甜甜圈
API 3 android 1.5 ICupcake 纸杯蛋糕
API 2 android 1.1
API 1 android 1.0

Android5.0

1、Android Runtime (ART)

Android运行时由Android核心库集和Dalvike虚拟机改成Android核心库集和ART(Android Runtime)模式。两者的区别就是Dalvike虚拟机采用了一种被称为JIT(just-in-time)的解释器进行动态编译,而ART模式则在用户安装App是进行预编译AOT(Ahead-of-time),将android5.X的运行速度提高了3倍左右。

ART的特性:
1: 用户安装应用时就进行预编译操作,将原本在程序运行中时的编译动作提前到应用安装时。在省去解释代码这一过程之后,应用的运行效率会更高。
缺点:(1) 安装时间增加 (2) 安装后的文件占用更多空间?(外存储器)
2: 解决垃圾回收 (GC) 问题
在 Dalvik 中,应用常常发现显式调用 System.gc() 非常有用,可促进垃圾回收 (GC)。对 ART 而言这种做法的必要性低得多,尤其是当您需要通过垃圾回收来预防出现 GC_FOR_ALLOC 类型或减少碎片时。
而且,Android 开源项目 (AOSP) 中正在开发一种紧凑型垃圾回收器,以改善内存管理。
3:预防 JNI 问题
ART 的 JNI 比 Dalvik 的 JNI 更为严格一些。使用 CheckJNI 模式来捕获常见问题是一种特别实用的方法。
1): 检查 JNI 代码中的垃圾回收问题
2): 错误处理 ART 的 JNI 会在多种情况下引发错误,而 Dalvik 则不然。(同样地,您可以通过使用 CheckJNI 执行测试来捕获大量此种情况)
3): 预防堆栈大小问题  Dalvik 具有单独的原生代码堆栈和 Java 代码堆栈,并且默认的 Java 堆栈大小为 32KB,默认的原生堆栈大小为 1MB。

2、Button将总是位于最上层

从5.0开始,在同一个layout下,就算你在Button上覆盖了相应的View,Button将总是位于最上层。产生原因:stateListAnimator属性。谷歌在Material Design中推出,是一个非常简单的方法用来实现在可视状态之间平滑过渡。这个属性可以通过android:stateListAnimator进行设置,可以使控件在点击时产生不同的交互。对于Button,点击时默认有个阴影的效果用于表示按下的状态(5.0以前就是简单的变色)。 解决方法:可以使用 android:stateListAnimator="@null" 去掉阴影效果而使Button可以被正常的覆盖。

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:stateListAnimator="@null"/>

Android6.0

1、动态权限

动态权限适配是 Android 6.0 最先开始的,也是 Android 系统对开发者影响最大的改动之一。系统权限主要分为两类,正常权限和危险权限。不管哪个版本的android,你应用中所用到的所有权限,不管是正常权限还是危险权限,都需要在应用Manifest中申明。你的目标SDK(targetSdkVersion)是23以及23以上版本:应用必须在Manifest中罗列出所有的权限,并且在程序运行时,它必须请求用户授予每一个危险权限,此时用户可以授予或者拒绝每一个权限,并且应用程序可以继续运行有限的功能,即使用户拒绝了权限请。在 Android 6.0 ~ Android 8.0中,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用,即对于同一组内的权限,只要有一个被同意,其他的都会被同意。在 Android 8.0 之后,此行为已被纠正。系统只会授予应用明确请求的权限。然而一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都将被自动批准,但是若没有请求相应的权限而进行操作的话就会出现应用 crash 的情况。

危险权限分组说明

权限组 权限名称
CALENDAR android.permission.READ_CALENDAR
android.permission.WRITE_CALENDAR
CAMERA android.permission.CAMERA
CALENDAR android.permission.READ_CALENDAR
android.permission.WRITE_CALENDAR
CONTACTS android.permission.READ_CONTACTS
android.permission.WRITE_CONTACTS
android.permission.GET_ACCOUNTS
LOCATION android.permission.ACCESS_FINE_LOCATION
android.permission.ACCESS_COARSE_LOCATION
MICROPHONE android.permission.RECORD_AUDIO
PHONE android.permission.READ_PHONE_STATE
android.permission.CALL_PHONE
android.permission.READ_CALL_LOG
android.permission.ADD_VOICEMAIL
android.permission.WRITE_CALL_LOG
android.permission.USE_SIP
android.permission.PROCESS_OUTGOING_CALLS
android.permission.ANSWER_PHONE_CALLS(8.0新增)
android.permission.READ_PHONE_NUMBERS(8.0新增)
SENSORS android.permission.BODY_SENSORS
SMS android.permission.SEND_SMS
android.permission.RECEIVE_SMS
android.permission.READ_SMS
android.permission.RECEIVE_WAP_PUSH
android.permission.RECEIVE_MMS
STORAGE android.permission.READ_EXTERNAL_STORAGE
android.permission.WRITE_EXTERNAL_STORAGE

对应在清单文件中的展示

<!--CALENDAR-->
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
<!--CAMERA-->
<uses-permission android:name="android.permission.CAMERA"/>
<!--CONTACTS-->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS"/>
<!--LOCATION-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!--MICROPHONE-->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!--PHONE-->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.ADD_VOICEMAIL"/>
<uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
<uses-permission android:name="android.permission.USE_SIP"/>
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS"/>
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
<!--SENSORS-->
<uses-permission android:name="android.permission.BODY_SENSORS"/>
<!--SMS-->
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_WAP_PUSH"/>
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
<!--STORAGE-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

2、规避动态权限

如果想规避动态权限策略也是可以的,配置以下

android {
    ...
    defaultConfig {
        ...
        targetSdkVersion 22 // 不使用api:23以及以上的动态权限策略
        ...
    }
}

3、Wifi相关操作

Android6.0之后,Wifi的使用更加严格。需要动态获取LOCATION权限,如果还想获取Wifi列表的话还需要打开GPS(位置信息)。

  • 首先在AndroidManifest.xml文件中增加以下权限
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission>
  • 其次还需要动态申请定位权限组
ActivityCompat.requestPermissions(getActivity(),new String[]{Manifest.permission.ACCESS_FINE_LOCATION,Manifest.permission.ACCESS_COARSE_LOCATION},REQUEST_CODE_ACCESS_COARSE_LOCATION);
  • 最后如果是用getScanResults()获取Wifi列表的话还需要打开GPS(位置信息)开关。
if(!isGPSOpen()){
    //跳转到手机原生设置页面,打开定位功能
    Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
    this.startActivityForResult(intent,GPS_REQUEST_CODE);
}else{
    //你的业务逻辑
}

/**
 * 检查有没打开定位
 */
private boolean isGPSOpen() {
    LocationManager locationManager = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE);
    return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
}

Android7.0

1、FileProvider

在官方7.0的以上的系统中,尝试传递 file://URI可能会触发FileUriExposedException。要应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider类。

使用FileProvider授权

1)、创建新的FileProvider

当你需要以独立的模块分享出去,需要继承FileProvider,创建新的FileProvider,防止与主工程有冲突

/**
 * 继承FileProvider,防止冲突
 */
public class RoProvider extends FileProvider {

}

2)、创建file_path.xml

"." 表示共享该目录下所有的文件

<?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="external-files" path="" />
    <external-cache-path name="external-cache" path="" />
</paths>

各个标签代表的意义

name path
名称标志字符串,不可以同名 文件夹“相对路径”,完整路径取决于当前的标签类型
标签 路径
root-path 代表设备的根目录new File("/")
files-path 代表context.getFilesDir()
cache-path 代表context.getCacheDir()
external-path 代表Environment.getExternalStorageDirectory()
external-files-path 代表context.getExternalFilesDirs()
external-cache-path 代表context.getExternalCacheDirs()

举例

<external-path name="external" path="pics" />代表的目录即为:Environment.getExternalStorageDirectory()/pics,其他同理。

3)、 注册FileProvider

authorities:一个标识,在当前系统内必须是唯一值,一般用包名。
exported:表示该 FileProvider 是否需要公开出去。
granUriPermissions:是否允许授权文件的临时访问权限。这里需要,所以是 true。

<provider
    android:name=".RoProvider"
    android:authorities="${applicationId}.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

禁用FileProvider授权

绕过版本限制,删除Uri的检测,这样就可以绕过7.0的文件共享限制

/**
 * 需要在Application中执行
 */
private void detectFileUriExposure() {
    Builder builder = new Builder();
    StrictMode.setVmPolicy(builder.build());
    builder.detectFileUriExposure();
}

2、APK signature scheme v2

Android 7.0 引入一项新的应用签名方案 APK Signature Scheme v2,它能提供更快的应用安装时间和更多针对未授权 APK 文件更改的保护。在默认情况下,Android Studio 2.2 和 Android Plugin for Gradle 2.2 会使用 APK Signature Scheme v2 和传统签名方案来签署您的应用。

<img src="https://upload-images.jianshu.io/upload_images/7912789-da6e85a74c243749.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp"/>

说明

  • 只勾选V1签名就是传统方案签署,但是在 Android 7.0 上不会使用V2安全的验证方式。
  • 只勾选V2签名7.0以下会显示未安装,Android 7.0 上则会使用了V2安全的验证方式。
  • 同时勾选V1和V2则所有版本都没问题。

3、org.apache不支持问题

build.gradle里面加上这句话

defaultConfig {
    useLibrary 'org.apache.http.legacy'
}

或者在AndroidManifest.xml添加下面的配置

<uses-library
    android:name="org.apache.http.legacy"
    android:required="false" />

4、SharedPreferences闪退

// MODE_WORLD_READABLE:Android 7.0以后不能使用这个获取,会闪退
// 应修改成MODE_PRIVATE
SharedPreferences read = getSharedPreferences(RELEASE_POOL_DATA, MODE_WORLD_READABLE);

5、三个广播被禁止监听或发送

CONNECTIVITY_CHANGE 广播

在后台时不再能接收到 CONNECTIVITY_CHANGE 广播,前台不影响。

ACTION_NEW_PICTURE 和 ACTION_NEW_VIDEO 广播

不能发送或是接收新增图片(ACTION_NEW_PICTURE)和新增视频(ACTION_NEW_VIDEO) 的广播。

Android8.0

1、Notification(通知权限)

Android 8.0之后通知权限默认都是关闭的,无法默认开启以及通过程序去主动开启,需要程序员读取权限开启情况,然后提示用户去开启。

  • 判断权限是否开启
/**
 * 判断通知权限是否开启
 * @param context 上下文
 */
public static boolean isNotificationEnabled(Context context){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).areNotificationsEnabled();
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
        ApplicationInfo appInfo = context.getApplicationInfo();
        String pkg = context.getApplicationContext().getPackageName();
        int uid = appInfo.uid;

        try {
            Class<?> appOpsClass = Class.forName(AppOpsManager.class.getName());
            Method checkOpNoThrowMethod = appOpsClass.getMethod("checkOpNoThrow", Integer.TYPE, Integer.TYPE, String.class);
            Field opPostNotificationValue = appOpsClass.getDeclaredField("OP_POST_NOTIFICATION");
            int value = (Integer) opPostNotificationValue.get(Integer.class);
            return (Integer) checkOpNoThrowMethod.invoke(appOps, value, uid, pkg) == 0;
        } catch (NoSuchMethodException | NoSuchFieldException | InvocationTargetException | IllegalAccessException | RuntimeException | ClassNotFoundException ignored) {
            return true;
        }
    } else {
        return true;
    }
}
  • 前往设置开启权限
/**
 * 打开设置页面打开权限
 *
 * @param activity activity
 * @param requestCode 这里的requestCode和onActivityResult中requestCode要一致
 */
public static void startSettingActivity(@NonNull Activity activity, int requestCode) {
    try {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + activity.getPackageName()));
        intent.addCategory(Intent.CATEGORY_DEFAULT);
        activity.startActivityForResult(intent, requestCode);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

2、Notification(通知适配)

Android 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);
    }
}

<img src="https://upload-images.jianshu.io/upload_images/3018964-006db30436c92ddb.png?imageMogr2/auto-orient/strip|imageView2/2/w/635/format/webp"/>

3、自适应启动图标

从Android 8.0系统开始,应用程序的图标被分为了两层:前景层和背景层。也就是说,我们在设计应用图标的时候,需要将前景和背景部分分离,前景用来展示应用图标的Logo,背景用来衬托应用图标的Logo。需要注意的是,背景层在设计的时候只允许定义颜色和纹理,但是不能定义形状。注意图标图层的大小,两层的尺寸必须为108x108dp,前景图层中间的72x72dp图层就是在手机界面上展示的应用图标范围。这样系统在四面各留出18dp以产生有趣的视觉效果,如视差或脉冲(动画视觉效果由受支持的启动器生成,视觉效果可能因发射器而异)。

  • mipmap-anydpi-v26文件夹
<?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>

4、安装APK

Android 8.0去除了“允许未知来源”选项,如果我们的App具备安装App的功能,那么AndroidManifest文件需要包含REQUEST_INSTALL_PACKAGES权限,未声明此权限的应用将无法安装其他应用。当然,如果你不想添加这个权限,也可以通过getPackageManager().canRequestPackageInstalls()查询是否有此权限,没有的话使用Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES这个action将用户引导至安装未知应用权限界面去授权。

<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

5、SecurityException的闪退

项目使用了ActiveAndroid,在 8.0 或 8.1 系统上使用 26 或以上的版本的 SDK 时,调用 ContentResolver 的 notifyChange 方法通知数据更新,或者调用 ContentResolver 的 registerContentObserver 方法监听数据变化时,会出现上述异常。解决方案:

  • 方案1、在清单文件配置。
<provider
   android:name="com.activeandroid.content.ContentProvider"
   android:authorities="com.jz.androidclient"
   android:enabled="true"
   android:exported="false"/>
  • 方案2、去掉这个监听刷新的方法,改为广播刷新。

6、静态广播无法正常接收

Google官方声明:从android 8.0(API26)开始,对清单文件中静态注册广播接收者增加了限制,建议大家不要在清单文件中静态注册广播接收者,改为动态注册。当然,如果你还是想用静态注册的方式也是有方法的,Intent里添加Component参数可实现。

  • 发送静态广播的特殊处理
Intent intent = new Intent( "广播的action" );
intent.setComponent( new ComponentName( "包名(如:com.yhd.rocket)","接收器的完整路径(如:com.yhd.rocket.receiver.RoReceiver)" ) );
sendBroadcast(intent);

Android9.0

1、刘海屏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 = msg +"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;
}
  • 设置代码
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
getWindow().setAttributes(lp);

2、CLEARTEXT communication to http://xxx not permitted by network security policy

问题原因: Android P 限制了明文流量的网络请求,非加密的流量请求(http)都会被系统禁止掉。解决方案:

  • 方案一:将http请求改为https
  • 方案二:添加usesCleartextTraffic属性
<application
    android:usesCleartextTraffic="true">
</application>        
  • 方案三:添加资源文件(复杂)

1、在资源文件新建xml目录,新建文件network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

2、清单文件配置:

<application
    android:networkSecurityConfig="@xml/network_security_config">
</application>

3、java.lang.IllegalArgumentException: Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed

在自定义绘制View过程中会遇到 Android 9.0 兼容问题导致的Crash,解决方案:

if (Build.VERSION.SDK_INT >= 26){
  canvas.clipPath(mPath); 
} else {
  canvas.clipPath(mPath, Region.Op.REPLACE);
}

4、前台服务需要添加权限

在安卓9.0版本之后,必须要授予FOREGROUND_SERVICE权限,才能够使用前台服务,否则会抛出异常。对此,我们只需要在AndroidManifest添加对应的权限即可,这个权限是普通权限,不需要动态申请。

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

5、全面限制静态广播的接收

升级安卓9.0之后,隐式广播将会被全面禁止,在AndroidManifest中注册的Receiver将不能够生效,你需要在应用中进行动态注册。

MyReceiver myReceiver = new MyReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(MY_ACTION);
registerReceiver(myReceiver, intentFilter);

6、非 SDK 接口访问限制

在 Android 9.0 版本中,谷歌加入了非 SDK 接口使用限制,无论是通过调用、反射还是JNI等方式,开发者都无法对非 SDK 接口进行访问,此接口的滥用将会带来严重的系统兼容性问题。 在开发过程中,开发者如果调用了非 SDK 接口,会导致应用出现crash,无法启动;或在运行过程中出现崩溃、闪退等现象;也可能导致应用功能不可用等严重兼容性问题,其影响范围波及所有调用此接口的应用。

那么什么是非SDK接口呢,所谓非SDK接口就是所有不能够在谷歌官网上查询到的接口,谷歌提供了 查询接口的网站

  • 例如我们通过反射修改Dialog窗体的颜色

此方法在安卓9.0版本将不能够正常运行,会抛出NoSuchFieldException,对于诸如此类的调用官方private方法或者@hide方法,都将不能使用。

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();
}

7、Apache HTTP 客户端弃用

将 compileSdkVersion 升级到 28 之后,如果在项目中用到了 Apache HTTP client 的相关类,就会抛出找不到这些类的错误。这是因为官方已经在 Android P 的启动类加载器中将其移除,如果仍然需要使用 Apache HTTP client.在 Manifest 文件中加入:

<uses-library 
    android:name="org.apache.http.legacy" 
    android:required="false"/>

8、Calandar(日历)

Android 9.0日历的时间戳小于0

  • Android 9.0
long timeMil = Calendar.getInstance().getTimeInMillis();//timeMil < 0
  • Android 9.0以前
long timeMil = Calendar.getInstance().getTimeInMillis();//timeMil > 0

关于我

持续更新中...