Android 笔记

targetSdkVersion升级为26+遇到的问题及解决方案。

Android 9.0 以上修改

1.当在 Android 9.0 加载网络请求数据时,有时会抛出如下异常:

Cause (1 of 1): class java.io.IOException: Cleartext HTTP traffic to xxxx.xxxx.xxxx not permitted

这是因为Android 9.0版本系统默认支持一个网络访问协议:Https协议的网络,所以不支持网络访问:Http协议的网络面对这样的问题,解决办法:

方法1

直接通过在AnroidManifest.xml中的<application>标签内添加</application>

<application android:usesCleartextTraffic="true"/>

原来默认为 true,但在 Android 9.0 中默认值改为了 false,因此将配置手动设为 true 即可解决明文传输被限制的问题

方法2

在res文件夹下创建一个xml文件夹,然后创建一个network_security_config.xml文件,文件内容如下:

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

然后在<application>标签内添加以下代码:</application>

android:networkSecurityConfig="@xml/network_security_config"

2. Android 9.0 弃用 Apache HTTP Client

由于官方在 Android 9.0 中移除了所有 Apache HTTP Client 相关的类,因此我们的应用或是一些第三方库如果使用了这些类,就会抛出找不到类的异常:

java.lang.NoClassDefFoundError: Failed resolution of: Lorg/apache/http/conn/scheme/SchemeRegistry;

若需要继续使用 Apache HTTP Client ,可通过以下方法进行适配:

在 AndroidManifest.xml 中的<application>标签内添加以下内容:</application>

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

3. Android 9.0 Build.SERIAL 被弃用

Android 9.0 之前,开发者可以使用 Build.SERIAL 获取设备的序列号。现在这个方法被弃用了,Build.SERIAL 将始终设置为 "UNKNOWN" 以保护用户的隐私。适配的方法为先请求READ_PHONE_STATE权限,然后调用Build.getSerial()方法。

/**
 * 获取android设备唯一标识码
 */
fun getClientId(context: Context): String {
    val clientId: String
    val androidID = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
    clientId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        androidID + Build.getSerial()
    } else {
        androidID + Build.SERIAL
    }
    Log.e("", "ClientId: $clientId")
    return clientId
}

4. Android 9.0 使用Intent卸载应用无反应

使用下面代码在安卓9.0并不管用

//卸载APP
fun uninstallApp(activity: Activity, packageName: String) {
    val intent = Intent(Intent.ACTION_DELETE)
    intent.data = Uri.parse("package:$packageName")
    activity.startActivity(intent)
}

原因是没有添加权限,解决办法:
在AnroidManifest.xml中加入

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

5. Android 9.0 取消 BottomNavigationView 动画效果的实现

在 API 28 之前,取消 BottomNavigationView 动画效果的实现方法为:

//使用
BottomNavHelper.disableShiftMode(bottomNavigationView);

/**
 * 处理BottomNavigationView控件底部按钮超过3个文字不显示
 */
