Android-8.0适配

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/yang1349day/article/details/80016607

本知识点只是个人见解,具体知识及使用请查阅官网,以免被误导,同时大家可以对此文发表自己的见解。

阅读本文之前最好先看看官网的对8.0系统的全面讲解

我们需要自己动手去查看新特性,以便用已知的知识来解决未知的问题

https://developer.android.google.cn/about/versions/oreo/android-8.0.html (此处是中文版的google开发者文档)


8.0新增和优化了很多功能,以上是部分截图



再来看看本文的适配内容包括:

1 启动页适配

2 android 8.0 手机版本更新,系统内新版本安装适配

3 渠道通知适配

4 图标适配

5 超长屏幕尺寸适配

6 后台执行限制适配建议(对后台服务和隐式广播的限制)

最后给出了一些获取最新andrid系统的网站



1 启动页适配

新版本刚刚发,显示就用用户反馈更新后无法启动,然后我就赶紧去找bug,发现了这个错误.

有一句是这样的Only fullscreen opaque activities can request orientation,也就是说只有全屏不透明的activity才可以设置方向,既然知道问题所在就好办了。

原因

出现这样的问题,绝大多数都是因为我们为了提高用户体验,手动取消App启动白屏或者黑屏的时候,将Splash界面设为了透明,然后这个时候又设置了方向为垂直,从而导致了这个问题。


解决

a 如果不考虑配置屏幕方向的话,直接去掉方向的设置即可解决问题

android:screenOrientation="portrait" 去掉即可

注意:此处需要注意保存好页面数据,否则屏幕旋转会导致页面重新创建。


b 如果,去掉透明属性的话,就需要解决app启动白屏(黑屏)的问题,白屏或者黑屏是根据App使用的主题来显示的,下面会给出官方的解决方案

如果既要固定屏幕方向,又解决启动过慢导致的白(黑)屏问题,以下办法可以解决:

1.找到你设置透明的Activity,然后在他的theme中将android:windowIsTranslucent改为false
eg:<item name="android:windowIsTranslucent">false</item>

2.再加入<item name="android:windowDisablePreview">true</item>就搞定了。

注意:使用 windowDisablePreview主题属性是用来关闭启动应用程序时系统进程绘制的初始空白屏幕。但是,与未禁止预览窗口的应用程序相比,此方法可能会导致更长的启动时间。此外,它会强制用户在活动启动时等待而没有任何反馈,无法让用户知道应用程序是否正常工作。

Goodle针对以上启动白屏或者黑屏给出了解决方案:
我们建议您不要禁用预览窗口,而要遵循常见的 Material Design模式。您可以使用活动的 windowBackground主题属性为开始活动提供简单的自定义绘图。
例如,您可以创建一个新的可绘制文件,并从布局XML和应用程序清单文件中引用它,如下所示:
Layout XML file:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
  <!-- The background color, preferably the same as your normal theme -->
  <item android:drawable="@android:color/white"/>
  <!-- Your product logo - 144dp color version of your app icon -->
  <item>
    <bitmap
      android:src="@drawable/product_logo_144dp"
      android:gravity="center"/>
  </item>
</layer-list>

然后设置<item name="android:windowBackground">@drawable/your xml file</item>

然后在Manifest file声明activity的时候添加主题:
<activity ...android:theme="@style/AppTheme.Launcher" />
ok,大功告成!!

如果需要转回正常主题,转换回正常主题的最简单方法是在调用 setTheme(R.style.AppTheme) 之前调用super.onCreate()setContentView()
public class MyMainActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // Make sure this is before calling super.onCreate
    setTheme(R.style.Theme_MyApp);
    super.onCreate(savedInstanceState);
    // ...
  }
}


2  8.0的新版本的安装适配

