內存管理之多進程實踐

背景

我們知道,每個進程所分配的堆內存空間是寶貴且有限的。而我們經(jīng)常所面對的異常就是內存溢出(OOM)。
拋開由于使用不當而造成的內存泄漏,在一個復雜的 UI 上進行頻繁的頁面更新,容易造成 OOM ,即使沒有發(fā)生,那么也會帶來界面卡頓的現(xiàn)象,非常影響用戶體驗。
還有一點,就是在編程時候的封裝思想。我們希望也努力嘗試將不同的東西分別封裝起來然后組合在一起使用,這樣盡量達到解耦的效果,以更加靈活的方式面對未來需求的變更。
ok,讓我們將在邏輯上獨立的東西封裝起來,單獨運行在一個進程中,不影響 UI 進程的使用,從而使它們各司其職,協(xié)作起來。

讓組件運行在新的進程中

組件可以運行在指定的進程中,這時只需要在 manifest.xml 配置文件中,指定組件的 process 屬性。

<service
            android:name=".DataProcessService"
            android:label="@string/title_activity_login"
            android:process=":DataSerivce"/>

如上,我們將 DataProcessService 運行在指定的 login 進程中。前面的冒號,是指它將運行在一個私有進程中,進程名的前綴為包名。假設包名為 "com.yuegs.process",則該組件將運行在 "com.yuegs.process:DataService" 進程中。
這是一個私有進程,不能和其他應用共享。相對的就有全局進程的概念:

<service
            android:name=".DataProcessService"
            android:label="@string/title_activity_login"
            android:process="com.yuegs.newProcess"/>

以上 activity 將運行在進程名為 "com.yuegs.newProcess" 的進程中。其他應用組件可以通過 ShareUserId 共享這個進程。
一個小插曲,讓我們向系統(tǒng)盡量請求更多的內存空間,在 manifest.xml 中配置:

    <application
        android:name=".App"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        //注意這里
        android:largeHeap="true">

這時,將向系統(tǒng)申請更多的堆內存,給不給取決于設備和系統(tǒng)。

多進程使用的坑

我們使用多進程的目的,主要是為了減輕主用戶交互進程的堆內存使用壓力。但是,如果使用不當,效果可能不明顯,甚至起發(fā)作用。
每啟動一個新的進程,Application 就會被實例化創(chuàng)建一次。這也就意味著,如果我們在 Application 中進行了一些全局初始化工作,那么,必須根據(jù)實際情況,處理好初始化工作。

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        String processName = Utils.getProcessName();
        if(processName.equals("com.yuegs.process:DataService")){
            PushUtils.init();
        }else if(processName.equals("com.yuegs.process")){
            PicassoUtils.init();
        }
    }
}

即,根據(jù)需要分別在不同進程中初始化不同的資源。注意,這里容易出現(xiàn) bug。比如,在初始化長連接的時候,原本只是想在 push 進程中使用,但是不小心在 com.yuegs.process 進程中也初始化了,那么將出現(xiàn)兩條長連接。然而,如果一臺設備只允許一條連接,那么這就悲劇了,我就是遇到過這樣的情況:使用了定位服務,它運行在新的進程中,我沒有進行資源分別初始化,稍不注意,這條連接被實例化了兩條。

跨進程的通信問題

現(xiàn)在我們做到了對不同模塊的多進程應用,但是,問題是協(xié)作是需要通信的。
最容易想到的,可能是 aidl。但是它的使用相對繁瑣,也更加強大,現(xiàn)在我們需要一個更加輕量級、更加方便的跨進程方式。
我們可以使用 Messenger。

如需讓接口跨不同的進程工作,則可使用 Messenger 為服務創(chuàng)建接口。服務可以這種方式定義對應于不同類型對象的 Handler。此 Handler 是 Messenger 的基礎,后者隨后可與客戶端分享一個 IBinder,從而讓客戶端能利用 Message 對象向服務發(fā)送命令。此外,客戶端還可定義自有Messenger,以便服務回傳消息。

引用來源
簡單來說,Messenger 封裝了一個 Handler,從而具備了發(fā)送 Message 的能力,同時,它也可以返回一個 IBinder,從而具備了跨進程的能力。

