前言
? ? ? ?正常情況下,一個apk啟動后只會運行在一個進程中,其進程名為AndroidManifest.xml文件中指定的應用包名,所有的基本組件都會在這個進程中運行。但是如果需要將某些組件(如Service、Activity等)運行在單獨的進程中,就需要用到android:process屬性了。我們可以為android的基礎組件指定process屬性來指定它們運行在指定進程中。android使用多進程重構項目架構,可以分擔主進程壓力以免因資源消耗過大被crash掉,另外多進程相互監聽喚醒,可以使應用程序長期駐守后臺接受即時消息和通知,防止應用被系統回收。下面就總結一下多進程開發的經驗及優化。
一、多進程概念
? ? ? ?一般情況下,一個應用程序就是一個進程,這個進程名稱就是應用程序包名。我們知道進程是系統分配資源和調度的基本單位,所以每個進程都有自己獨立的資源和內存空間,別的進程是不能任意訪問其他進程的內存和資源的。
二、多進程機制
? ? ? ?四大組件在AndroidManifest文件中注冊的時候,有個屬性android:process這里可以指定組件的所處的進程。
? ? ? ?對process屬性的設置有兩種形式:
? ? ? ?第一種形式:如android:process=":remote",以冒號開頭,冒號后面的字符串原則上是可以隨意指定的。如果我們的包名為“com.example.processtest”,則實際的進程名為“com.example.processtest:remote”。這種設置形式表示該進程為當前應用的私有進程,其他應用的組件不可以和它跑在同一個進程中。
? ? ? ?第二種形式:如android:process="com.example.processtest.remote",以小寫字母開頭,表示運行在一個以這個名字命名的全局進程中,其他應用通過設置相同的ShareUID可以和它跑在同一個進程。
? ? ? ?下面通過一個例子來進行一下驗證。我們定義兩個類:ProcessTestActivity和ProcessTestService,然后在AndroidManifest.xml文件中增加這兩個類,并為我們的Service指定一個process屬性,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.processtest" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="19" /> <application android:name="com.example.processtest.MyApplication" android:icon="@drawable/ic_launcher" android:label="@string/app_name"> <activity android:name=".ProcessTestActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".ProcessTestService" android:process=":remote"> </service> </application> </manifest>
運行代碼,通過DDMS進行觀察
? ? ? ?我們可以看到兩個進程,名字分別是“com.example.processtest”和“com.example.processtest:remote”,進程ID分別為2722和2739。
? ? ? ?ProcessTestService運行在一個單獨的私有進程中,如果想讓ProcessTestService運行在一個全局進程中,那么只需修改android:process="com.example.processtest.remote"即可。Android為每一個應用程序分配了一個獨立的虛擬機,或者說為每個進程都分配了一個獨立的虛擬機,不同的虛擬機在內存分配上 有不同的地址空間,這就會導致在不同的虛擬機中訪問同一類的對象會產生多份副本。在一個進程中修改某個值只會影響當前進程,對其他進程不會造成任何影響。
三、多進程優點
一般來說,Android應用多進程有三個好處:
? ? ? ?1)我們知道Android系統對每個應用進程的內存占用是有限制的,而且占用內存越大的進程,通常被系統殺死的可能性越大。讓一個組件運行在單獨的進程中,可以減少主進程所占用的內存,降低被系統殺死的概率.
? ? ? ?2)如果子進程因為某種原因崩潰了,不會直接導致主程序的崩潰,可以降低我們程序的崩潰率。
? ? ? ?3)即使主進程退出了,我們的子進程仍然可以繼續工作,假設子進程是推送服務,在主進程退出的情況下,仍然能夠保證用戶可以收到推送消息。
四、多進程缺點
? ? ? ?我們已經開啟了應用內多進程,那么,開啟多進程是不是只是我們看到的這么簡單呢?其實這里面會有一些陷阱,稍微不注意就會陷入其中。我們首先要明確的一點是進程間的內存空間時不可見的。
從而,開啟多進程后,我們需要面臨這樣幾個問題:
? ? ? ?1)Application的多次重建。運行在同一個進程中的組件是屬于同一個虛擬機和同一個Application的,運行在不同進程中的組件是屬于兩個不同的虛擬機和Application的。
? ? ? ?2)靜態成員和單例模式完全失效。
? ? ? ?3)文件共享問題。比如SharedPreferences 的可靠性下降。
? ? ? ?4)斷點調試問題。
1、Application的多次重建
我們先通過一個簡單的例子來看一下第一種情況。
? ? ? ?Manifest文件如上面提到的,定義了兩個類:ProcessTestActivity和ProcessTestService,我們只是在Activity的onCreate方法中直接啟動了該Service,同時,我們自定義了自己的Application類。代碼如下:
public class MyApplication extends Application { public static final String TAG = "viclee"; @Override public void onCreate() { super.onCreate(); int pid = android.os.Process.myPid(); Log.d(TAG, "MyApplication onCreate"); Log.d(TAG, "MyApplication pid is " + pid); }
}
public class ProcessTestActivity extends Activity { public final static String TAG = "viclee"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_process_test); Log.i(TAG, "ProcessTestActivity onCreate"); this.startService(new Intent(this, ProcessTestService.class)); }
}
public class ProcessTestService extends Service { public static final String TAG = "viclee"; @Override public void onCreate() { Log.i(TAG, "ProcessTestService onCreate"); } @Override public IBinder onBind(Intent arg0) { return null; } }
執行上面這段代碼,查看打印信息:
? ? ? ?我們發現MyApplication的onCreate方法調用了兩次,分別是在啟動ProcessTestActivity和ProcessTestService的時候,而且我們發現打印出來的pid也不相同。由于通常會在Application的onCreate方法中做一些全局的初始化操作,它被初始化多次是完全沒有必要的。出現這種情況,是由于即使是通過指定process屬性啟動新進程的情況下,系統也會新建一個獨立的虛擬機,自然需要重新初始化一遍Application。那么怎么來解決這個問題呢?
? ? ? ?我們可以通過在自定義的Application中通過進程名來區分當前是哪個進程,然后單獨進行相應的邏輯處理。
public class MyApplication extends Application { public static final String TAG = "viclee"; @Override public void onCreate() { super.onCreate(); int pid = android.os.Process.myPid(); Log.d(TAG, "MyApplication onCreate"); Log.d(TAG, "MyApplication pid is " + pid); ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); List<ActivityManager.RunningAppProcessInfo> runningApps = am.getRunningAppProcesses(); if (runningApps != null && !runningApps.isEmpty()) { for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) { if (procInfo.pid == pid) { if (procInfo.processName.equals("com.example.processtest")) { Log.d(TAG, "process name is " + procInfo.processName); } else if (procInfo.processName.equals("com.example.processtest:remote")) { Log.d(TAG, "process name is " + procInfo.processName); } } } } }
}
運行之后,查看Log信息:
? ? ? ?圖中可以看出,不同的進程執行了不同的代碼邏輯,可以通過這種方式來區分不同的進程需要完成的初始化工作。
2、靜態變量和單例模式完全失效
? ? ? ?下面我們來看第二個問題,將之前定義的Activity和Service的代碼進行簡單的修改,代碼如下:
public class ProcessTestActivity extends Activity { public final static String TAG = "viclee"; public static boolean processFlag = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_process_test); processFlag = true; Log.i(TAG, "ProcessTestActivity onCreate"); this.startService(new Intent(this, ProcessTestService.class)); }
}
public class ProcessTestService extends Service { public static final String TAG = "viclee"; @Override public void onCreate() { Log.i(TAG, "ProcessTestService onCreate"); Log.i(TAG, "ProcessTestActivity.processFlag is " + ProcessTestActivity.processFlag); } @Override public IBinder onBind(Intent arg0) { return null; } }
重新執行代碼,打印Log:
? ? ? ?針對靜態變量失效的問題,我們可以使用Intent或者aidl等進程通訊方式傳遞內容。
3、文件共享問題
? ? ? ?第三個問題是文件共享問題。多進程情況下會出現兩個進程在同一時刻訪問同一個數據庫文件的情況。這就可能造成資源的競爭訪問,導致諸如數據庫損壞、數據丟失等。在多線程的情況下我們有鎖機制控制資源的共享,但是在多進程中比較難,雖然有文件鎖、排隊等機制,但是在Android里很難實現。解決辦法就是多進程的時候不并發訪問同一個文件,比如子進程涉及到操作數據庫,就可以考慮調用主進程進行數據庫的操作。
4、斷點調試問題
? ? ? ?最后是斷點調試的問題。調試就是跟蹤程序運行過程中的堆棧信息,由于每個進程都有自己獨立的內存空間和各自的堆棧,無法實現在不同的進程間調試。不過可以通過下面的方式實現:調試時去掉AndroidManifest.xml中android:process標簽,這樣保證調試狀態下是在同一進程中,堆棧信息是連貫的。待調試完成后,再將標簽復原。
五、多進程使用場景
? ? ? ?類似音樂類、跑步健身類、手機管家類等長時間需要在后臺運行的應用。這些應用的特點就是,當用戶切到別的應用,或者關掉手機屏幕的時候,應用本身的核心模塊還在正常運行,提供服務。如果因為手機內存過低,或者是進程重要性降低,導致應用被殺掉,后臺服務停止,對于這些應用來說,就是滅頂之災。合理利用多進程,將核心后臺服務模塊和其他UI模塊進行分離,保證應用能更穩定的提供服務,從而提升用戶體驗。
舉個例子:
現在要做一款音樂播放器,現在有以下幾種方案:
A. 在Activity中直接播放音樂。
B. 啟動后臺Service,播放音樂。
C. 啟動前臺Service,播放音樂。
D. 在新的進程中,啟動后臺Service,播放音樂。
E. 在新的進程中,啟動前臺Service,播放音樂。
A. 在Activity中直接播放音樂。
? ? ? ?在A中,我們的播放器是直接在activity中啟動的。首先這么做肯定是不對的,我們需要在后臺播放音樂,所以當activity退出后就播不了了,之所以給出這個例子是為了控制變量作對比。
? ? ? ?然后我們來看下A的使用場景。
? ? ? ?音樂播放器無非是打開app,選歌,播放,退到桌面,切其他應用。我們選取了三個場景,打開、按home切換其他應用、按back退回桌面。讓我們看一下A的相對應的oom_adj、oom_score、oom_score_adj的值。(下面三張圖依次對應為【打開狀態】、【按了Home鍵被切換狀態】、【按了Back鍵被退出狀態】)
上圖為打開狀態下oom_adj、oom_score、oom_score_adj的值
上圖為按了Home鍵狀態下oom_adj、oom_score、oom_score_adj的值
上圖為按了Back鍵狀態下oom_adj、oom_score、oom_score_adj的值
? ? ? ?當我們應用在前臺的時候,無論adj還是score還是score_adj,他們的值都非常的小,基本不會被LMK所殺掉,但是當我們按了Home之后,進程的adj就會急劇增大,變為7,相應的score和score_adj也會增大。在上篇文章中我們得知,adj=7即為被切換的進程,兩個進程來回切換,上一個進程就會被設為7。當我們按Back鍵的時候,adj就會被設為9,也就是緩存進程,優先級比較低,有很大的幾率被殺掉。
B. 啟動后臺Service,播放音樂。
? ? ? ?B是直接啟動一個后臺service并且播放音樂,這個處理看起來比A好了很多,那么實際上,B的各個場景的優先級和A又有什么不同呢?讓我們來看下B的對應的打開、切換、退出相應的adj、score、score_adj的值。(下面三張圖依次對應為【打開狀態】、【按了Home鍵被切換狀態】、【按了Back鍵被退出狀態】)
上圖為打開狀態下oom_adj、oom_score、oom_score_adj的值
上圖為按了Home鍵狀態下oom_adj、oom_score、oom_score_adj的值
上圖為按了Back鍵狀態下oom_adj、oom_score、oom_score_adj的值
? ? ? ?B的情況其實是與A類似的,三種狀態的adj、score_adj的值都是一樣的,只有score有一點出入,其實分析源碼得知,LMK殺進程的時候,score的左右其實并不大,所以我們暫時忽略它。所以,與A相比,他們的adj和score_adj的值都相同,如果遇到內存不足的情況下,這兩個應用誰占得內存更大,誰就會被殺掉。不過鑒于A實在activity中播放音樂,所以B還是比A略好的方案。
C. 啟動前臺Service,播放音樂。
? ? ? ?C的話是啟動一個前臺Service來播放音樂。讓我們來看一下對應的值。(下面三張圖依次對應為【打開狀態】、【按了Home鍵被切換狀態】、【按了Back鍵被退出狀態】)
上圖為打開狀態下oom_adj、oom_score、oom_score_adj的值
上圖為按了Home鍵狀態下oom_adj、oom_score、oom_score_adj的值
上圖為按了Back鍵狀態下oom_adj、oom_score、oom_score_adj的值
? ? ? ?在前臺的時候,和AB是一樣的,adj都是0,當切到后臺,或者back結束時,C對應的adj就是2,也就是可感知進程。adj=2可以說是很高優先級了,非root手機,非系統應用已經沒有辦法將其殺掉了。adj<5的應用不會被殺掉。
? ? ? ?總的來說,C方案比B優秀,擁有前臺Service的C更不容易被系統或者其他應用所殺掉了,進程的優先級一下子提高到了2,相對于B來說更穩定,用戶體驗更好。不過有一點不足是必須啟動一個前臺service。不過現在大部分的音樂類軟件都會提供一個前臺service,也就不是什么缺點了。其實也是有灰色方法可以啟動一個不顯示通知的前臺service,這里就不過多介紹了。
那么還有可改進的余地嗎?
答案當然是肯定的。
D. 在新的進程中,啟動后臺Service,播放音樂。
? ? ? ?終于我們的主角,多進程登場了。
? ? ? ?D把應用進行了拆分,把用于播放音樂的service放到了新的進程內,讓我們看一下對應的值。(下面三張圖依次對應為【打開狀態】、【按了Home鍵被切換狀態】、【按了Back鍵被退出狀態】)
上圖為打開狀態下oom_adj、oom_score、oom_score_adj的值
上圖為按了Home鍵狀態下oom_adj、oom_score、oom_score_adj的值
上圖為按了Back鍵狀態下oom_adj、oom_score、oom_score_adj的值
? ? ? ?上面三張圖對應的是D應用主進程的ADJ相關值,我們可以看出來,跟A類似,adj都是0,7,9。由于少了service部分,內存使用變少,最后計算出的oom_score_adj也更低了,意味著主進程部分也更不容易被殺死。
下面我們看下拆分出的service的相關值
上圖為后臺Service的oom_adj、oom_score、oom_score_adj的值
? ? ? ?因為是service進程,所以不受打開,關閉,切換所影響,這里就放了一張圖。
? ? ? ?我們可以看到,service的adj值一直是5,也就是活躍的服務進程,相比于B來說,優先級高了不少。不過對于C來說,其實這個方案反倒不如C的adj=2的前臺進程更穩定。但是D可以自主釋放主進程,使D實際所占用的內存很小,從而不容易被殺掉。那么到底C和D誰是更優秀的設計?我個人認為,在ABCDE這5個設計中,D是最具智慧的設計,具體是為什么?先賣個關子,等我們說完了E,再作總結。
E. 在新的進程中,啟動前臺Service,播放音樂。
? ? ? ?E也是使用了多進程,并且在新進程中,使用了前臺service,先來看下對應的值。(下面三張圖依次對應為【打開狀態】、【按了Home鍵被切換狀態】、【按了Back鍵被退出狀態】)
上圖為打開狀態下oom_adj、oom_score、oom_score_adj的值
上圖為按了Home鍵狀態下oom_adj、oom_score、oom_score_adj的值
上圖為按了Back鍵狀態下oom_adj、oom_score、oom_score_adj的值
? ? ? ?這個不多解釋,和ABD基本差不多,都是0,7,9。我們看下拆分出來的進程的值。? ? ? ?其實我們可以做個比喻,把整個Android系統比喻成一個旅游景點,Low Memory Killer就是景點的門衛兼保安,然后我們每個進程的ADJ相當于手里的門票,有的人是VIP門票,有的人是普通門票。景點平常沒人的時候還好,誰拿票都能進,當人逐漸擁擠的時候,保安就開始根據票的等級,往外轟人。E方案就是一個拿著普通票的媽媽,帶著一個VIP的孩子去參觀,D方案就是一個拿著普通票的媽媽,帶著一個拿著中等票的孩子參觀。當內存不夠的時候,保安會先把兩個媽媽轟出去,孩子們在里面看,再不夠了,就會把D孩子給轟出去。這么看來,顯然E的效果更好一些,不過由于Android系統對于VIP票的發放沒有節制,大家都可以領VIP票,那也就是相當于沒有VIP票了。所以如果E方案是一種精明,那么D才是真正的智慧。將調度權還給系統,做好自己,維護好整個Android生態。
多模塊應用
? ? ? ?多進程還有一種非常有用的場景,就是多模塊應用。比如我做的應用大而全,里面肯定會有很多模塊,假如有地圖模塊、大圖瀏覽、自定義WebView等等(這些都是吃內存大戶),還會有一些諸如下載服務,監控服務等等,一個成熟的應用一定是多模塊化的。
? ? ? ?首先多進程開發能為應用解決了OOM問題,Android對內存的限制是針對于進程的,這個閾值可以是48M、24M、16M等,視機型而定,所以,當我們需要加載大圖之類的操作,可以在新的進程中去執行,避免主進程OOM。
? ? ? ?多進程不光解決OOM問題,還能更有效、合理的利用內存。我們可以在適當的時候生成新的進程,在不需要的時候及時殺掉,合理分配,提升用戶體驗。減少系統被殺掉的風險。
? ? ? ?多進程還能帶來一個好處就是,單一進程崩潰并不影響整體應用的使用。例如我在圖片瀏覽進程打開了一個過大的圖片,java heap 申請內存失敗,但是不影響我主進程的使用,而且,還能通過監控進程,將這個錯誤上報給系統,告知他在什么機型、環境下、產生了什么樣的Bug,提升用戶體驗。
? ? ? ?再一個好處就是,當我們的應用開發越來越大,模塊越來越多,團隊規模也越來越大,協作開發也是個很麻煩的事情。項目解耦,模塊化,是這階段的目標。通過模塊解耦,開辟新的進程,獨立的JVM,來達到數據解耦目的。模塊之間互不干預,團隊并行開發,責任分工也明確。至于模塊化開發與多進程的結合,后續會寫一篇專門的文章來研究這個問題。