安装适配问题在8.0及以上的模拟器上面是需要的,不然无法安装设备。但是亲测,在小米的8.0部分机型和华为部分8.0机型即使没有适配也是可以安装的,安装前勾选下未知来源apk的风险即可。所以在新版本apk能够正常覆盖的情况下,就不必适配了,当然适配是个坑,用不用看大家自己了

解决:
1、在清单文件中增加请求安装权限

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

2、申请打开未知来源权限。

3、打开未知来源授权列表,开启权限。

权限申请部分用到的是AndPermission,具体使用方法参考wiki  http://www.yanzhenjie.com/AndPermission/

大家用自己原来的权限申请逻辑也是可以的,我做好了注释.

public static boolean installApp(final Context context, final File appFile) {

    try {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //版本判断
            boolean b = context.getPackageManager().canRequestPackageInstalls();
            if (b) {
                toInstallApp(context, appFile); //有权限就直接安装
            } else {
                sDefaultRationale = new DefaultRationale(); //申请权限的对话框说明
                sPermissionSetting = new PermissionSetting(context);//申请权限失败,跳转到设置界面

                //栈顶没有activity,无法请求权限
                if (com.reds.didi.view.ActivityManager.getTopActivity() == null){
                    return false;
                }
                AndPermission.with(com.reds.didi.view.ActivityManager.getTopActivity())
                        .permission(Manifest.permission.REQUEST_INSTALL_PACKAGES)//申请权限
                        .rationale(sDefaultRationale)
                        .onGranted(new Action() {
                            @Override
                            public void onAction(List<String> permissions) {
                                ToastUtil.showToast("权限设置成功!");
                                toInstallApp(context, appFile); //申请成功直接安装
                            }
                        })
                        .onDenied(new Action() {
                            @Override
                            public void onAction(@NonNull List<String> permissions) {
                                ToastUtil.showToast("权限设置失败!");
                                toInstallApp(context, appFile);//申请失败也安装,万一成功了呢?

                                if (AndPermission.hasAlwaysDeniedPermission(com.reds.didi.view.ActivityManager.getTopActivity(), permissions)) {
                    //用户总是拒绝权限,则引导只设置界面,防止找不到开启的位置
                                    sPermissionSetting.showSetting(permissions);
                                }
                            }
                        })
                        .start();
            }
        } else {
            toInstallApp(context, appFile);
        }
        return true;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return false;
}

private static void toInstallApp(Context context, File appFile) {
    if (sPermissionSetting != null) {
        sPermissionSetting.release();
        sPermissionSetting = null;
    }
    if (sDefaultRationale != null) {
        sDefaultRationale.release();
        sDefaultRationale = null;
    }

    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //此处涉及到的android7.0的适配,后面欧文给出了具体方案
        Uri fileUri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".fileProvider", appFile);
        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
    } else {
        intent.setDataAndType(Uri.fromFile(appFile), "application/vnd.android.package-archive");
    }
    if (context.getPackageManager().queryIntentActivities(intent, 0).size() > 0) {
        context.startActivity(intent);
    }
}
Android7.0的适配方案,详情参考:https://www.jianshu.com/p/56b9fb319310

这里直接给出7.0的适配方案

1 创建类FileProvider

import android.support.v4.content.FileProvider;

public class UpdateFileProvider extends FileProvider {
}
2 在AndroidManifest文件中声明<provider>标签

<provider
    android:name="com.reds.didi.weight.version.UpdateFileProvider"
    android:authorities="${applicationId}.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/new_app_file_paths"/>
</provider>

3 在res目录下创建xml文件,然后创建


<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path
        name="external_files"
        path="." />
    <root-path
        name="root_path"
        path="." />
</paths>

ok! 具体用法上面给出了哦!


3 系统通知适配

Google这次对于8.0系统通知渠道的推广态度还是比较强硬的。

如下异常:

# main(1)

android.app.RemoteServiceException

Bad notification for startForeground: java.lang.RuntimeException: invalid channel for service notification: Notification(channel=null pri=0 contentView=null vibrate=null sound=null defaults=0x0 flags=0x40 color=0x00000000 vis=PRIVATE)


