這一年半以來,關于 Android,我都寫了什么文章(一)

本文原創,轉載請注明出處。
歡迎關注我的 簡書 ,關注我的專題 Android Class 我會長期堅持為大家收錄簡書上高質量的 Android 相關博文。

16年7月份畢業,轉眼從學校到公司已經一年半的時間了。在16年年末寫過一篇年終總結,當時立下一個關于寫作的目標,打算17年至少每個月產出一篇??戳讼挛业陌l文記錄,額。。。勉勉強強達到了吧。

今年以來我始終覺得沒什么可寫的,原因可以算是找到了正確獲取知識的辦法,關于我想寫的東西,網上已經有很成熟很完善的文章了,所以自然不需要重復造輪子。

從分享知識的角度來說,確實如此,但是技術博客作為未來復習的筆記,也十分有意義。就好像以前上數學課,上課確實聽懂了,但是不做習題,不做筆記,是不可能完全消化這個知識的,編程也是如此,看書/看文章獲取知識,寫 demo 鞏固,最后在項目中應用,不斷填坑,方可真正吸收。

以上說了這么多,其實就是我打算最近復習一下,這一年半我寫了哪些知識點,準備復習一下,溫故而知新,還是蠻有意義的。

Android 進程創建流程

Android 進程的創建可以分為三個階段:

  1. 發起進程:比如從桌面點擊圖標進入,那么發起進程就是 Launcher 進程,如果是 A 應用進入到 B 應用,那么發起進程就是 A 應用所在的進程。發起進程會通過 Binder 發送一個消息到 system_service 進程。system_service 就是 framework 層的系統服務進程。
  2. system_service 進程調用 Process.start 方法,通過 socket 將消息發送的 zygote 進程。
  3. zygote 進程最終 fork 出應用真正的進程,并且通過 invokeStaticMain 方法,通過反射,調用 ActivityThread 的 main 方法,真正的進入了程序的入口。

Android 進程的種類級別

前臺進程:
有正在與用戶交互的 Activity;調用了 startForeground 的 Service,或者 Service bind 了一個前臺交互的 Activity,Service 正在調用生命周期方法,BoradCastReceiver 正在調用 onReceive 方法。
可見進程:
Activity 調用了 onPause 方法,但是并沒有調用 onStop,或者一個 Service 綁定了這樣的一個 Activity。
服務進程:
當 Activity 調用了 startService 并且不屬于以上兩種情況下,這個進程就是服務進程。
后臺進程:
當前進程的組件已經調用了 onStop,這個時候在內存緊張的時候,有可能會被系統回收,系統回收后臺進程的時候,會遵循 LRU 算法,保證最近使用的那個進程,最后被回收。
空進程:
已經沒有任何存活組件的進程,通??者M程都是留給下次進入做緩沖的。

Android 進程清理機制

Android 系統是基于 Linux 系統的,Linux 系統清理進程通過 Low Memory Killer 機制完成。Low Memory Killer 會分析此進程的 adj 所占的級別,以及這個進程所占的內存大小兩方面來分析。不同種類的進程 adj 值是不同的,adj 值越高,越可能被系統回收,當 adj 值相同時,會先回收所占內存更大的那個。

前臺進程:adj = 0;可見進程 adj = 1;服務進程 adj = 5;后臺進程 adj = 7;空進程 adj = 9;

所以對于一個 app 進程如何才能“綠色”的?;钅??提升其 adj 的值才是正確的思路,這個我們未來再說。

Android 線程的管理

先來說說進程和線程的區別吧:進程就是一個擁有資源的單位,有獨立的內存地址,不同進程之間,內存是不可共享的;線程是最小執行和調度的單元,不同的線程可共享進程中的堆內存;對于 Linux 系統來說,無論進程還是線程,都是一個可執行的 task_struck 結構體。

在 Android 中,盡可能的要用線程池去管理線程,不要直接 new Thread().start,因為使用線程池控制線程可以合理利用空閑線程,并且對線程進行回收,高效的利用資源。

Excutors 中有幾個工廠靜態方法,來創建幾個不同的線程池:

newCachedThreadPool:無 corethread,maxthread 數為 Integer.MAX,空閑線程超過60s時,會被回收。

newFixedThreadPool:Thread 數量固定的線程池,線程空閑也不會被回收,當線程無空閑的時候,任務會被放在 LinkedBlockQueue 中。

