Are you sure you know about Activity interaction issues?

For Android Developer, activity is like first love, with low startup requirements, fast response, and many tricks that can be played under the premise of proper preparation, but there are also many possible problems with activity interaction. This article will pass data restrictions from common binders , the impact of multiple applications on activity jumping and other aspects will be discussed step by step.

Binder pass data limit

data transfer restrictions

Passing data when jumping between activities through intent is already the most common basic operation, but this operation can cause crashes in certain situations, such as the following scenarios:

Intent intent = new Intent(MainActivity.this, NextPageActivity.class); 
Bitmap icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
Bitmap bitmap = Bitmap.createScaledBitmap(icon, 1024, 1024, false); intent.putExtra("data",bitmap); 
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 
startActivity(intent);
复制代码

Jump to the NextPageActivity page in MainActivity, and pass a bitmap data through the intent. The error caused by executing the above code is as follows: It can be image.pngseen that the reason for the error: the data transmitted by the intent is too large, and the Android system has limited the size of the data transmission using Binder ( The relevant limit is defined in the frameworks/native/libs/binder/processState.cpp class, the code may be slightly different due to different versions), usually the system limit is 1M, on Android 6 and Android 7, the data size limit for a single transfer It is 200KB, but according to different versions and different manufacturers, this value will be different.

In fact, considering the original design of Binder, it is understandable to have this exception thrown: the Binder itself is designed for frequent and flexible communication between processes, not for copying large data, so performance is prioritized and data size is Naturally there are limitations.

solution

1. Convert the object into a Json string

JVM 加载类时常会伴随额外的空间来保存类相关信息,将类中数据转化为 JSON 字符串可以减少数据大小。比如使用 Gson.toJson 方法。此方法几乎万能,当然,有时候将类转化成Json字符串后还是会超出Binder限制,说明这时候要传输的数据量较大,建议使用缓存等本地持久化方式、或全局观察者方式(EventBus、RxBus之类)来实现数据共享。

2、使用 transient 关键字修饰非必须字段,减少通过 Intent 传递的数据

transient只能修饰变量,而不能修饰方法、类、局部变量,而静态变量无论是否被transient修饰,都不能被序列化。可见,这种方式不适合所有场景,包括上述代码场景,例如下述传递一个bean类数据:

Intent intent = new Intent(MainActivity.this, NextPageActivity.class); intent.putExtra("data",new TestBean()); 
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 
startActivity(intent); 

static class TestBean implements Serializable { 
    private transient byte[] bean = new byte[1024*1024]; 
}
复制代码

在该bean类中对byte[]进行 transient 关键词修饰,运行代码后会发现再无报错。

指定process造成多个Application

特定情况(需求)下,我们要对Activity指定别的进程名,如下:

image.png

由于Activity能在不同的进程中启动,且每一个进程都会默认创建一个Application,因此可能会使得Application的onCreate()多次调用。我们可在Application代码中打印对应进程日志:

public class MainApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(getClass().getName(),"process name : "+getProcessName(this, Process.myPid()));
    }

    public String getProcessName(Context cxt, int pid) {
        ActivityManager am = (ActivityManager) cxt.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();
        if (runningApps == null) {
            return null;
        }
        for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) {
            if (procInfo.pid == pid) {
                return procInfo.processName;
            }
        }
        return null;
    }

}
复制代码

MainActivity代码逻辑较简单,仅是一个点击事件,点击后跳转至指定process为“andev.process”的NextPageActivity,这里就不贴出来了,运行代码后,可看到对应打印的进程名日志为:

image.png

不难看出,MainApplication的onCreate()被调用了两次,如果按照我们往常的习惯在onCreate中进行一些环境的初始化的话,则对应各种初始化方法会执行两次,会造成一些意想不到的报错和崩溃。

解决方法

1、在Application的onCreate()中进行初始化前进行进程判断,如果是当前主进程则进行对应操作,一般都是用此方法;

2、网上说的抽象出一个与 Application 生命周期同步的类,并根据不同的进程创建相应的 Application 实例。不太建议,一般情况下通过当前进程名判断即可。

后台启动Activity问题

Android10(API29)开始,Android系统对从后台启动Activity做了一定限制,要用户同意“后台弹出界面”权限后,APP才能从后台服务或操作中弹出Activity,自从2019年5月份开始,小米开启了这项权限判断,从此各大厂商陆续添加此权限要求。其实此权限的设计初衷不难理解,为了避免当前前台用户的交互被打断,保证当前屏幕上展示的内容不受影响。想想看,风和日丽的一天,你吃着火锅唱着歌,玩着手里的农药,突然某个不知名APP在后台给你弹了个界面,你点了关闭重新回到农药,却发现只能看黑白电视了,还招来队友的问候,是不是顿时间就觉得火锅不香了。

其实这属于Android系统的优化,应尽可能的去适配厂商,给安卓生态圈带来更加用户体验,如果实在需要,可通过产品设计引导用户去开启对应权限,当然也有绕过此权限的方法,如下:

public void startWithNotify(Intent intent) {
    String channelId = "xxxxxxx";
    String nTag = "xxxxxxx";
    PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 2022712, intent, PendingIntent
            .FLAG_UPDATE_CURRENT);
    try {
        pendingIntent.send();

        notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
        if (notificationManager == null) {
            return;
        }
        if (Build.VERSION.SDK_INT >= 26 && notificationManager.getNotificationChannel(channelId) == null) {
            final NotificationChannel notificationChannel = new NotificationChannel(channelId, mContext.getString(R.string.app_name), NotificationManager.IMPORTANCE_HIGH);
            notificationChannel.setDescription(mContext.getString(R.string.app_name));
            notificationChannel.setLockscreenVisibility(-1);
            notificationChannel.enableLights(false);
            notificationChannel.enableVibration(false);
            notificationChannel.setShowBadge(false);
            notificationChannel.setSound((Uri) null, (AudioAttributes) null);
            notificationChannel.setBypassDnd(true);
            notificationManager.createNotificationChannel(notificationChannel);
        }
        //不弹出来的空通知栏
        NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext, channelId)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setFullScreenIntent(pendingIntent, true)
                .setCustomHeadsUpContentView(new RemoteViews(mContext.getPackageName(), R.layout.layout_empty_notify));
        if (Build.VERSION.SDK_INT >= 21) {
            builder.setVisibility(Notification.VISIBILITY_PRIVATE);
        }
        notificationManager.notify(nTag, notificationId, builder.build());
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {

                if (notificationManager != null) {//需延迟取消。不然activity出不来
                    notificationManager.cancel(nTag, notificationId);
                }
            }
        }, 1000);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
复制代码

当然,此方法涉及到版本适配,不一定适配当前所有Room,此时可以采用另一套方案:

1、判断当前界面是否在前台,热后获取对应的ActivityManager;

2、利用系统的当前的task堆栈,遍历找到需要的task,将其调至强行切换到前台即可。

此方法相对耗时,有自己的弊端。但两种方式结合,能应对90%以上Room。而最好的方式,还是引导用户打开对应权限。

Guess you like

Origin juejin.im/post/7119465581232783397