同时首先,如果你升级了appcompat库,那么所有使用appcompat库来构建通知的地方全部都会进行废弃方法提示,如下所示:

上图告诉我们,此方法已废弃,需要使用带有通知渠道的方法才行。

当然,Google也并没有完全做绝,即使方法标为了废弃,但还是可以正常使用的。可是如果你将项目中的targetSdkVersion指定到了26或者更高,那么Android系统就会认为你的App已经做好了8.0系统的适配工作,当然包括了通知栏的适配。这个时候如果还不使用通知渠道的话,那么你的App的通知将完全无法弹出。因此这里给大家的建议就是,一定要适配。如果未适配成功,请留言给我!

先来看看我自己写的

/**
 * 创建通知
 */
private void setUpNotification() {
    if (mDismissNotificationProgress) {
        return;
    }

    mBuilder = new NotificationCompat.Builder(this,"newversion");
    mBuilder.setContentTitle("开始下载")
            .setContentText("正在连接服务器")
            .setSmallIcon(R.mipmap.lib_update_app_update_icon)
            .setLargeIcon(AppUpdateUtils.drawableToBitmap(AppUpdateUtils.getAppIcon(DownloadService.this)))
            .setOngoing(true)
            .setAutoCancel(true)
            .setWhen(System.currentTimeMillis());
    //8.0系统适配
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        mNotificationManager.createNotificationChannel(getNotificationChannel()); //在通知显示前调用
    }
    mNotificationManager.notify(NOTIFY_ID, mBuilder.build());
}
创建通知渠道号
@RequiresApi(api = Build.VERSION_CODES.O)
public NotificationChannel  getNotificationChannel(){ //设置quda
    String channelId = "newversion";
    String channelName = "升级夜游团新版本";
    int importance = NotificationManager.IMPORTANCE_DEFAULT;

    return new NotificationChannel(channelId, channelName, importance);
}

至于为什么需要适配渠道,渠道是什么,如何区分渠道,新的系统通知栏如何交互,

请看官方视频及介绍 

Android 8.0 Oreo 之推送通知的变化 | 中文教学视频 https://juejin.im/post/5a52dfe3f265da3e4e257762

或者郭霖大佬的博客

https://blog.csdn.net/guolin_blog/article/details/79854070



4 图标适配

如果你将targetSdkVersion指定到了26,但是却没有进行Android 8.0系统的应用图标适配,那么会出现什么样的效果呢?这里我举几个反面示例:


在androidStudio中,按下Windows:Ctrl+Shift+A / Mac:command+shft+A 快捷键,并输入Image Asset,如下所示:

点击回车键打开Asset Studio编辑器,在这里就可以进行应用图标适配了。

这个Asset Studio编辑器非常简单好用,一学就会。左边是操作区域,右边是预览区域。

先来看操作区域,第一行的Icon Type保持默认就可以了,表示同时创建兼容8.0系统以及老版本系统的应用图标。第二行的Name用于指定应用图标的名称,这里也保持默认即可。接下来的三个页签,Foreground Layer用于编辑前景层,Background Layer用于编辑背景层,Legacy用于编辑老版本系统的图标。

再来看预览区域,这个就十分简单了,用于预览应用图标的最终效果。在预览区域中给出了可能生成的图标形状,包括圆形、圆角矩形、方形等等。注意每个预览图标中都有一个圆圈,这个圆圈叫作安全区域,必须要保证图标的前景层完全处于安全区域当中才行,否则可能会出现图标被手机厂商的mask裁剪掉的情况。

更详细的具体细节和步骤,看大佬博客 https://blog.csdn.net/guolin_blog/article/details/79417483


5 超长屏幕尺寸适配