newSingleThreadPool:單一線程,不會被超時回收,任務會依次執行。

newScheduleThreadPool:創建一個線程池,可以指定核心線程數和最大線程數,每間隔一定時間去執行任務。

線程的任務處理方法,首先判斷核心線程有沒有滿,如果沒有滿,即使有空閑的,也會創建一個新線程,如果滿了,則優先復用空閑線程,其次才會創建新線程,如果線程達到了最大線程數,則將任務放入任務隊列中,如果最后任務隊列都滿了,那就會拋出異常,或者進行其他處理。

其實這幾個線程池都是對 ThreadPoolExecutor 的封裝,設置不同的參數罷了。

對于非常消耗 CPU 的任務,盡量不要讓核心線程數超過 CPU 核心數 。Runtime.getRuntime().availableProcessors

對于I/O操作頻繁的任務,可以盡可能多的創建線程數。

以上就是對 Android 線程池的創建和使用的總結。

Context 的原理用法以及和四大組件的關系

Context 是一個頂層抽象類,掌管著 Android 的資源和組件。
直接子類:ContextWrapper(包裝類) ContextImpl(實現類) MockContext(測試用的)

ContextWrapper 的直接子類:Application、Service、ContextThemeWrapper,ContextThemeWrapper 的子類是 Activity(因為 Activity 中有 theme 屬性)

ContextWrapper 本身并沒有真正實現 Context 的方法,而是持有了一個 mBase 變量,這個 mBase 就是 ContextImpl,通過 mBase 來實現 Context 的方法,這就是裝飾模式。

ContextWrapper 和 ContextImpl 是何時建立聯系的呢?在 ActivityThread 的 main 方法中,調用了 mThread.attach 方法,將 AMS(Activity Service 等調度者) 與 ApplicationThread(四大組件事件接受者,通過 mH 通知到主線程) 建立聯系。

當 BIND_APPLICATION、LAUNCH_ACTIVITY、START_SERVICE 事件到來之時,會 new 一個 ContextImpl 然后調用 setOuterContext,這樣就將二者建立了聯系。

未來我們再去詳細探究 Application、Service、Activity 的啟動流程,和被調度的流程吧。

Context 的使用

不同對象的 Context 在使用上是有區別的,比如只有 Activity 的 context 才可以 show a dialog。對于和 View 相關的 context,只可以用 Activity 的,但其他的 Context,比如調數據庫、系統服務等等,盡量還是使用 Application 的 Context,畢竟 Application 的 Context 生命周期是伴隨著進程本身的,不會造成內存泄漏。

context.getApplicationContext

與 Context 相關聯的資源都是同一份,通過 ResourceManager 獲取,都指向了同一個文件夾下,只不過不同的 Context 對這個資源處理的方式不同才導致了這個差異。

內存泄漏

剛剛我們提到了 Context 使用不當導致的內存泄漏,內存泄漏在 java 中就是“該松手時不松手”,本來應該被 java gc 掉的對象,但是依然有著引用可達它。所以這個時候造成了內存泄漏。在 Android 中,內存泄漏有 90% 都是因為 Activity 已經 onDestory 了,但是依然有其他對象引用著它,導致它無法被 gc 回收掉。

所以理所當然的,當這個 Activity 中有一些對象引用著它,并且其生命周期超過了 Activity 本身之時,就會造成內存泄漏。

Handler:
Handler 通常都會怎么用呢?

Handler mHandler = new Handler()

如果在 Activity 中這樣聲明一個內部類的話,是可能造成內存泄漏的,為什么呢?因為在 Java 中非靜態的內部類會持有外部類的引用,當 Activity 已經 onDestory 了,但是主線程的 MessageQueue 依然還有沒處理的消息,則 Handler 一直抓著 Activity 不讓其被 GC 回收。

正確的用法:

private SafeHandler mHandler = new SafeHandler(this);

private static class SafeHandler extands Handler {
    private final WeakReference<SimpleActivity> mRef;
    public SafeHandler(SimpleActivity activity){
        mRef = new WeakReference<>(activity);
    }
    
    @Override
    public void handleMessage(Message msg) {
        SimpleActivity activity = mRef.get();
        if(activity != null){
        }
    }
}

靜態變量:
靜態變量有什么特性呢?隨著類(ClassLoader)的創建而創建,被所有類的對象共用,也就是說可能會有一些線程安全問題。隨著類的銷毀而被銷毀,類什么時候被銷毀呢?是在 ClassLoader 銷毀這個類的時候,也就是在這個進程被殺死的時候。

