App記憶體優化-實踐

貓尾巴發表於2019-04-27

多程式的優點

系統為每個應用分配一定大小的記憶體,從之前的 16M 到 32M、48M,甚至更高。但畢竟有限。

程式是資源分配的基本單位。也就是說,一個應用有對個程式,那這個應用可以獲得更多的記憶體。

所以,開啟多程式可以分擔主程式的記憶體消耗,常見音樂類 APP 的後臺播放,應用的推送服務等。

多程式的不足

1、資料共享問題

Android 系統為每個程式分配獨立的虛擬機器,不同的虛擬機器之間資料不能共享,即使是靜態成員還是單例模式。

2、執行緒同步機制失效

不同程式鎖的不是同一個物件,無法保證執行緒同步了。

3、SharedPreferences 可靠性下降

SharedPreferences 還沒有增加對多程式的支援。

4、Application 多次建立

當一個元件跑在新的程式中,系統要在建立程式的同時為其分配獨立的虛擬機器,自然就會建立新的 Application。這就導致了 application 的 onCreate方法重複執行全部的初始化程式碼。因此,可以根據程式需要進行最小的業務初始化。

多程式注意事項

  • 靜態成員和單例模式會失效
  • 執行緒同步機制失效
  • SharePreference 穩定性不能保證,使用 ContentProvider 封裝,對外提供資料服務
  • Application 會多次建立,需要注意多程式間使用的物件是否初始化 在 web 程式中呼叫主程式功能都需要注意 Context 和資料的讀取,否則會出現空指標的問題.

Application 多次建立

不同程式共同的初始化業務邏輯 :

public class AppInitialization {

    /**
     * 不同程式共同的初始化程式碼
     * @param application
     */
    public void onAppCreate(Application application) {
        // init
    }
複製程式碼

簡單工廠模式 :

根據程式名進行對應程式的初始化邏輯。

public class AppInitFactory {
 
    public static AppInitialization getAppInitialization(String processName) {
        AppInitialization appInitialization;
        if (processName.endsWith(":second")) {
            appInitialization = new SecondApplication();
        } else if (processName.endsWith(":third")) {
            appInitialization = new ThirdApplication();
        } else {
            appInitialization = new AppInitialization();
        }
        return appInitialization;
    }

    static class SecondApplication extends AppInitialization {

        @Override
        public void onAppCreate(Application application) {
            super.onAppCreate(application);
            // init
        }
    }

    static class ThirdApplication extends AppInitialization {
        @Override
        public void onAppCreate(Application application) {
            super.onAppCreate(application);
            // init
        }
    }
複製程式碼

具體呼叫程式碼 :

public class MyApplication extends Application {

    private static final String TAG = "MyApplication";

    @Override
    public void onCreate() {
        super.onCreate();
        String currentProcessName = getCurrentProcessName();
        Log.e(TAG, "currentProcessName : " + currentProcessName );
        AppInitialization appInitialization =                                AppInitFactory.getAppInitialization(currentProcessName);
        if (appInitialization != null) {
            appInitialization.onAppCreate(this);
        }
    }

    /**
     * 獲取當前程式名稱
     * @return
     */
    private String getCurrentProcessName() {
        String currentProcessName = "";
        int pid = android.os.Process.myPid();
        ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
        for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
            if (processInfo.pid == pid) {
                currentProcessName = processInfo.processName;
                break;
            }
        }
        return currentProcessName;
    }
}
複製程式碼

Web 獨立程式

WebView 拆分為獨立程式執行,從而減輕主程式記憶體壓力很有必要,當記憶體緊張時,系統則會自動殺死 web 程式.拆分為多程式後,主要問題在於程式間通訊與主程式保活.

拆分 Web 程式

獨立程式分為兩種模式,私有獨立程式 和 全域性獨立程式 兩種模式,開始方式也很簡單.

 <!-- 私有獨立程式:與主程式同ShareUID,共享data目錄、元件資訊、共享記憶體資料  -->
 <activity
            android:name=".WebActivity"
            android:process=":web"/>
<!-- 全域性獨立程式:與主程式不同ShareUID -->
 <activity
            android:name=".WebActivity"
            android:process=".web"/>
複製程式碼

如果只與本應用通訊,不需要為全域性獨立程式.

當開啟網頁時,傳送請求應該帶使用者資訊,原來的資訊儲存方式是SharePreference,但是這種方式對於多程式呼叫時,容易出現不穩定的情況,並且它的多程式呼叫方式已經被標記為廢棄,所以為了保證穩定性,使用ContentProvider將其封裝,供不同的程式呼叫.

/**
 * 在 web 程式裡只是獲取使用者資訊,當web裡返回要登入資訊時,跳轉至主程式裡的登入頁面,所以只實現了 query 方法,更新 SharePreference 依舊使用了單程式讀寫模式.
 */
public class UserProvider extends ContentProvider {