系统根据Android操作系统级别控制完成操作的方式:

  • 如果您的应用目标为Android 8.0(API级别26)或更高,则根据其布局填充整个屏幕。
  • 如果您的应用目标为Android 7.1(API等级25)或更低,则系统会将应用界面的大小限制为16:9(大约1.86)的窗口。如果应用程序在屏幕宽高比较大的设备上运行,则该应用程序会显示在16:9的信箱中,从而导致部分屏幕未使用。

如果您的应用布局无法适应任意大的纵横比,您可以通过设置最大纵横比在所有Android操作系统级别上明确声明手机的最大横纵比。我们建议比例为2.4(12:5)。

要为Android 8.0(API级别26)和更高级别设置最大宽高比,请 android:MaxAspectRatio在您的<activity>代码中使用最大比率 以下示例显示如何声明2.4的最大宽高比:

<!-- Render on full screen up to screen aspect ratio of 2.4 --><!-- Use a letterbox on screens larger than 2.4 -->

<activity android:maxAspectRatio="2.4"> ...</activity>

对于Android 7.1及更低版本,添加 <meta-data>元素中指定android.max_aspect 的元素,如下所示:

<!-- Render on full screen up to screen aspect ratio of 2.4 -->

<!-- Use a letterbox on screens larger than 2.4 -->

<meta-data android:name="android.max_aspect" android:value="2.4" />

如果您设置了最大宽高比,请不要忘记也设置 android:resizeableActivity false否则,最大宽高比不起作用。

android:resizeableActivity 是设置在<application> 标签上面的,例如

 <application
        android:name=".DidiApplication"
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:resizeableActivity="true"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        android:uiOptions="splitActionBarWhenNarrow"
        tools:replace="android:icon,android:theme,android:allowBackup"
        tools:targetApi="n">


        <!--android:screenOrientation="portrait"-->
        <activity
            android:name=".view.module.didi.activity.WellComeActivity"
            android:launchMode="singleTop"
            android:theme="@style/AppTheme.Splash"
            >
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

注意:如果您的应用不可调整大小,您应该测试它在尽可能多的设备上的行为。检查所有控件是否可见。有些设备让用户强制应用程序进入全屏显示,这将调整它们的大小。参考自Google中文文档 https://developer.android.google.cn/guide/practices/screens-distribution


6 后台执行限制适配建议

后台适配这块,坑比较多,每个手机业务不同,策略不同,只能给出建议来,让自己去斟酌

这里先给出以前大佬给出的方案和相关知识点:

https://www.jianshu.com/p/06a1a434e057

http://www.52im.net/thread-1135-1-1.html (强烈推荐)


以下是8.0的关于service的新增的知识点:

6.1 后台服务限制

先来了解一些基本概念:

在后台中运行的服务会消耗设备资源,这可能降低用户体验。 为了缓解这一问题,系统对这些服务施加了一些限制。也就是说无法在后台去启动"后台服务"了.


这里解释下什么是后台服务,解决办法可以从以下概念入手:

系统可以区分 前台 和 后台 应用。 (用于服务限制目的的后台定义与内存管理使用的定义不同;一个应用按照内存管理的定义可能处于后台,但按照能够启动服务的定义又处于前台。)如果满足以下任意条件,应用将被视为处于前台:

  • 具有可见 Activity(不管该 Activity 已启动还是已暂停)。
  • 具有前台服务。
  • 另一个前台应用已关联到该应用(不管是通过绑定到其中一个服务,还是通过使用其中一个内容提供程序)。 例如,如果另一个应用绑定到该应用的服务,那么该应用处于前台:

    • IME
    • 壁纸服务
    • 通知侦听器
    • 语音或文本服务

如果以上条件均不满足,应用将被视为处于后台

        处于前台时,应用可以自由创建和运行前台服务与后台服务。 进入后台时,在一个持续数分钟的时间窗内,应用仍可以创建和使用服务。在该时间窗结束后,应用将被视为处于 空闲 状态。 此时,系统将停止应用的后台服务,就像应用已经调用服务的“Service.stopSelf()”方法。

