玩转Android版本适配

363 阅读7分钟

Android各个版本特性介绍及适配

一、Android 6(api 23)

  • 运行时权限

    对于Android 6.0以上系统的手机,以下危险权限需要使用 checkSelfPermission()方法来鉴别是否有该权限,如果没有该权限,需要使用requestPermissions()来申请权限

    1、相机权限

    Manifest.permission.CAMERA

    2、存储权限

    Manifest.permission.READ_EXTERNAL_STORAGE 读取存储权限

    Manifest.permission.WRITE_EXTERNAL_STORAGE 写入存储权限

    3、录音权限

    Manifest.permission.RECORD_AUDIO

    4、电话权限

    Manifest.permission.CALL_PHONE 拨号权限

    Manifest.permission.READ_PHONE_STATE

    5、定位权限

    Manifest.permission.ACCESS_FINE_LOCATION

    Manifest.permission.ACCESS_COARSE_LOCATION

    6、日历权限

    Manifest.permission.READ_CALENDAR 读取日历权限

    Manifest.permission.WRITE_CALENDAR 写入日历权限

    7、通讯录权限

    Manifest.permission.WRITE_CONTACTS 写入联系人权限

    Manifest.permission.READ_CONTACTS 读取联系人权限

    Manifest.permission.GET_ACCOUNTS 获取设备账号权限

    判断是否有该权限示例:

      //检查是否有相机权限
      ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
              == PackageManager.PERMISSION_GRANTED;
    

    请求示例:

      //请求读取日历权限
      ActivityCompat.requestPermissions(activity,
          new String[]{Manifest.permission.READ_CALENDAR,
                  Manifest.permission.WRITE_CALENDAR},
          REQ_CODE.CALENDAR); //REQ_CODE.CALENDAR为选择后的回调requestCode
          
    

    请求结束回调:

    //待完善

    如果在Android 6.0及以上手机中,不判断并且请求相关权限,会发生崩溃哦

    在实际开发中,笔者比较喜欢用RxPermission的第三方库,以观察者模式的方式进行回调的监听,比较方便哦~

  • 取消支持 Apache HTTP 客户端

  • 通知

  • 音频管理器变更

二、Android 7(api 24)

  • 隐式广播

    Android 7.0 移除了三项隐式广播,以帮助优化内存使用和电量消耗。此项变更很有必要,因为隐式广播会在后台频繁启动已注册侦听这些广播的应用。删除这些广播可以显著提升设备性能和用户体验。

    移动设备会经历频繁的连接变更,例如在 WLAN 和移动数据之间切换时。目前,可以通过在应用清单中注册一个接收器来侦听隐式 CONNECTIVITY_ACTION 广播,让应用能够监控这些变更。由于很多应用会注册接收此广播,因此单次网络切换即会导致所有应用被唤醒并同时处理此广播。

    同理,在之前版本的 Android 中,应用可以注册接收来自其他应用(例如相机)的隐式 ACTION_NEW_PICTURE 和 ACTION_NEW_VIDEO 广播。当用户使用相机应用拍摄照片时,这些应用即会被唤醒以处理广播。

    为缓解这些问题,Android 7.0 应用了以下优化措施:

    面向 Android 7.0 开发的应用不会收到 CONNECTIVITY_ACTION 广播,即使它们已有清单条目来请求接受这些事件的通知。在前台运行的应用如果使用 BroadcastReceiver 请求接收通知,则仍可以在主线程中侦听 CONNECTIVITY_CHANGE。 应用无法发送或接收 ACTION_NEW_PICTURE 或 ACTION_NEW_VIDEO 广播。此项优化会影响所有应用,而不仅仅是面向 Android 7.0 的应用。 如果您的应用使用任何 intent,您仍需要尽快移除它们的依赖关系,以正确适配 Android 7.0 设备。Android 框架提供多个解决方案来缓解对这些隐式广播的需求。例如,JobScheduler API 提供了一个稳健可靠的机制来安排满足指定条件(例如连入无限流量网络)时所执行的网络操作。您甚至可以使用 JobScheduler 来适应内容提供程序变化。 --- 参考自Google Android 官方文档

    当你想要让之前老代码中的隐式广播能够继续正常work,只需要增加一行代码就可以解决:

          intent.setPackage(ContextHolder.getContext().getPackageName());
    

    设置Package就可以正常work了

  • 应用间共享文件

    此前获取uri路径方法: Uri.fromFile(new file(localPath))

    如果还用此方法会出现crash

    这时候就要使用到推荐的FileProvider来解决此问题

    首先需要在Manifest清单中注册我们的FileProvider

      <provider
          android:name="android.support.v4.content.FileProvider"
          android:authorities="com.gnet.onemeeting.fileSelect.fileprovider"
          android:exported="false"
          android:grantUriPermissions="true">
    
          <!-- 元数据 -->
          <meta-data
              android:name="android.support.FILE_PROVIDER_PATHS"
              android:resource="@xml/file_paths" />
      </provider>
    

    authorities为作者,在调用中需要使用到作者

    exported = false 是设置让其他程序不可以调用到我的FileProvider

    grantUriPermissions 属性值为“true”,则该 Provider 内所有的数据都能被授权访问。 可它如果设为“false”,则只有本元素指定的数据子集才能被授权

    那么@xml/file_paths是什么呢?这是我们能够获取并且加密的所有文件,需要自己定义

      <?xml version="1.0" encoding="utf-8"?> 
      <paths xmlns:android="http://schemas.android.com/apk/res/android"> 
          <files-path name="gnet_sdk_files" path="." /> 
          <external-path name="gnet_sdk_external" path="." /> 
          <cache-path name="gnet_sdk_cache" path="." /> 
          <external-files-path name="gnet_sdk_external_files" path="." /> 
          <external-cache-path name="gnet_sdk_external_cache" path="." /> 
      </paths>
    

    path = "."path中设置可获取哪些文件,"."代表可获取此路径下所有文件

    files-path 代表 /data/user/0/<package_name>/files或者/data/data/<package_name>/files这两个目录指向相同的位置

    external-path/storage/emulated/0或者/sdcard/

    cache-path 代表代表/data/user/0/<package_name>/cache或者/data/data/<package_name>/cache

    external-files-path代表/storage/emulated/0/Android/data/<package_name>/files 或者 /sdcard/Android/data/<package_name>/files

    external-cache-path代表/storage/emulated/0/Android/data/<package_name>/cache 或者 /sdcard/Android/data/<package_name>/cache

    name为被加密后被暴露出去的时候的路径文件名称

    使用方法为: FileProvider.getUriForFile(context, context.getPackageName() + ".fileSelect.fileprovider", new File(localPath)); 第二个属性为之前manifest中配置的authorities(作者)