    private static String sAuthoriry = BuildConfig.APPLICATION_ID + ".UserProvider";

    @Override
    public boolean onCreate() {
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        //這個 name 獲取的就是 xml 的檔名,預設取 uri 的 path 欄位的第一個
        if (!sAuthoriry.equals(uri.getAuthority())) {
            return null;
        }
        Bundle bundle = new Bundle();
        if (getContext() != null) {
            bundle.putString("user", SharedPreferUtil.get(getContext(), Constants.EXTRA_USER_CACHE, ""));
        }
        return new BundleCursor(bundle);
    }

    private static final class BundleCursor extends MatrixCursor {
        private Bundle mBundle;

        public BundleCursor(Bundle extras) {
            super(new String[]{}, 0);
            mBundle = extras;
        }

        @Override
        public Bundle getExtras() {
            return mBundle;
        }

        @Override
        public Bundle respond(Bundle extras) {
            mBundle = extras;
            return mBundle;
        }
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        throw new UnsupportedOperationException("No external call");
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        throw new UnsupportedOperationException("No external call");
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        throw new UnsupportedOperationException("No external call");
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        throw new UnsupportedOperationException("No external call");
    }
}
複製程式碼

AndroidManifest.xml 配置

<!-- 由於只與本應用通訊所以就沒有配置許可權 -->
 <provider
            android:name=".provider.UserProvider"
            android:authorities="${applicationId}.UserProvider"
            android:exported="true" />
複製程式碼

這樣就配置好了多程式讀取 SharePreference 的 ContentProvider,凡是讀取使用者資訊的地方都需替換為如下方式

@Nullable
public static User getUser() {
    String authority = "content://" + BuildConfig.APPLICATION_ID + ".UserProvider";
    Uri uri = Uri.parse(authority);
    //由於多程式模式,所以 Application 會多次初始化,MyApplication.getInstance() 的初始化不能寫在某個程式裡,如果這樣則其他程式獲取不到例項,導致這裡會出現NPE
    Cursor cursor = MyApplication.getInstance().getContentResolver().query(uri, null, null, null, null);
    if (cursor != null) {
        Bundle args = cursor.getExtras();
        cursor.close();
        if (args != null) {
            return User.stringToUser(args.getString("user"));
        }
    }
    return null;
}
複製程式碼

通過以上配置,就可以在不同程式裡獲取User物件.

我們業務中有個邏輯是分享網頁,當使用者點選網頁分享按鈕,調起分享頁面,然後分享至微信,分享成功返回後呼叫 js,所以需要在微信回撥中通知網頁,使用BroadcastReceiver通知網頁執行 js 指令碼.

主程式保活

在完成程式拆分後測試中發現,當主程式佔用一百多 MB 時紅米 Note3 機器開啟網頁程式,再消耗一百多 MB時,系統會自動殺死主程式,導致返回到主程式會再次載入,為了避免這種問題發生,只能在網頁程式啟動後,將主程式置為前臺程式. 程式保活話題如果要展開談,可以寫好多東西,這裡只介紹我們應用的方法.邏輯很簡單就是在主程式中啟動一個前臺 Service,然後再啟動一個相同 ID 的 Service,最後停止一個 Service,這樣通知欄裡便不會出現通知,而應用在前臺 oom_adj 值較高,程式不會被殺死.

public class KeepLiveService extends Service {
    public static final int NOTIFICATION_ID = 0x11;

    public KeepLiveService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            //API 18 以下,直接傳送 Notification 並將其置為前臺
            startForeground(NOTIFICATION_ID, new Notification());
        } else {
            //API 18 以上,傳送 Notification 並將其置為前臺後,啟動 InnerService
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.drawable.push);
            startForeground(NOTIFICATION_ID, builder.build());
            ContextCompat.startForegroundService(getApplicationContext(), new Intent(this, InnerService.class));
        }
    }

    public static class InnerService extends Service {

        public InnerService() {
        }

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

        @Override
        public void onCreate() {
            super.onCreate();
            //傳送與 KeepLiveService 中I D 相同的 Notification,然後取消自己的前臺顯示
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.drawable.push);
            startForeground(NOTIFICATION_ID, builder.build());
            stopSelf();
        }

    }
}
複製程式碼