在这些情况下,后台应用将被置于一个临时白名单中并持续数分钟。 位于白名单中时,应用可以无限制地启动服务,并且其后台服务也可以运行。

处理对用户可见的任务时,应用将被置于白名单中,例如:

在很多情况下,您的应用都可以使用 JobScheduler 作业替换后台服务。 例如,CoolPhotoApp 需要检查用户是否已经从朋友那里收到共享的照片,即使该应用未在前台运行。

那么我们可以从前台应用角度入手,去保活和启动服务
例如,在后台的时候让前台服务或者应用去启动一个后台服务(此处按自己应用的逻辑去处理) 下面会给出具体的解决方案。

那么官网给我们的建议是什么呢?

同样,建议之前先了解下以下概念:

Android 8.0 引入了一种全新的方法,即 Context.startForegroundService(),以在前台启动新服务。

在系统创建服务后,应用有五秒的时间来调用该服务的 startForeground() 方法以显示新服务的用户可见通知。

注意: 如果应用在此时间限制内调用 startForeground(),则系统将停止服务并声明此应用为 ANR


参照以上概念,官网的建议是:

  • 如果处于后台时您的应用需要创建一个前台服务,请使用新的API NotificationManager.startServiceInForeground()方法,而不是创建一个后台服务,然后尝试将其推到前台。
  • 如果服务容易被用户注意,请将其设为前台服务。 例如,播放音频的服务始终应为前台服务。您应该使用 NotificationManager.startServiceInForeground(),而不是 startService() 创建服务。
  • 发生网络事件时,请使用 FCM 选择性地唤醒您的应用,而不是在后台轮询。

  • 在应用正常处于前台之前,请推迟后台工作。

5.1 广播限制

 Android 8.0 的应用无法继续在其清单中为隐式广播注册广播接收器

那么这句话怎么理解呢? 也就是说隐式广播在清单中注册了,当你在后台的时候,广播发送出去了,你的app是无法响应这些广播的

那么需要解释""隐式广播" 是什么意思?

隐式广播是一种不专门针对该应用的广播。 例如,ACTION_PACKAGE_REPLACED 就是一种隐式广播,因为它将发送到注册的所有侦听器,让后者知道设备上的某些软件包已被替换。

不过,ACTION_MY_PACKAGE_REPLACED 不是隐式广播,因为不管已为该广播注册侦听器的其他应用有多少,它都会只发送到软件包已被替换的应用。

  • 应用可以继续在它们的清单中注册显式广播。
  • 应用可以在运行时使用 Context.registerReceiver() 为任意广播(不管是隐式还是显式)注册接收器。

  • 需要签名权限的广播不受此限制所限,因为这些广播只会发送到使用相同证书签名的应用,而不是发送到设备上的所有应用。

在许多情况下,之前注册隐式广播的应用使用 JobScheduler 作业可以获得类似的功能。

这里解释下哪些广播可以被接收到(即不受8.0系统的限制):https://developer.android.google.cn/guide/components/broadcast-exceptions.html


那么也就是说,我们只要发送针对我们自己app的广播入手,或者是动态的去注册接收器还是可以解决这个问题的.


同样,官网给的建议方法是: 检查在您应用的清单中定义的广播接收器。 如果您的清单为显式广播声明了接收器,您必须予以替换。 可能的解决方法包括:

  • 通过调用 Context.registerReceiver() 而不是在清单中声明接收器的方式在运行时创建接收器。

  • 使用计划作业检查条件是否会触发隐式广播。(测试!测试!测试!)


最后,需要注意点的是一定要兼容最新版本!一定要兼容最新版本!一定要兼容最新版本

最新的Android系统发布网站及咨询,可以经常逛逛csdn或者其他网站如:


展开阅读全文

没有更多推荐了,返回首页