Activity互動問題,你確定都知道?

語言: CN / TW / HK

對於AndroidDeveloper來說,activity就像初戀一樣,啟動要求低,響應快,準備得當的前提下能玩的花樣也多,但關於activity互動方面的可能問題也有不少,本文將從常見Binder傳遞資料限制、多個Application對activity跳轉的影響等方面進行逐步探討。

Binder傳遞資料限制

資料傳遞限制

通過intent在Activity之間相互跳轉時傳遞資料,已經是最常見的基本操作,但這種操作在特定情況下會引起崩潰,例如以下場景: 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); 在MainActivity中跳轉NextPageActivity頁面,並通過intent傳遞一個bitmap資料,執行上述程式碼後引起的報錯如下: image.png 可見報錯原因:Intent傳輸資料過大,而Android系統對使用Binder進行資料傳輸的大小做了限制(相關限制定義在frameworks/native/libs/binder/processState.cpp類中,可能因為版本不同而程式碼有些許不同),通常情況下系統限制為1M,在Android 6和Android 7上單次傳輸資料大小限制為200KB,但是根據不同版本、不同廠商,這個值又會有不同的區別。

其實,考慮到Binder的設計初衷,有此異常丟擲也能理解:其Binder本身就是為了程序間頻繁且靈活的通訊所設計的,並不是為了拷貝大資料而使用的,所以效能優先,資料大小自然有所限制。

解決措施

1、將物件轉化成Json字串

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[10241024]; } ``` 在該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。而最好的方式,還是引導使用者開啟對應許可權。