所以一個靜態變量的生命周期是與所在的 app 進程同步的,比如你在一個 activity 中保有了一個 static 的 Context(this),即使 Activity 調用了 onDestory ,這個 Context 依然是無法回收的。

與之情況相似的還有單例模式,因為單例模式也是占用內存,無法釋放,所以如果傳入了 Context,盡量使用 Application 的 Context 防止內存泄漏。

匿名內部類:
比如 new Thread(new Runnable()) 的這種情況所造成的內存泄漏,也是匿名內部類會持有外部類的強引用,導致 Activity 無法被回收。

View 和屬性動畫導致的內存泄漏

因為動畫持有 View 的引用,View 持有 Activity 的引用,所以要避免 Activity 被銷毀了,但是 View 依然抓著它不讓它回收。比如要在 activity onDestory 之前 dissmiss dialog,停止循環的屬性動畫等等。

LeakCanary 原理分析

既然上面說到了內存泄漏,我們也知道在 Android 上有一個非常好用的開源庫檢測內存泄漏,那就是 LeakCanary,本著知其所以然的態度,大致看了下 LeakCanary 的源碼,分析下原理:

  1. 在一個 Activity 調用 onDestory 之時,是 LeakCanary 開始進行檢測的入口,檢測的類是 RefWatcher 類 的 watch 方法。首先會給這個 Activity 創建一個唯一的 ReferenceKey,并且使用一個帶 ReferenceQueue 參數的構造方法,創建一個 WeakReference,其目的是,當 Activity 被 GC 回收之時,會出現在這個 ReferenceQueue 中。如果反復 GC ,Activity 的對象都不在隊列里,說明就可能發生了內存泄漏,進行進一步分析。
  2. 接著調用了 watchExecutor.execute 方法,其目的是向主線程推一個消息執行 ensureGone 方法,為了不影響主線程其他任務的調用,這個消息只有在主線程空閑的時候執行。
  3. ensureGone 方法:
void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
    ...

    removeWeaklyReachableReferences();
    if (gone(reference) || debuggerControl.isDebuggerAttached()) {
        return;
    }
    gcTrigger.runGc();      // 手動執行一次gc
    removeWeaklyReachableReferences();
    if (!gone(reference)) {

        long startDumpHeap = System.nanoTime();
        long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap - gcStartNanoTime);

        File heapDumpFile = heapDumper.dumpHeap();
        if (heapDumpFile == null) {
            // Could not dump the heap, abort.
            Log.d(TAG, "Could not dump the heap, abort.");
            return;
        }
        long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap);

        heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs,
                watchDurationMs, gcDurationMs, heapDumpDurationMs));
    }
  }

removeWeaklyReachableReferences 這個方法的目的就是移除弱引用,如果當前 Activity 的弱引用還在 retainedKeys 中,那么說明就還沒被移除,則手動調用一次 GC,重復上述的判斷,如果發現 Activity 的引用還在 retainedKeys 中,也就是說,這個 Activity 對象并沒有按照期望,被回收到 ReferenceQueue 中,則說明它極有可能發生了內存泄漏。

  1. 既然判斷出現了內存泄漏,那么下一步就是定位分析了。上面代碼中 heapDumpFile 這個就是我們要分析的內存文件,因為這個步驟比較耗時,所以被放進了一個新的進程里,HeapAnalyzerService 這個服務就跑在這個進程里進行內存泄漏分析,HeapAnalyzerService 通過調用 HeapAnalyzer 使用 HAHA 解析這個內存文件。
  2. 得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位內存泄漏。
  3. HeapAnalyzer 計算 到 GC roots 的最短強引用路徑,并確定是否是泄漏。如果是的話,建立導致泄漏的引用鏈。
  4. 引用鏈傳遞到 APP 進程中的 DisplayLeakService, 并以通知的形式展示出來。

上面就是 LeakCanary 的原理分析,LeakCanary 的問題是無法檢測出 Service 的內存泄漏的。如果最底層的MainActivity一直未走onDestroy生命周期(它在Activity棧的最底層),無法檢測出它的調用棧的內存泄漏。

先復習這么多吧,一篇一篇的來。我發現很多知識點一年以后重新看,理解會比當時更加深刻,更全面,所以時常復習是很重要的。

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

推薦閱讀更多精彩內容