當然 Service 也必須在 AndroidManifest 中註冊.

  • compleVerison 27 targetVersion 26 如果再為更高的版本則通知攔會顯示出應用正在後臺執行,給使用者造成不好的體驗
  • 為了在使用者體驗和記憶體消耗間平衡,在 Application 的 onTrimMemory中,當 level 值大於等於 TRIM_MEMORY_MODERATE 且 Web 程式在後臺後,主動殺死 web 程式.

跨程式通訊

程式間的通訊方式:

  • 四大元件間傳遞Bundle
  • 使用檔案共享方式,多程式讀寫一個相同的檔案,獲取檔案內容進行互動
  • AIDL
  • 使用Messenger,一種輕量級的跨程式通訊方案,底層使用AIDL實現
  • 使用ContentProvider,常用於多程式共享資料,比如系統的相簿,音樂等,我們也可以通過ContentProvider訪問到
  • Socket

Android的程式與程式之間通訊,有些不需要我們額外編寫通訊程式碼,例如:把選擇圖片模組放到獨立的程式,我們仍可以使用startActivityForResult方法,將選中的圖片放到Bundle中,使用Intent傳遞即可。

但是對於把“訊息推送Service”放到獨立的程式,這個業務就稍微複雜點了,這個時候可能會發生Activity跟Service傳遞物件,呼叫Service方法等一系列複雜操作。

由於各個程式執行在相對獨立的記憶體空間,所以它們是不能直接通訊的,因為程式裡的變數、物件等初始化後都是具有記憶體地址的,舉個簡單的例子,讀取一個變數的值,本質是找到變數的記憶體地址,取出存放的值。不同的程式,執行在相互獨立的記憶體(其實就可以理解為兩個不同的應用程式),顯然不能直接得知對方變數、物件的記憶體地址,這樣的話也自然不能訪問對方的變數,物件等。此時兩個程式進行互動,就需要使用跨程式通訊的方式去實現。簡單說,跨程式通訊就是一種讓程式與程式之間可以進行互動的技術。

AIDL是Android提供給我們的標準跨程式通訊API。Messenger也是使用AIDL實現的一種跨程式方式,Messenger顧名思義,就像是一種序列的訊息機制,它是一種輕量級的IPC方案,可以在不同程式中傳遞Message物件,我們在Message中放入需要傳遞的資料即可輕鬆實現程式間通訊。但是當我們需要呼叫服務端方法,或者存在併發請求,那麼Messenger就不合適了。而四大元件傳遞Bundle,這個就不需要解釋了,把需要傳遞的資料,用Intent封裝起來傳遞即可。

AIDL實現一個多程式訊息推送

像圖片選擇這樣的多程式需求,可能並不需要我們額外編寫程式通訊的程式碼,使用四大元件傳輸Bundle就行了,但是像推送服務這種需求,程式與程式之間需要高度的互動,此時就繞不過程式通訊這一步了。 下面我們就用即時聊天軟體為例,手動去實現一個多程式的推送例子,具體需求如下:

  • UI和訊息推送的Service分兩個程式;
  • UI程式用於展示具體的訊息資料,把使用者傳送的訊息,傳遞到訊息Service,然後傳送到遠端伺服器;
  • Service負責收發訊息,並和遠端伺服器保持長連線,UI程式可通過Service傳送訊息到遠端伺服器,Service收到遠端伺服器訊息通知UI程式;
  • 即使UI程式退出了,Service仍需要保持執行,收取伺服器訊息。

實現思路

  • 建立UI程式(下文統稱為客戶端);
  • 建立訊息Service(下文統稱為服務端);
  • 把服務端配置到獨立的程式(AndroidManifest.xml中指定process標籤);
  • 客戶端和服務端進行繫結(bindService);
  • 讓客戶端和服務端具備互動的能力。(AIDL使用)

Step0. AIDL呼叫流程概覽

開始之前,我們先來概括一下使用AIDL進行多程式呼叫的整個流程:

  • 客戶端使用bindService方法繫結服務端;
  • 服務端在onBind方法返回Binder物件;
  • 客戶端拿到服務端返回的Binder物件進行跨程式方法呼叫;

App記憶體優化-實踐

Step1.客戶端使用bindService方法繫結服務端

建立客戶端和服務端,把服務端配置到另外的程式

  • 建立客戶端 -> MainActivity;
  • 建立服務端 -> MessageService;
  • 把服務端配置到另外的程式 -> android:process=”:remote”