三、Android 8(api 26)

  • 后台限制

    问题:针对 Android 8.0 的应用尝试在不允许其创建后台服务的情况下使用 startService() 函数,则该函数将引发一个 IllegalStateException

    解决方案:新的 Context.startForegroundService() 函数将启动一个前台服务。现在,即使应用在后台运行,系统也允许其调用 Context.startForegroundService()。不过,应用必须在创建服务后的五秒内调用该服务的 startForeground() 函数。

    此方案会在用户的下拉更多窗口中看到一个常显的通知栏,告诉用户你的app正在前台运行

    示例:

      Intent intent = new Intent(context, LogService.class);
      try {
      if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//手机大于Android8及以上系统
          context.startForegroundService(intent);//使用前台服务
      } else {
          context.startService(intent);
      }
      } catch (Exception e) {
          LogUtil.w(TAG, "startService -> failed, exception :", e);
      }
    

    然后需要在LogService的onStartCommond()生命周期中进行startForeGround()方法开启前台,来解除5s的anr

    String channelId = "com_gnet_bee_channel_id";   //通道的id,可随意选取,但是要在新建Notification时保持一致 
    String channelName = service.getString(R.string.uc_foreground_service_name);  //通道的name 
    NotificationManager manager = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE);
    NotificationChannel channel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE); 
    if (manager != null) {
        manager.createNotificationChannel(channel); //创建通道 
        Notification notification = new Notification.Builder(service.getApplicationContext(), channelId).build();
        service.startForeground(666, notification);
    }           
    String channelId = "com_gnet_bee_channel_id";   //通道的id,可随意选取,但是要在新建Notification时保持一致 
    String channelName = service.getString(R.string.uc_foreground_service_name);  //通道的name 
    NotificationManager manager = (NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE);
    NotificationChannel channel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE); 
    if (manager != null) {
        manager.createNotificationChannel(channel); //创建通道 
        Notification notification = new Notification.Builder(service.getApplicationContext(), channelId).build();
        service.startForeground(666, notification);//开启前台
    }
  • 语言区域和国际化

Android 7.0(API 级别 24)引入能指定默认类别语言区域的概念,但是某些 API 在本应使用默认 DISPLAY 类别语言区域时,仍然使用不带参数的通用 Locale.getDefault() 函数。现在,在 Android 8.0 中,以下函数使用 Locale.getDefault(Category.DISPLAY) 来代替 Locale.getDefault()

Currency.getDisplayName() Currency.getSymbol() Locale.getDisplayScript() 当为 Locale 参数指定的 displayScript 值不可用时,Locale.getDisplayScript(Locale) 同样回退到 Locale.getDefault()

与语言区域和国际化有关的其他变更如下:

调用 Currency.getDisplayName(null) 会引发 NullPointerException,以与文档规定的行为保持一致。 时区名称的分析方法发生变化。之前,Android 设备使用在启动时取样的系统时钟值,缓存用于分析日期时间的时区名称。因此,如果在启动时或其他较为罕见的情况下系统时钟出错,可能对分析产生负面影响。 现在,一般情况下,在分析时区名称时分析逻辑将使用 ICU 和当前系统时钟值。此项变更可提供更加准确的结果,如果您的应用使用 SimpleDateFormat 等类,此结果可能与之前的 Android 版本不同。--- 摘自Google Android官方文档

  • 提醒窗口
  • 悬浮窗

在Android 8.0以上手机上,需要使用TYPE_APPLICATION_OVERLAY类型,不可以使用TYPE_PHONE

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
    mLayoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}

四、Android 9(P)(api 28)

  • 限制Http网络请求 Android 9.0 中限制了 HTTP(明文传输)网络请求,若仍继续使用HTTP请求,则会在日志中提示以下异常(只是无法正常发出请求,不会导致应用崩溃):

适配方法如下:

在 AndroidManifest.xml application添加

useCleartextTraffic="true"
  • 弃用 Apache HTTP Client 由于官方在 Android 9.0 中移除了所有 Apache HTTP Client 相关的类,因此我们的应用或是一些第三方库如果使用了这些类,就会抛出找不到类的异常:
java.lang.NoClassDefFoundError: Failed resolution of: Lorg/apache/http/conn/scheme/SchemeRegistry;

适配方法如下:

在AndroidManifest.xml中添加下述配置

<uses-library android:name="org.apache.http.legacy" android:required="false"/>
  • Build.SERIAL 弃用

Android 9.0 之前,开发者可以使用 Build.SERIAL 获取设备的序列号。现在这个方法被弃用了,Build.SERIAL 将始终设置为 "UNKNOWN" 以保护用户的隐私。

适配的方法为先请求 READ_PHONE_STATE 权限,然后调用 Build.getSerial() 方法。

五、Android 10(Q)(api 29)

未完待续......