public class BottomNavHelper {
    @SuppressLint("RestrictedApi")
    public static void disableShiftMode(BottomNavigationView bottomNavView) {
        BottomNavigationMenuView menuView = (BottomNavigationMenuView) bottomNavView.getChildAt(0);
        try {
            Field shiftingMode = menuView.getClass().getDeclaredField("mShiftingMode");
            shiftingMode.setAccessible(true);
            shiftingMode.setBoolean(menuView, false);
            shiftingMode.setAccessible(false);

            for (int i = 0; i < menuView.getChildCount(); i++) {
                BottomNavigationItemView itemView = (BottomNavigationItemView) menuView.getChildAt(i);
                itemView.setShiftingMode(false);
                itemView.setChecked(itemView.getItemData().isChecked());
            }

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

然而升级 API 28 后,BottomNavigationView 的 setShiftingMode(boolean) 不能用了。

解决方法:直接设置labelVisibilityMode属性即可。

<android.support.design.widget.BottomNavigationView
    app:labelVisibilityMode="labeled" />

labelVisibilityMode 用于设置图标下面的文字显示,该属性对应的值为:

  • auto : 当 item 小于等于3时,显示文字,item 大于3个默认不显示,选中显示文字
  • labeled : 始终显示文字
  • selected : 选中时显示
  • unlabeled : 选中时显示

该属性对应的方法是setLabelVisibilityMode(LabelVisibilityMode.LABEL_VISIBILITY_LABELED);

Android 8.0 以上修改

1. Android 8.0 无法通过 "application/vnd.android.package-archive" 安装应用

原因:targetsdkversion 大于 25 必须声明 REQUEST_INSTALL_PACKAGES 权限

解决办法:在AnroidManifest.xml中加入

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

2. Android 8.0 的 Notification 不显示问题

原因:NotificationChannel 是 Android 8.0 新增的特性,如果 targetSdkVersion >= 26,没有设置 channel 通知渠道的话,就会导致通知无法展示。

Android O 引入了通知渠道(Notification Channels),以提供统一的系统来帮助用户管理通知,如果是针对 Android O 为目标平台时,必须实现一个或者多个通知渠道,以向用户显示通知。比如聊天软件,为每个聊天组设置一个通知渠道,指定特定声音、灯光等配置。

示例代码如下:

/**
 * Notification通知设置
 */
fun pushMsg(context: Context) {

    val channelId = "channel_id"
    val channelName = "channel_name"

    val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
        val channel = NotificationChannel(
            channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
            manager.createNotificationChannel(channel)
    }
    val builder = NotificationCompat.Builder(context, channelId)
    builder.setContentTitle("新消息来了") //标题
        .setContentText("周末到了,不用上班了") //文本内容
        .setSmallIcon(R.mipmap.ic_launcher) //小图标
        .setAutoCancel(true) //设置点击信息后自动清除通知
    manager.notify(1, builder.build())
}

3. Android 8.0 启动后台 service 问题

Android P 上应用在后台启动 service 时报了个异常:

java.lang.IllegalStateException, Not allowed to start service Intent

原因:Android 8.0+ 对后台服务进行了限制,如果依然采用之前startService()方式会导致问题。

Android 8.0 对特定函数做出了以下变更:

  • 如果针对 Android 8.0 的应用尝试在不允许其创建后台服务的情况下使用 startService() 函数,则该函数将引发一个 IllegalStateException。
  • 新的 Context.startForegroundService() 函数将启动一个前台服务。现在,即使应用在后台运行,系统也允许其调用 Context.startForegroundService()。不过,应用必须在创建服务后的五秒内调用该服务的 startForeground() 函数。

解决办法:原来的 startService() 需要根据sdk版本进行兼容

Intent intent = new Intent(context, SampleService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    context.startForegroundService(intent);
} else {
    context.startService(intent);
}

之后在被启动的 Service 创建服务后的五秒内需要调用 startForeground(1, notification) ,如果不调用或调用时间超过5秒则会抛出一个 ANR 错误。

所以需要在 service 的 onCreat() 方法中按如下代码设置:

public class SampleService extends Service {

    private static final String CHANNEL_ID = "channel_id";
    private static final String CHANNEL_NAME = "channel_name";

    @Override
    public void onCreate() {
        super.onCreate();
        //适配 8.0 service
        NotificationManager notificationManager = (NotificationManager) App.getInstance()
                .getSystemService(Context.NOTIFICATION_SERVICE);
        NotificationChannel mChannel;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            mChannel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
            if (notificationManager != null) {
                notificationManager.createNotificationChannel(mChannel);
            }
            Notification notification = new Notification
                    .Builder(getApplicationContext(), CHANNEL_ID)
                    .build();
            startForeground(1, notification);
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

}

除此之外,Android 9.0 要求创建一个前台服务需要请求 FOREGROUND_SERVICE 权限,否则系统会引发 SecurityException。

解决方法:AndroidManifest.xml中添加FOREGROUND_SERVICE权限:

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

4. Android 8.0 静态注册广播 行为变更

Android 8.0 在 AndroidManifest 中注册的 receiver 不能收到广播

原因:Android 8.0 引入了新的广播接收器限制,加强对匿名 receiver 的控制,以至于在 manifest 中注册的隐式 receiver 都失效了。

不过 8.0 以后,静态注册的广播接收器还是可以接收到广播的,只要广播是通过显示方式发送的。

当广播接收器使用静态注册方式时,除了一些例外,这个接收器接收不到隐式广播。注意这个“隐式”是重点

先定义一个简单广播:

public class MyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context,context.toString(),Toast.LENGTH_SHORT).show();
    }
}

然后注册到AndroidManifest.xml中

<receiver android:name=".broadcast.MyReceiver">
    <intent-filter>
        <action android:name="com.demo.recriver"/>
    </intent-filter>
</receiver>

最后,在Activity中发送一个广播,intent通过设置Action为com.demo.recriver的形式发送隐式广播。

Intent intent = Intent intent = new Intent("com.demo.recriver");//隐式intent,发送隐式广播
sendBroadcast(intent);

运行后会发现在8.0以下的手机上,会有Toast显示,8.0以上的手机不会弹出,说明没有接收到广播。

原因在于这个广播 是“隐式” 发送的,8.0中,静态注册的广播接收器无法接受 隐式广播。

有两种解决方法:

  1. 在Activity或其他组件中动态注册广播

  2. 发送显式广播

发送显式广播写法:

Intent intent = new Intent(MainActivity.this,MyReceiver.class);//显示指定组件名
sendBroadcast(intent);