public class DataProcessService extends Service {

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Handler handler = new DataIncomingHandler() ;
        return new Messenger(new DataIncomingHandler()).getBinder();
    }
}

客戶端通過 bindService 綁定服務,服務端通過 Messenger 返回給客戶端它的代理類。
此時,我們注意到在構建 Messenger 時帶入了 Handler 參數(shù)。
注意,Handler 和創(chuàng)建它的線程以及該線程中的 MessageQueue 是息息相關、綁定在一起的,所以,Handler的創(chuàng)建場合很重要。如果我們希望 DataInComingHandler 運行在 DataService 進程中,那么,它必須在 DataService 進程中帶有 MessageQueue 的線程中創(chuàng)建。

下面,我們來啟動這個服務:

public class DataServiceProcessStart {

    private Messenger mService = null;

    private boolean mIsBound;

    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                                       IBinder service) {
            mService = new Messenger(service);
            DMHelper.inMsger = mService ;
            try {
                Message msg = Message.obtain(null,
                        DataIncomingHandler.DATA_SERVICE_PROCESS_CLIENT);
                //注意,這里用于接受服務端消息的 Messenger 的創(chuàng)建場合
                DMHelper.outMsger = new Messenger(new DataOutgoingHandler()) ;
                msg.replyTo = DMHelper.outMsger;
                mService.send(msg);
            } catch (RemoteException e) {

            }
        }

        public void onServiceDisconnected(ComponentName className) {
            mService = null;
        }
    };


    public void start(Context context){
        doBindService(context) ;
    }

    private void doBindService(Context context) {
        context.bindService(new Intent(context,
                DataProcessService.class), mConnection, Context.BIND_AUTO_CREATE);
        mIsBound = true;
    }

    public void doUnbindService(Context context) {
        if (mIsBound) {
            // Detach our existing connection.
            context.unbindService(mConnection);
            mIsBound = false;
        }
    }
}

調用 new DataServiceProcessStart().start() ,這樣就完成了服務綁定工作。
綁定完成之后,我們獲得的 DMHelper.inMsger 就擁有了向服務端進程發(fā)送消息的能力。同時,我們拿上利用這種能力,將主進程的 DMHelper.outMsger 發(fā)送給服務端,使得服務端也拿有客戶端的 Messenger ,可以向客戶端發(fā)送消息。

// handleMessage 將運行在 DataService 進程中
public class DataIncomingHandler extends Handler {

    public static final int DATA_SERVICE_PROCESS_CLIENT = 1 ;

    public DataIncomingHandler(){

    }

    @Override
    public void handleMessage(Message msg) {

        switch (msg.what){
            case DATA_SERVICE_PROCESS_CLIENT:
                //拿到客戶端的 messenger,在需要的時候發(fā)消息給客戶端
                DMHelper.outMsger = msg.replyTo ;
                break;
       }
}

DMHelper 是 DataMessage 的 Helper 類,用于封裝客戶端和服務端需要的 Messenger。

public class DMHelper {

    public static Messenger outMsger ;

    public static Messenger inMsger ;

    public static final String TAG = "DMHelper" ;
    //從 DataService 進程向 主進程中發(fā)送消息,消息處理在 主進程中
    public static void outMsgerSend(Message msg){
        try{
            outMsger.send(msg);
        }catch (Exception e){
            Logger.e(e.getMessage());
        }
    }

    /**
     * 從 主進程 往 DataService 進程中發(fā)消息,在 DataService 做消息處理
     * @param msg
     */
    public static void inMsgerSend(Message msg){
        try{

            inMsger.send(msg);
        }catch (Exception e){
            Logger.e(e.toString());
        }
    }
}

小結

現(xiàn)在,我們可以愉快的使用多進程工作了。
跨進程的通信,是個相對麻煩且容易出錯的地方,但是從邏輯的清晰性和節(jié)省內存的角度來看,還是值得的。
我們都知道,非前臺進程,都有可能因為系統(tǒng)資源緊張的問題,而被系統(tǒng)殺死,一個可以優(yōu)化的方向就是進程管理。當進程由于資源緊張或者異常崩潰之后,重啟進程;或者在服務進程中檢測內存使用,當內存資源緊張的時候,也可以做一些工作。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377