上面描述的客戶端、服務端、以及把服務端配置到另外程式,體現在AndroidManifest.xml中,如下所示:

<manifest ...>
    <application ...>
        <activity android:name=".ui.MainActivity"/>
        <service
            android:name=".service.MessageService"
            android:enabled="true"
            android:exported="true"
            android:process=":remote" />
    </application>
</manifest>
複製程式碼

繫結MessageService到MainActivity

建立MessageService 此時的MessageService就是剛建立的模樣,onBind中返回了null,下一步中我們將返回一個可操作的物件給客戶端。

public class MessageService extends Service {
    public MessageService() {
    }
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}
複製程式碼

客戶端MainActivity呼叫bindService方法繫結MessageService

這一步其實是屬於Service元件相關的知識,在這裡就比較簡單地說一下,啟動服務可以通過以下兩種方式:

  • 使用bindService方法 -> bindService(Intent service, ServiceConnection conn, int flags);
  • 使用startService方法 -> startService(Intent service);

bindService & startService區別:

使用bindService方式,多個Client可以同時bind一個Service,但是當所有Client unbind後,Service會退出,通常情況下,如果希望和Service互動,一般使用bindService方法,使用onServiceConnected中的IBinder物件可以和Service進行互動,不需要和Service互動的情況下,使用startService方法即可。

正如上面所說,我們是要和Service互動的,所以我們需要使用bindService方法,但是我們希望unbind後Service仍保持執行,這樣的情況下,可以同時呼叫bindService和startService(比如像本例子中的訊息服務,退出UI程式,Service仍需要接收到訊息),程式碼如下:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setupService();
    }
    /**
     * unbindService
     */
    @Override
    protected void onDestroy() {
        unbindService(serviceConnection);
        super.onDestroy();
    }
    /**
     * bindService & startService
     */
    private void setupService() {
        Intent intent = new Intent(this, MessageService.class);
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
        startService(intent);
    }
    ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.d(TAG, "onServiceConnected");
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.d(TAG, "onServiceDisconnected");
        }
    };
}
複製程式碼

Stpe2.服務端在onBind方法返回Binder物件

首先,什麼是Binder?

要說Binder,首先要說一下IBinder這個介面,IBinder是遠端物件的基礎介面,輕量級的遠端過程呼叫機制的核心部分,該介面描述了與遠端物件互動的抽象協議,而Binder實現了IBinder介面,簡單說,Binder就是Android SDK中內建的一個多程式通訊實現類,在使用的時候,我們不用也不要去實現IBinder,而是繼承Binder這個類即可實現多程式通訊。

其次,這個需要在onBind方法返回的Binder物件從何而來?

在這裡就要引出本文中的主題了——AIDL 多程式中使用的Binder物件,一般通過我們定義好的 .adil 介面檔案自動生成,當然你可以走野路子,直接手動編寫這個跨程式通訊所需的Binder類,其本質無非就是一個繼承了Binder的類,鑑於野路子走起來麻煩,而且都是重複步驟的工作,Google提供了 AIDL 介面來幫我們自動生成Binder這條正路。

定義AIDL介面

很明顯,接下來我們需要搞一波上面說的Binder,讓客戶端可以呼叫到服務端的方法,而這個Binder又是通過AIDL介面自動生成,那我們就先從AIDL搞起,搞之前先看看注意事項,以免出事故:

AIDL支援的資料型別:

Java 程式語言中的所有基本資料型別(如 int、long、char、boolean 等等) String和CharSequence Parcelable:實現了Parcelable介面的物件 List:其中的元素需要被AIDL支援,另一端實際接收的具體類始終是 ArrayList,但生成的方法使用的是 List 介面 Map:其中的元素需要被AIDL支援,包括 key 和 value,另一端實際接收的具體類始終是 HashMap,但生成的方法使用的是 Map 介面

其他注意事項:

在AIDL中傳遞的物件,必須實現Parcelable序列化介面; 在AIDL中傳遞的物件,需要在類檔案相同路徑下,建立同名、但是字尾為.aidl的檔案,並在檔案中使用parcelable關鍵字宣告這個類; 跟普通介面的區別:只能宣告方法,不能宣告變數; 所有非基礎資料型別引數都需要標出資料走向的方向標記。可以是 in、out 或 inout,基礎資料型別預設只能是 in,不能是其他方向。

建立一個AIDL介面,介面中提供傳送訊息的方法(Android Studio建立AIDL:專案右鍵 -> New -> AIDL -> AIDL File),程式碼如下:

package com.example.aidl;
import com.example.aidl.data.MessageModel;
interface MessageSender {
    void sendMessage(in MessageModel messageModel);
}
複製程式碼

被“in”標記的引數,就是接收實際資料的引數,這個跟我們普通引數傳遞一樣的含義。在AIDL中,“out” 指定了一個僅用於輸出的引數,換而言之,這個引數不關心呼叫方傳遞了什麼資料過來,但是這個引數的值可以在方法被呼叫後填充(無論呼叫方傳遞了什麼值過來,在方法執行的時候,這個引數的初始值總是空的),這就是“out”的含義,僅用於輸出。而“inout”顯然就是“in”和“out”的合體了,輸入和輸出的引數。區分“in”、“out”有什麼用?這是非常重要的,因為每個引數的內容必須編組(序列化,傳輸,接收和反序列化)。in/out標籤允許Binder跳過編組步驟以獲得更好的效能。

上述的MessageModel為訊息的實體類,該類在AIDL中傳遞,實現了Parcelable序列化介面,程式碼如下:

public class MessageModel implements Parcelable {
    private String from;
    private String to;
    private String content;
    ...
    Setter & Getter
    ...
    @Override
    public int describeContents() {
        return 0;
    }
    //...
    序列化相關程式碼
    //...
}
複製程式碼

手動實現Parcelable介面比較麻煩,安利一款AS自動生成外掛android-parcelable-intellij-plugin 建立完MessageModel這個實體類,別忘了還有一件事要做:”在AIDL中傳遞的物件,需要在類檔案相同路徑下,建立同名、但是字尾為.aidl的檔案,並在檔案中使用parcelable關鍵字宣告這個類“。程式碼如下:

package com.example.aidl.data;
parcelable MessageModel;
複製程式碼

App記憶體優化-實踐

我們剛剛新增的3個檔案:

  • MessageSender.aidl -> 定義了傳送訊息的方法,會自動生成名為MessageSender.Stub的Binder類,在服務端實現,返回給客戶端呼叫
  • MessageModel.java -> 訊息實體類,由客戶端傳遞到服務端,實現了Parcelable序列化
  • MessageModel.aidl -> 宣告瞭MessageModel可在AIDL中傳遞,放在跟MessageModel.java相同的包路徑下

在服務端建立MessageSender.aidl這個AIDL介面自動生成的Binder物件,並返回給客戶端呼叫,服務端MessageService程式碼如下:

public class MessageService extends Service {
    private static final String TAG = "MessageService";
    public MessageService() {
    }
    IBinder messageSender = new MessageSender.Stub() {
        @Override
        public void sendMessage(MessageModel messageModel) throws RemoteException {
            Log.d(TAG, "messageModel: " + messageModel.toString());
        }
    };
    @Override
    public IBinder onBind(Intent intent) {
        return messageSender;
    }
}
複製程式碼

MessageSender.Stub是Android Studio根據我們MessageSender.aidl檔案自動生成的Binder物件(至於是怎樣生成的,下文會有答案),我們需要把這個Binder物件返回給客戶端。

客戶端拿到Binder物件後呼叫遠端方法

呼叫步驟如下:

  • 在客戶端的onServiceConnected方法中,拿到服務端返回的Binder物件;
  • 使用MessageSender.Stub.asInterface方法,取得MessageSender.aidl對應的操作介面;
  • 取得MessageSender物件後,像普通介面一樣呼叫方法即可。
public class MainActivity extends AppCompatActivity {
    private MessageSender messageSender; 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setupService();
    }
    //...
    
    private void setupService() {
        Intent intent = new Intent(this, MessageService.class);
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
        startService(intent);
    }
    ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //使用asInterface方法取得AIDL對應的操作介面
            messageSender = MessageSender.Stub.asInterface(service);
            //生成訊息實體物件
            MessageModel messageModel = new MessageModel();
            messageModel.setFrom("client user id");
            messageModel.setTo("receiver user id");
            messageModel.setContent("This is message content");
            //呼叫遠端Service的sendMessage方法,並傳遞訊息實體物件
            try {
                messageSender.sendMessage(messageModel);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };
}
複製程式碼

在客戶端中我們呼叫了MessageSender的sendMessage方法,向服務端傳送了一條訊息,並把生成的MessageModel物件作為引數傳遞到了服務端,最終服務端列印的結果如下:

App記憶體優化-實踐

客戶端與服務端通訊過程

我們先來回顧一下從客戶端發起的呼叫流程:

  • MessageSender messageSender = MessageSender.Stub.asInterface(service);
  • messageSender.sendMessage(messageModel);

拋開其它無關程式碼,客戶端調跨程式方法就這兩個步驟,而這兩個步驟都封裝在 MessageSender.aidl 最終生成的 MessageSender.java 原始碼

public interface MessageSender extends android.os.IInterface {
   
    public static abstract class Stub extends android.os.Binder implements com.example.aidl.MessageSender {
        private static final java.lang.String DESCRIPTOR = "com.example.aidl.MessageSender";
        /**
         * 把IBinder物件轉換為 com.example.aidl.MessageSender 介面
         * 判斷IBinder是否處於相同程式,相同程式返回Stub實現的com.example.aidl.MessageSender介面
         * 不同程式,則返回Stub.Proxy實現的com.example.aidl.MessageSender介面
         */
        public static com.example.aidl.MessageSender asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.example.aidl.MessageSender))) {
                return ((com.example.aidl.MessageSender) iin);
            }
            return new com.example.aidl.MessageSender.Stub.Proxy(obj);
        }
        /**
         * 同一程式時,不會觸發
         *
         * 不同程式時,asInterface會返回Stub.Proxy,客戶端呼叫 messageSender.sendMessage(messageModel)
         * 實質是呼叫了 Stub.Proxy 的 sendMessage 方法,從而觸發跨程式資料傳遞,
         * 最終Binder底層將處理好的資料回撥到此方法,並呼叫我們真正的sendMessage方法
         */
        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_sendMessage: {
                    data.enforceInterface(DESCRIPTOR);
                    com.example.aidl.data.MessageModel _arg0;
                    if ((0 != data.readInt())) {
                        _arg0 = com.example.aidl.data.MessageModel.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.sendMessage(_arg0);
                    reply.writeNoException();
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }
        private static class Proxy implements com.example.aidl.MessageSender {
            private android.os.IBinder mRemote;
            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }
            /**
             * Proxy中的sendMessage方法,並不是直接呼叫我們定義的sendMessage方法,而是經過一頓的Parcel讀寫,
             * 然後呼叫mRemote.transact方法,把資料交給Binder處理,transact處理完畢後會呼叫上方的onTransact方法,
             * onTransact拿到最終得到的引數資料,呼叫由我們真正的sendMessage方法
             */
            @Override
            public void sendMessage(com.example.aidl.data.MessageModel messageModel) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    if ((messageModel != null)) {
                        _data.writeInt(1);
                        messageModel.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    //呼叫Binder的transact方法進行多程式資料傳輸,處理完畢後呼叫上方的onTransact方法
                    mRemote.transact(Stub.TRANSACTION_sendMessage, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
        }
        static final int TRANSACTION_sendMessage = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    }
    public void sendMessage(com.example.aidl.data.MessageModel messageModel) throws android.os.RemoteException;
}
複製程式碼

App記憶體優化-實踐

從客戶端的sendMessage開始,整個AIDL的呼叫過程如上圖所示,asInterface方法,將會判斷onBind方法返回的Binder是否存處於同一程式,在同一程式中,則進行常規的方法呼叫,若處於不同程式,整個資料傳遞的過程則需要通過Binder底層去進行編組(序列化,傳輸,接收和反序列化),得到最終的資料後再進行常規的方法呼叫。

敲黑板:物件跨程式傳輸的本質就是 序列化,傳輸,接收和反序列化 這樣一個過程,這也是為什麼跨程式傳輸的物件必須實現Parcelable介面

跨程式的回撥介面

在上面我們已經實現了從客戶端傳送訊息到跨程式服務端的功能,接下來我們還需要將服務端接收到的遠端伺服器訊息,傳遞到客戶端。有同學估計會說:“這不就是一個回撥介面的事情嘛”,設定回撥介面思路是對的,但是在這裡使用的回撥介面有點不一樣,在AIDL中傳遞的介面,不能是普通的介面,只能是AIDL介面,所以我們需要新建一個AIDL介面傳到服務端,作為回撥介面。

新建訊息收取的AIDL介面MessageReceiver.aidl:

package com.example.aidl;
import com.example.aidl.data.MessageModel;
interface MessageReceiver {
    void onMessageReceived(in MessageModel receivedMessage);
}
複製程式碼

接下來我們把回撥介面註冊到服務端去,修改我們的MessageSender.aidl:

package com.example.aidl;
import com.example.aidl.data.MessageModel;
import com.example.aidl.MessageReceiver;
interface MessageSender {
    void sendMessage(in MessageModel messageModel);
    void registerReceiveListener(MessageReceiver messageReceiver);
    void unregisterReceiveListener(MessageReceiver messageReceiver);
}
複製程式碼

以上就是我們最終修改好的aidl介面,接下來我們需要做出對應的變更:

  • 在服務端中增加MessageSender的註冊和反註冊介面的方法;
  • 在客戶端中實現MessageReceiver介面,並通過MessageSender註冊到服務端。
public class MainActivity extends AppCompatActivity {
    private MessageSender messageSender;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //...
    }
    /**
     * 1.unregisterListener
     * 2.unbindService
     */
    @Override
    protected void onDestroy() {
    	//解除訊息監聽介面
        if (messageSender != null && messageSender.asBinder().isBinderAlive()) {
            try {
                messageSender.unregisterReceiveListener(messageReceiver);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        unbindService(serviceConnection);
        super.onDestroy();
    }
    //訊息監聽回撥介面
    private MessageReceiver messageReceiver = new MessageReceiver.Stub() {
        @Override
        public void onMessageReceived(MessageModel receivedMessage) throws RemoteException {
            Log.d(TAG, "onMessageReceived: " + receivedMessage.toString());
        }
    };
    ServiceConnection serviceConnection = new ServiceConnection() {
    
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //使用asInterface方法取得AIDL對應的操作介面
            messageSender = MessageSender.Stub.asInterface(service);
            //生成訊息實體物件
            MessageModel messageModel = new MessageModel();
            //...
            try {
                //把接收訊息的回撥介面註冊到服務端
                messageSender.registerReceiveListener(messageReceiver);
                //呼叫遠端Service的sendMessage方法,並傳遞訊息實體物件
                messageSender.sendMessage(messageModel);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };
}
複製程式碼

客戶端主要有3個變更:

  • 增加了messageReceiver物件,用於監聽服務端的訊息通知;
  • onServiceConnected方法中,把messageReceiver註冊到Service中去;
  • onDestroy時候解除messageReceiver的註冊。

服務端MessageServie進行變更:

public class MessageService extends Service {
    private static final String TAG = "MessageService";
    private AtomicBoolean serviceStop = new AtomicBoolean(false);
    //RemoteCallbackList專門用來管理多程式回撥介面
    private RemoteCallbackList<MessageReceiver> listenerList = new RemoteCallbackList<>();
    public MessageService() {
    }
    IBinder messageSender = new MessageSender.Stub() {
        @Override
        public void sendMessage(MessageModel messageModel) throws RemoteException {
            Log.e(TAG, "messageModel: " + messageModel.toString());
        }
        @Override
        public void registerReceiveListener(MessageReceiver messageReceiver) throws RemoteException {
            listenerList.register(messageReceiver);
        }
        @Override
        public void unregisterReceiveListener(MessageReceiver messageReceiver) throws RemoteException {
            listenerList.unregister(messageReceiver);
        }
    };
    @Override
    public IBinder onBind(Intent intent) {
        return messageSender;
    }
    @Override
    public void onCreate() {
        super.onCreate();
        new Thread(new FakeTCPTask()).start();
    }
    @Override
    public void onDestroy() {
        serviceStop.set(true);
        super.onDestroy();
    }
    //模擬長連線,通知客戶端有新訊息到達
    private class FakeTCPTask implements Runnable {
        @Override
        public void run() {
            while (!serviceStop.get()) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                MessageModel messageModel = new MessageModel();
                messageModel.setFrom("Service");
                messageModel.setTo("Client");
                messageModel.setContent(String.valueOf(System.currentTimeMillis()));
                /**
                 * RemoteCallbackList的遍歷方式
                 * beginBroadcast和finishBroadcast一定要配對使用
                 */
                final int listenerCount = listenerList.beginBroadcast();
                Log.d(TAG, "listenerCount == " + listenerCount);
                for (int i = 0; i < listenerCount; i++) {
                    MessageReceiver messageReceiver = listenerList.getBroadcastItem(i);
                    if (messageReceiver != null) {
                        try {
                            messageReceiver.onMessageReceived(messageModel);
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
                    }
                }
                listenerList.finishBroadcast();
            }
        }
    }
}
複製程式碼

服務端主要變更:

  • MessageSender.Stub實現了註冊和反註冊回撥介面的方法;
  • 增加了RemoteCallbackList來管理AIDL遠端介面;
  • FakeTCPTask模擬了長連線通知客戶端有新訊息到達。(這裡的長連線可以是XMPP,Mina,Mars,Netty等,這裡弄個假的意思意思,有時間的話我們開個帖子聊聊XMPP)

這裡還有一個需要講一下的,就是RemoteCallbackList,為什麼要用RemoteCallbackList,普通ArrayList不行嗎?當然不行,不然幹嘛又整一個RemoteCallbackList,registerReceiveListener 和 unregisterReceiveListener在客戶端傳輸過來的物件,經過Binder處理,在服務端接收到的時候其實是一個新的物件,這樣導致在 unregisterReceiveListener 的時候,普通的ArrayList是無法找到在 registerReceiveListener 時候新增到List的那個物件的,但是它們底層使用的Binder物件是同一個,RemoteCallbackList利用這個特性做到了可以找到同一個物件,這樣我們就可以順利反註冊客戶端傳遞過來的介面物件了。RemoteCallbackList在客戶端程式終止後,它能自動移除客戶端所註冊的listener,它內部還實現了執行緒同步,所以我們在註冊和反註冊都不需要考慮執行緒同步,的確是個666的類。

DeathRecipient

不知道你有沒有感覺到,兩個程式互動總是覺得缺乏那麼一點安全感…比如說服務端程式Crash了,而客戶端程式想要呼叫服務端方法,這樣就呼叫不到了。此時我們可以給Binder設定一個DeathRecipient物件,當Binder意外掛了的時候,我們可以在DeathRecipient介面的回撥方法中收到通知,並作出相應的操作,比如重連服務等等。

DeathRecipient的使用如下:

  • 宣告DeathRecipient物件,實現其binderDied方法,當binder死亡時,會回撥binderDied方法;
  • 給Binder物件設定DeathRecipient物件。

在客戶端MainActivity宣告DeathRecipient:

/**
    * Binder可能會意外死忙(比如Service Crash),Client監聽到Binder死忙後可以進行重連服務等操作
    */
   IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
       @Override
       public void binderDied() {
           Log.d(TAG, "binderDied");
           if (messageSender != null) {
               messageSender.asBinder().unlinkToDeath(this, 0);
               messageSender = null;
           }
           //// TODO: 2017/2/28 重連服務或其他操作
           setupService();
       }
   };
   
   ServiceConnection serviceConnection = new ServiceConnection() {
       @Override
       public void onServiceConnected(ComponentName name, IBinder service) {
           //...
           try {
               //設定Binder死亡監聽
               messageSender.asBinder().linkToDeath(deathRecipient, 0);
           } catch (RemoteException e) {
               e.printStackTrace();
           }
       }
       //...
   };
複製程式碼

Binder中兩個重要方法:

  • linkToDeath -> 設定死亡代理 DeathRecipient 物件;
  • unlinkToDeath -> Binder死亡的情況下,解除該代理。

此外,Binder中的isBinderAlive也可以判斷Binder是否死亡。

許可權驗證

介紹兩種常用驗證方法:

  • 在服務端的onBind中校驗自定義permission,如果通過了我們的校驗,正常返回Binder物件,校驗不通過返回null,返回null的情況下客戶端無法繫結到我們的服務;
  • 在服務端的onTransact方法校驗客戶端包名,不通過校驗直接return false,校驗通過執行正常的流程。

自定義permission,在Androidmanifest.xml中增加自定義的許可權:

<permission
    android:name="com.example.aidl.permission.REMOTE_SERVICE_PERMISSION"
    android:protectionLevel="normal" />
<uses-permission android:name="com.example.aidl.permission.REMOTE_SERVICE_PERMISSION" />
複製程式碼

服務端檢查許可權的方法:

IBinder messageSender = new MessageSender.Stub() {
    //...
    @Override
    public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        /**
         * 包名驗證方式
         */
        String packageName = null;
        String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
        if (packages != null && packages.length > 0) {
            packageName = packages[0];
        }
        if (packageName == null || !packageName.startsWith("com.example.aidl")) {
            Log.d("onTransact", "拒絕呼叫:" + packageName);
            return false;
        }
        return super.onTransact(code, data, reply, flags);
    }
};
@Override
public IBinder onBind(Intent intent) {
    //自定義permission方式檢查許可權
    if (checkCallingOrSelfPermission("com.example.aidl.permission.REMOTE_SERVICE_PERMISSION") == PackageManager.PERMISSION_DENIED) {
        return null;
    }
    return messageSender;
}
複製程式碼

相關文章