Android從啟動到程式執行發生的事情

李中權的部落格發表於2016-04-14

前言

好久沒有寫部落格了,瞬間感覺好多學了的東西不進行一個自我的總結與消化總歸變不成自己的。通過部落格可能還可以找到一些當初在學習的時候沒有想到的問題。想了半天,從大二上學期自學Android以來還沒有對Android從啟動到程式執行期間進行一個完整的歸納,剛好最近又學到了一些新東西,那就以這篇部落格為媒介,總結一下從Android啟動到程式執行期間發生的所有事吧。包括什麼ClassLoader, JVM,IPC, 訊息處理機制要是總結到了就順帶BB一下。但是這裡就不包含很多細節了,比如為什麼PMS內部為什麼要這麼構造,好處是什麼,如果我來設計的話我會怎麼設計啊這種暫時就不總結了,因為我覺得以我現在的水平還有學習精力來說把這些細節都一個個的弄清楚有點沒抓住重點。現階段還是先能夠了解整個流程,有個大局觀才是最重要的。至於以後如果有需要或者是有精力的時候再一個個的突破。

在正式開始之前還是忍不住想要BB一下最近參加的京東筆試,被坑得有點憋屈。憋屈啥勒,被編譯器坑了。這次京東的筆試說實話感覺真的好簡單,真的沒有什麼技術上的難點,但是尼瑪程式設計題把我坑了。提前一個小時把程式碼在本地編譯器上編譯完成並通過,當時心裡還有些小激動,一提交,線上編譯器說得不到指定結果,尼瑪,頓時整個人都斯巴達了。最開始的時候還以為是自己本身程式碼的Bug,後來順著思路又理了幾遍,完全沒問題啊,又自己創了幾個新的輸入也都能夠執行,返回正常結果。整個人都是崩潰的,在這上面花了20多分鐘時候不經意間瞥了一下左邊的樣例輸入和輸出,哦豁,這下全懂了。

因為我沒有很多這種參加線上筆試的經驗,也沒在網上怎麼刷題,所以在樣例輸入和輸出那裡摻雜了一些自己想當然的想法。

題目要求的樣例輸入是一直輸入,有兩種情況,一種情況返回No,一種情況返回Yes並返回對應的結果。是要求連續輸入的,也就是你在輸入的時候我至少要用一個陣列或者是List、Map來儲存你的輸入。當檢測到輸入為空也就是直接按了回車的同時就開始執行,然後再一次性的列印出結果。我不知道啊,第一次看這種樣例輸入輸出,一看以為只要能返回就好了,然後就是分開做的,輸入錯的就返回No,輸入對的就返回Yes和結果,並不能夠一起輸入及返回。而這個時候時間又過了好多了,改程式碼的話整個程式碼的架構都要變,時間上完全來不及。這筆試要是程式設計題錯了那估計是沒戲了。

這其實也怪自己吧,怨不得別的,只好等下次了,只是這次的題真的簡單,錯過了好可惜,畢竟還是非常想進京東鍛鍊鍛鍊的,就算進不了去體驗京東的面試,知道哪裡有不足也是好的。

正式開始

上面BB了這麼多,也是超過了我的預料,這裡就正式開始這篇部落格了。

首先,我們知道,Android是基於Linux的一個作業系統,它可以分為五層,下面是它的層次架構圖,可以記一下,因為後面應該會總結到SystemServer這些Application Framework層的東西

Android的五層架構從上到下依次是應用層,應用框架層,庫層,執行時層以及Linux核心層。

而在Linux中,它的啟動可以歸為一下幾個流程:
Boot Loader-》初始化核心-》。。。。。。

當初始化核心之後,就會啟動一個相當重要的祖先程式,也就是init程式,在Linux中所有的程式都是由init程式直接或間接fork出來的。

而對於Android來說,前面的流程都是一樣的,而當init程式建立之後,會fork出一個Zygote程式,這個程式是所有Java程式的父程式。我們知道,Linux是基於C的,而Android是基於Java的(當然底層也是C)。所以這裡就會fork出一個Zygote Java程式用來fork出其他的程式。【斷點1】

總結到了這裡就提一下之後會談到的幾個非常重要的物件以及一個很重要的概念。

  • ActivityManagerServices(AMS):它是一個服務端物件,負責所有的Activity的生命週期,ActivityThread會通過Binder與之互動,而AMS與Zygote之間進行互動則是通過Socket通訊(IPC通訊在之後會總結到)
  • ActivityThread:它也就是我們俗稱的UI執行緒/主執行緒,它裡面存在一個main()方法,這也是APP的真正入口,當APP啟動時,就會啟動ActivityThread中的main方法,它會初始化一些物件,然後開啟訊息迴圈佇列(之後總結),之後就會Looper.loop死迴圈,如果有訊息就執行,沒有就等著,也就是事件驅動模型(edt)的原理。
  • ApplicationThread:它實現了IBinder介面,是Activity整個框架中客戶端和服務端AMS之間通訊的介面,同時也是ActivityThread的內部類。這樣就有效的把ActivityThread和AMS繫結在一起了。
  • Instrumentation:這個東西我把它理解為ActivityThread的一個工具類,也算是一個勞動者吧,對於生命週期的所有操作例如onCreate最終都是直接由它來執行的。

Android系統中的客戶端和伺服器的概念

在Android系統中其實也存在著伺服器和客戶端的概念,伺服器端指的就是所有App共用的系統服務,比如上面的AMS,PackageManagerService等等,這些系統服務是被所有的App共用的,當某個App想要實現某個操作的時候,就會通知這些系統服務。

繼續斷點1

當Zygote被初始化的時候,會fork出System Server程式,這個程式在整個的Android程式中是非常重要的一個,地位和Zygote等同,它是屬於Application Framework層的,Android中的所有服務,例如AMS, WindowsManager, PackageManagerService等等都是由這個SystemServer fork出來的。所以它的地位可見一斑。

而當System Server程式開啟的時候,就會初始化AMS,同時,會載入本地系統的服務庫,建立系統上下文,建立ActivityThread及開啟各種服務等等。而在這之後,就會開啟系統的Launcher程式,完成系統介面的載入與顯示。【斷點2】

Context總結

Context是一個抽象類,下面是它的註釋資訊,摘自原始碼。

/**
 * Interface to global information about an application environment.  This is
 * an abstract class whose implementation is provided by
 * the Android system.  It
 * allows access to application-specific resources and classes, as well as
 * up-calls for application-level operations such as launching activities,
 * broadcasting and receiving intents, etc.
 */
public abstract class Context {

從上面的這段話可以簡單理解一下,Context是一個關於應用程式環境的全域性變數介面,通過它可以允許去獲得資源或者類,例如啟動Activity,廣播,intent等等。

我的理解:Context的具體實現是Application, Activity,Service,通過Context能夠有許可權去做一些事情,其實我覺得就是一個執行環境的問題。

需要注意的地方

Android開發中由於很多地方都包含了Context的使用,因此就必須要注意到記憶體洩露或者是一些可能會引起的問題。

例如在Toast中,它的Context就最好設定為Application Context,因為如果Toast在顯示東西的時候Activity關閉了,但是由於Toast仍然持有Activity的引用,那麼這個Activity就不會被回收掉,也就造成了記憶體洩露。

Toast的相關總結

上面舉例的時候舉到了Toast,其實Toast也是很有意思的一個東西,它的show方法其實並不是顯示一個東西這麼簡單。

Toast實際上是一個佇列,會通過show方法把新的任務加入到佇列當中去,列隊中只要存在訊息就會彈出來使用,而佇列的長度據說預設是40個(這是網上搜出來的,我在原始碼中沒找到對應的設定,感覺也沒啥必要就沒找了)。

所以這裡就要注意一下show這個操作了,它並不是顯示內容,而是把內容入佇列。

/**
     * Show the view for the specified duration.
     */
    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

Handler的記憶體洩露

對於Handler來說,如果我們直接在AndroidStudio中建立一個非靜態內部類Handler,那麼Handler這一大片的區域會被AS標記為黃色,這個應該很多人都遇到過吧。實際上是因為這樣設定會造成記憶體洩露,因為每一個非靜態內部類都會持有一個外部類的引用,那麼這裡也就產生了一個記憶體洩露的可能點,如果當Activity被銷燬時沒有與Handler解除,那麼Handler仍然會持有對該Activity的引用,那麼就造成了記憶體洩露。

解決方案

使用static修飾Handler,這樣也就成了一個靜態內部類,那麼就不會持有對外部類的引用了。而這個時候就可以在Handler中建立一個WeakReference(弱引用)來持有外部的物件。只要外部解除了與該引用的繫結,那麼垃圾回收器就會在發現該弱引用的時候立刻回收掉它。

垃圾回收

關於垃圾回收的相關總結看我之前的部落格,傳送門:JVM原理及底層探索

四種引用方式

上面扯到了弱引用,就再BB一下四種引用方式吧。

  • 強引用:垃圾回收器打死都不會回收掉一個強引用的,那怕是出現OOM也不會回收掉強引用,所有new出來的都是強引用。
  • 軟引用:垃圾回收器會在記憶體不足的情況下回收掉軟引用,如果記憶體充足的話不會理它
  • 弱引用:它跟軟引用類似,但是它更脆弱,只要垃圾回收器一發現它,就會立刻回收掉它。比如一個物件持有一個強引用和弱引用,當強引用被取消時,那麼只要GC發現了它,就會立刻回收掉。只是GC發現它的這個過程是不確定的,有可能不會馬上發生,所以它可能還會多活一會,中間存在一個優先順序。
  • 虛引用:它跟上面3種方式都不同。我對虛引用的理解就是如果一個物件持有虛引用,那麼就可以在被GC回收前進行一些設定好的工作等等。因為虛引用有個機制,因為虛引用必須和引用佇列聯合使用,當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就回在回收物件的記憶體前,把這個虛引用加入到與之關聯的引用佇列中。而程式如果判斷到引用佇列中已經加入了虛引用,那麼就可以瞭解到被引用的物件馬上就要被垃圾回收了,這個時候就可以做些被回收之前的事情啦。

ClassLoader

類載入器按層次從頂層到下依次為Boorsrtap ClassLoader(啟動類載入器),Extension ClassLoader(擴充類載入器),ApplicationClassLoader(應用程式類載入器)

判斷兩個類是否是同一個類就是看它們是否是由同一個類載入器載入而來。

這裡就需要介紹一下雙親委派模式了:

雙親委派模式的意思就是:除了啟動類載入器之外,其餘的載入器都需要指定一個父類的載入器,當需要載入的時候會先讓父類去試著載入,如果父類無法載入也就是找不到這個類的話就會讓子類去載入

好處:防止記憶體中出現多份同樣的位元組碼

比如類A和類B都要載入system類,如果不是委託的話,類A就會載入一份,B也會載入一份,那麼就會出現兩份SYstem位元組碼
如果使用委託機制,會遞迴的向父類查詢,也就是首選用Bootstrap嘗試載入,如果找不到再向下,如果A用這個已經載入了的話會直接返回記憶體中的system而不需要重新載入。那麼就只會存在一份

延遲載入的應用:單例模式

對於Java來說,類是需要使用到時才會載入,這裡也就出現了一個延遲載入的效果。而在延遲載入的時候,會預設保持同步。這也就產生了一種單例模式的方式,具體的看我之前的部落格:設計模式_單例模式

我覺得在android所有的建立單例模式方法中裡延遲載入方式是最好吧,雖然列舉比延遲載入更好,effiective java中也很推薦,但是並不怎麼適用於Android,Android裡列舉的消耗是static的兩倍,延遲載入的話只要我們在使用延遲載入方式時做好反序列化的返回值readResolve()準備就好了。

繼續斷點2

上面BB了太多其他的,現在有點緩不過來,下次自己看自己部落格的時候會不會都被自己的思路帶得亂七八糟的。

上面的時候我們就已經完成了整個Android系統的開機以及初始化。接下來就可以B一下從點選APP圖示開始到APP內部程式執行起來的流程了。

當我們點選螢幕時,觸控式螢幕的兩層電極會連線在一起,也就產生了一個電壓(具體的我忘了,書上有,圖找不到了),當產生電壓的時候,就可以通過對應的驅動把當前按壓點的XY座標傳給上層,這裡也就是作業系統。作業系統在獲取到XY值的時候,就會對按壓點的範圍進行一個判斷,如果確定按壓點處於一個APP圖示或者是Button等等的範圍中時,作業系統也就會認為使用者當前已經點選了這個東西,啟動對應的監聽。

而當系統判斷我們點選的是APP圖示時,該App就由Launcher開始啟動了【斷點3】

Launcher

Launcher是一個繼承自Activity,同時實現了點選事件,長按事件等等的一個應用程式。

public final class Launcher extends Activity
        implements View.OnClickListener,OnLongClickListener, LauncherModel.Callbacks,View.OnTouchListener

當我們點選一個APP的圖示時,會呼叫Launcher內部的startActivitySafely()方法,而這個方法則會進行兩件事,一個是啟動目標activity,另一個功能就是捕獲異常ActivityNotFoundException,也就是常見的“找不到activity,是否已經在androidmenifest檔案中註冊?”。而在startActivity方法中,經過一系列的轉換最終會呼叫到startActivityForResult這個方法。

    @Override
    public void startActivity(Intent intent, @Nullable Bundle options) {
        if (options != null) {
            startActivityForResult(intent, -1, options);
        } else {
            // Note we want to go through this call for compatibility with
            // applications that may have overridden the method.
            startActivityForResult(intent, -1);
        }
    }

所以實際上,我對整個Android的介面是這樣理解的:

當系統完成初始化以及各種服務的建立之後,就會啟動Launcher這個應用程式(它也是繼承自Activity的,包含自己對應的xml佈局檔案),然後再把各種圖示按照一個正常APP佈局的方式放在上面,當我們點選APP圖示時,也就相當於在Launcher這個APP應用程式中通過startActivity(在底層最後會轉為startActivityForResult)來啟動這個APP。簡單的講,我覺得就是一個主要的APP(Launcher)裡面啟動了其他的功能APP,例如QQ、微信這些。【個人理解,如果以後發現不對再修改】

Android中點選事件的處理

當我們手指按下時,Android是如何處理點選事件的呢?如何確定是讓哪一個控制元件來處理呢?

簡單一句話:層層傳遞-冒泡的方式處理

舉個例子:現在公司來了個小專案,老闆一看分配給經理做,經理一看分配給小組長,小組長一看好簡單,分配給組員。如果在這個傳遞過程中(也就是還為分配到最底部時),某一層覺得我來負責這個比較好的話就會攔截掉這個訊息,然後把它處理了,下面的就收不到有訊息的這個通知。如果一直到了底層的話,組員如果能完成,就完成它。如果不能完成,那麼就報告給組長,說組長我做不來,邊學邊做要影響進度。組長一看我也做不來,就給經理,經理一看我也不會,就給老闆。這樣也就一層層的傳遞了。

總結一下就是訊息從上到下依次傳遞,如果在傳遞的過程中被攔截了就停止下傳。如果沒有被攔截,就一直傳遞到底部,如果底部不能夠消耗該訊息,那麼就又一層層的返回來,返給上層,直到被消耗或者是到達最頂層。

在Android中,存在三個重要的方法:

  • dispathTouchEvent(MotionEvent ev)
  • onInterceptTouchEvent(MotionEvent ev)
  • onTouchEvent(MotionEvent ev)

第一個方法負責事件的分發,它的返回值就是表示是否消耗當前事件。

第二個方法是用於判斷是否攔截該訊息,如果當前View攔截了某個時間,那麼在同一個事件序列中,此方法不會被再次呼叫。返回結果表示是否攔截當前事件

第三個方法就是處理事件。返回結果表示是否消耗當前事件,如果不小號,則在同一時間序列中,當前View無法再次接收到事件。

對於一個根ViewGroup來說,點選事件產生後,首先會傳遞給它,呼叫它的dispath方法。如果這個ViewGroup的onIntercept方法返回true就表示它要攔截當前事件,false就表示不攔截,這個時候事件就會繼續傳遞給子元素,接著呼叫子元素的dispath方法,直到被處理。

滑動衝突

順帶總結一下滑動衝突的解決吧

View的滑動衝突一般可以分為三種:

  • 外部滑動和內部滑動方向不一致
  • 外部滑動方向和內部滑動方向一致
  • 巢狀上面兩種情況

比如說一個常見的,外部一個ListView,裡面一個ScrollView。這個時候該怎麼解決呢?其實這裡想到了ViewPager,它裡面實際上是解決了滑動衝突的,可以借鑑一下它的。

滑動處理規則

一般來說,我們可以根據使用者手指滑動的方向以及角度來判斷使用者是要朝著哪個方向去滑動。而很多時候還可以根據專案的需求來指定一套合適的滑動方案。

外部攔截法

這種方法就是指所有的點選時間都經過父容器的攔截處理,如果父容器需要此時間就攔截,如果不需要此事件就不攔截。通過重寫父容器的onInterceptTouchEvent方法:

case MotionEvent.ACTION_DOWN:
    intercepted = false;
break;

case MotionEvent.ACTION_MOVE:
if(父類容器需要) {
    intercepted = true;
} else {
    intercepted = false;
}
break;

case MotionEvent.ACTION_UP:
    intercepted = false;
break;

return intercepted;

這裡有一點需要注意,ACTION_DOWN事件父類容器就必須返回false,因為如果父類容器攔截了的話,後面的Move等所有事件都會直接由父類容器處理,就無法傳給子元素了。UP事件也要返回false,因為它本身來說沒有太多的意義,但是對於子元素就不同了,如果攔截了,那麼子元素的onClick事件就無法觸發。

內部攔截法

這種方法指的是父容器不攔截任何時間,所有的事件都傳遞給子元素,如果子元素需要此事件就直接消耗掉,否則就交給父容器進行處理。它需要配合requestDisallowInterceptTouchEvent方法才能正常工作。我們需要重寫子元素的dispatch方法

case MotionEvent.ACTION_DOWN:
    parent.requestDisallowInterceptTouchEvent(true);
break;

MotionEvent.ACTION_MOVE:
    if(父容器需要此類點選事件) {
    parent.requestDisallowInterceptTouchEvent(false);
    }
break;

return super.dispatchTouchEvent(event);

這種方法的話父類容器需要預設攔截除了ACTION_DOWN以外的其他時間,這樣當子元素呼叫request方法的時候父元素才能繼續攔截所需的事件。

其他的

如果覺得上面兩個方式太複雜,看暈了,其實也可以自己根據專案的實際需要來指定自己的策略實現。例如根據你手指按的點的位置來判斷你當前觸碰的是哪個控制元件,以此來猜測使用者是否是要對這個控制元件進行操作。如果點選的是空白的地方,就操作外部控制元件即可。

【等有時間了就把ViewPager的處理總結一下,挺重要的】

繼續斷點3

  • 當我們點選桌面的APP圖示時,Launcher程式會採用Binder的方式向AMS發出startActivity請求
  • AMS在接收到請求之後,就會通過Socket向Zygote程式傳送建立程式的請求
  • Zygote程式會fork出新的子程式(APP程式)
  • 之後APP程式會再向AMS發起一次請求,AMS收到之後經過一系列的準備工作再回傳請求。
  • APP程式收到AMS返回的請求後,會利用Handler向主執行緒傳送LAUNCH_ACTIVITY訊息
  • 主執行緒在收到訊息之後,就建立目標Activity,並回撥onCreate()/onStart()/onResume()等方法,UI渲染結束後便可以看到App主介面 【斷點4】

Handler/Looper/Message Queue/ThreadLocal機制

Android的訊息機制主要是指Handler的執行機制,Handler的執行需要底層的MessageQueue和Looper的支撐

雖然MessageQueue叫做訊息佇列,但是實際上它內部的儲存結構是單連結串列的方式。由於Message只是一個訊息的儲存單元,它不能去處理訊息,這個時候Looper就彌補了這個功能,Looper會以無限迴圈的形式去查詢是否有新訊息,如果有的話就處理訊息,否則就一直等待(機制等會介紹)。而對於Looper來說,存在著另外的一個很重要的概念,就是ThreadLocal。

ThreadLocal

ThreadLocal它並不是一個執行緒,而是一個可以在每個執行緒中儲存資料的資料儲存類,通過它可以在指定的執行緒中儲存資料,資料儲存之後,只有在指定執行緒中可以獲取到儲存的資料,對於其他執行緒來說則無法獲取到該執行緒的資料。

舉個例子,多個執行緒通過同一個ThreadLocal獲取到的東西是不一樣的,就算有的時候出現的結果是一樣的(偶然性,兩個執行緒裡分別存了兩份相同的東西),但他們獲取的本質是不同的。

那為什麼有這種區別呢?為什麼要這樣設計呢?

先來研究一下為什麼會出現這個結果。

在ThreadLocal中存在著兩個很重要的方法,get和set方法,一個讀取一個設定。

 /**
     * Returns the value of this variable for the current thread. If an entry
     * doesn't yet exist for this variable on this thread, this method will
     * create an entry, populating the value with the result of
     * {@link #initialValue()}.
     *
     * @return the current value of the variable for the calling thread.
     */
    @SuppressWarnings("unchecked")
    public T get() {
        // Optimized for the fast path.
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values != null) {
            Object[] table = values.table;
            int index = hash & values.mask;
            if (this.reference == table[index]) {
                return (T) table[index + 1];
            }
        } else {
            values = initializeValues(currentThread);
        }

        return (T) values.getAfterMiss(this);
    }

 /**
     * Sets the value of this variable for the current thread. If set to
     * {@code null}, the value will be set to null and the underlying entry will
     * still be present.
     *
     * @param value the new value of the variable for the caller thread.
     */
    public void set(T value) {
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values == null) {
            values = initializeValues(currentThread);
        }
        values.put(this, value);
    }

摘自原始碼

首先研究它的get方法吧,從註釋上可以看出,get方法會返回一個當前執行緒的變數值,如果陣列不存在就會建立一個新的。
這裡有幾個很重要的詞,就是“當前執行緒”和“陣列”。

這裡提到的陣列對於每個執行緒來說都是不同的,values.table,而values是通過當前執行緒獲取到的一個Values物件,因此這個陣列是每個執行緒唯一的,不能共用,而下面的幾句話也更直接了,獲取一個索引,再返回通過這個索引找到陣列中對應的值。這也就解釋了為什麼多個執行緒通過同一個ThreadLocal返回的是不同的東西。

那這裡為什麼要這麼設定呢?翻了一下書,搜了一下資料:

  • ThreadLocal在日常開發中使用到的地方較少,但是在某些特殊的場景下,通過ThreadLocal可以輕鬆實現一些看起來很複雜的功能。一般來說,當某些資料是以執行緒為作用域並且不同執行緒具有不同的資料副本的時候,就可以考慮使用ThreadLocal。例如在Handler和Looper中。對於Handler來說,它需要獲取當前執行緒的Looper,很顯然Looper的作用域就是執行緒並且不同的執行緒具有不同的Looper,這個時候通過ThreadLocal就可以輕鬆的實現Looper線上程中的存取。如果不採用ThreadLocal,那麼系統就必須提供一個全域性的雜湊表供Handler查詢指定的Looper,這樣就比較麻煩了,還需要一個管理類。
  • ThreadLocal的另一個使用場景是複雜邏輯下的物件傳遞,比如監聽器的傳遞,有些時候一個執行緒中的任務過於複雜,就可能表現為函式呼叫棧比較深以及程式碼入口的多樣性,這種情況下,我們又需要監聽器能夠貫穿整個執行緒的執行過程。這個時候就可以使用到ThreadLocal,通過ThreadLocal可以讓監聽器作為執行緒內的全域性物件存在,線上程內通過get方法就可以獲取到監聽器。如果不採用的話,可以使用引數傳遞,但是這種方式在設計上不是特別好,當呼叫棧很深的時候,通過引數來傳遞監聽器這個設計太糟糕。而另外一種方式就是使用static靜態變數的方式,但是這種方式存在一定的侷限性,擴充性並不是特別的強。比如有10個執行緒在執行,就需要提供10個監聽器物件。

訊息機制

上面提到了Handler/Looper/Message Queue,它們實際上是一個整體,只不過我們在開發中接觸更多的是Handler而已,Handler的主要作用是將一個任務切換到某個指定的執行緒中去執行,而Android之所以提供這個機制是因為Android規定UI只能在主執行緒中程式,如果在子執行緒中訪問UI就會丟擲異常。

為什麼Android不允許在子執行緒訪問UI

其實這一點不僅僅是對於Android,對於其他的所有圖形介面現在都採用的是單執行緒模式。

因為對於一個多執行緒來說,如果子執行緒更改了UI,那麼它的相關操作就必須對其他子執行緒可見,也就是Java併發中很重要的一個概念,執行緒可見性,Happen-before原則【下篇部落格總結一下自己對Java併發的理解吧,挺重要的,總結完後再把傳送門貼過來】而一般來說,對於這種併發訪問,一般都是採用加鎖的機制,但是加鎖的機制存在很明顯的問題:讓UI訪問間的邏輯變得複雜,同時效率也會降低。甚至有的時候還會造成死鎖的情況,這個時候就麻煩了。

而至於究竟能不能夠實現這種UI介面的多執行緒呢?SUN公司的某個大牛(忘了是誰,很久之前看的,好像是前副總裁)說:“行肯定是沒問題,但是非常考技術,因為必須要考慮到很多種情況,這個時候就需要技術專家來設計。而這種設計出來的東西對於廣大普通程式設計師來說又是異常頭疼的,就算是實現了多執行緒,普通人用起來也是怨聲載道的。所以建議還是單執行緒”。

死鎖

順帶著BB一下死鎖。

死鎖的四個必要條件

  1. 互斥條件:資源不能被共享,只能被同一個程式使用
  2. 請求與保持條件:已經得到資源的程式可以申請新的資源
  3. 非剝奪條件:已經分配的資源不能從相應的程式中被強制剝奪
  4. 迴圈等待條件:系統中若干程式組成環路,該環路中每個程式都在等待相鄰程式佔用的資源

舉個常見的死鎖例子:程式A中包含資源A,程式B中包含資源B,A的下一步需要資源B,B的下一步需要資源A,所以它們就互相等待對方佔有的資源釋放,所以也就產生了一個迴圈等待死鎖。

處理死鎖的方法

  1. 忽略該問題,也就是鴕鳥演算法。當發生了什麼問題時,不管他,直接跳過,無視它。
  2. 檢測死鎖並恢復
  3. 資源進行動態分配
  4. 破除上面的四種死鎖條件之一

繼續訊息機制

MessageQueue主要包含兩個操作:插入和讀取,讀取操作本身會伴隨著刪除操作,插入和讀取對應的方法分別為enqueueMessage和next,其中enqueueMessage的作用是往訊息佇列中插入一條訊息,而next的作用是從訊息佇列中取出一條訊息並將其從訊息佇列中移除。這也就是為什麼使用的是一個單連結串列的資料結構來維護訊息列表,因為它在插入和刪除上比較有優勢(把下一個連線的點切換一下就完成了)。

而對於MessageQueue的插入操作來說,沒什麼可以看的,也就這樣吧,主要需要注意的是它的讀取方法next。

 Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

原始碼有點長,總結一下就是:

next方法它是一個死迴圈,如果訊息佇列中沒有訊息,那麼next方法就會一直阻塞在這裡,當有新的訊息來的時候,next方法就會返回這條資訊並將其從單連結串列中移除。

而這個時候勒Looper就等著的,它也是一直迴圈迴圈,不停地從MessageQueue中檢視是否有新訊息,如果有新訊息就會立刻處理,否則就會一直阻塞在那裡。而對於Looper來說,它是隻能建立一個的,這個要歸功與它的prepare方法。

 /** Initialize the current thread as a looper.
      * This gives you a chance to create handlers that then reference
      * this looper, before actually starting the loop. Be sure to call
      * {@link #loop()} after calling this method, and end it by calling
      * {@link #quit()}.
      */
    public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

從這裡我們就可以看出該prepare方法會首先檢測是否已經存在looper了,如果不存在,就建立一個新的;如果存在,就丟擲異常。
而之後使用Looper.loop()就可以開啟訊息迴圈了。

  /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            msg.target.dispatchMessage(msg);

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }

從這裡面我們可以看到它也是個死迴圈,會不停的呼叫queue.next()方法來獲取資訊,如果沒有,就return,如果有就處理。

注意

當然了,這裡有一個很重要的點,一般可能會忘,那就是在子執行緒中如果手動為其建立了Looper,那麼在所有的事情完成以後應該呼叫quit方法來終止訊息迴圈,否則這個子執行緒就會一直處於等待狀態,而如果退出Looper之後,這個執行緒就會立刻終止,所以建議不需要使用的時候終止Looper。

Handler

上面總結了Looper和MessageQueue,這裡就對Handler進行一個總結吧。它的工作主要包含訊息的傳送和接受過程,訊息的傳送可以通過post的一系列方法以及send的一系列方法來實現,post的一系列方法最終是通過send的一系列方法來實現的。

實際上它傳送訊息的過程僅僅是向訊息佇列中插入了一條訊息,MessageQueue的next方法就會返回這條訊息給Looper,Looper在收到訊息之後就會開始處理了。最後由Looper交給Handler處理(handleMessage()方法)。

IPC通訊

上面總結完了Android的訊息處理機制,那麼就順帶總結一下IPC通訊吧,畢竟上面提到過那麼多次Binder和Socket。

資料:為什麼Android要採用Binder作為IPC機制?

知乎上面的回答相當的好,這個博主對系統底層也是頗有鑽研,學習。

這裡就結合上面的知乎回答以及加上《Linux程式設計》還有一本Linux核心剖析(書名忘了但是講得真的非常好),摻雜一些個人的理解。

程式的定義

UNIX標準把程式定義為:“一個其中執行著一個或多個程式的地址控制元件和這些執行緒所需要的系統資源”。目前,可以簡單的把程式看做正在執行的程式。

程式都會被分配一個唯一的數字編號,我們成為PID(也就是程式識別符號),它通常是一個取值範圍從2到32768的正整數。當程式被啟動時,系統將按順序選擇下一個未被使用的數字作為PID,當數字已經迴繞一圈時,新的PID重新從2開始,數字1一般是為init保留的。在程式中,存在一個自己的棧空間,用於儲存函式中的區域性變數和控制函式的呼叫與返回。程式還有自己的環境空間,包含專門為這個程式建立的環境變數,同時還必須要維護自己的程式計數器,這個計數器用來記錄它執行到的位置,即在執行執行緒中的位置。

在Linux中可以通過system函式來啟動一個程式

守護程式

這裡就需要提到一個守護程式了,這個在所有的底層中經常都會被提到。

在linux或者unix作業系統中在系統引導的時候會開啟很多服務,這些服務就叫做守護程式。為了增加靈活性,root可以選擇系統開啟的模式,這些模式叫做執行級別,每一種執行級別以一定的方式配置系統。 守護程式是脫離於終端並且在後臺執行的程式。守護程式脫離於終端是為了避免程式在執行過程中的資訊在任何終端上顯示並且程式也不會被任何終端所產生的終端資訊所打斷。

守護程式常常在系統引導裝入時啟動,在系統關閉時終止。如果想要某個程式不因為使用者或終端或其他的變化而受到影響,那麼就必須把這個程式變成一個守護程式

防止手機服務後臺被殺死

是不是在手機的設定介面看當前正在執行的服務時會發現有的APP不止存在一個服務?有的APP後臺存在兩個,有的存在三個?有的流氓軟體也會這麼設定,這樣的話就可以一直執行在後臺,使用者你關也關不了(倒不是說所有這麼設定的都是流氓軟體,因為有的軟體需要保持一個長期的後臺線上,這是由功能決定的)。

這裡有兩種方法(可能還有更多,這裡只總結我瞭解的):

  • 第一種方法就是利用android中service的特性來設定,防止手機服務後臺被殺死。通過更改onStartCommand方法的返回值,將service設定為粘性service,那麼當service被kill的時候就會將服務的狀態返回到最開始啟動的狀態,也就是執行的狀態,所以這個時候也就會再次重新執行。但是需要注意一點,這個時候的intent值就為空了,獲取的話需要注意一下這一點。
  • 第二種就是fork出一個C的程式,因為在Linux中,子類程式在父類被殺死銷燬的時候不會隨之殺死,它會被init程式領養。所以也就可以使用這一個方法,利用主程式fork出一個C程式在後臺執行,一旦檢測到服務被殺死(檢測的方式多種,可使用觀察者模式,廣播,輪詢等等),就重啟服務即可

IPC通訊

上面總結了程式的相關基礎,這裡就開始總結一下程式間通訊(IPC )的問題了。

現在Linux現有的所有IPC方式:

  1. 管道:在建立時分配一個page大小的記憶體,快取區大小有限
  2. 訊息佇列:資訊複製兩次,額外的cpu消耗,不適合頻繁或資訊量大的通訊
  3. 共享記憶體:無需複製,共享緩衝區直接附加到程式虛擬地址控制元件,速度是在所有IPC通訊中最快的。但是程式間的同步問題作業系統無法實現,必須由各程式利用同步工具解決。
  4. Socket:作為更通用的介面,傳輸效率低,主要用於不通機器或跨網路的通訊
  5. 訊號量:常作為一種鎖機制。
  6. 訊號:不適用於資訊交換,更適用於程式件中斷控制,例如kill process

到了這裡,就有了問題,為什麼在Linux已經存在這麼多優良的IPC方案時,Android還要採取一種新的Binder機制呢?

猜測:我覺得Android採用這種新的方式(當然也大面積的同時使用Linux的IPC通訊方式),最多兩個原因:

  1. 推廣時手機廠商自定義ROM底層的保密性或者公司之間的關係。
  2. 在某些情況下更適合手機這種低配置,對效率要求極高,使用者體驗極其重要的裝置

資料

對於Binder來說,存在著以下的優勢:

  • 效能角度:Binder的資料拷貝只需要一次,而管道、訊息佇列、Socket都需要2次,而共享記憶體是一次都不需要拷貝,因此Binder的效能僅次於共享記憶體
  • 穩定性來說:Binder是基於C/S架構的,也就是Client和Server組成的架構,Client端有什麼需求,直接傳送給Server端去完成,架構清晰,分工明確。而共享記憶體的實現方式複雜,需要充分考慮訪問臨界資源的併發同步問題,否則可能會出現死鎖等問題。從穩定性來說,Binder的架構優於共享記憶體。
  • 從安全的角度:Linux的傳統IPC方式的接收方無法獲得對方程式可靠的UID(使用者身份證明)/PID(程式身份證明),從而無法鑑別對方身份,而Android是一個對安全效能要求特別高的作業系統,在系統層面需要對每一個APP的許可權進行管控或者監視,對於普通使用者來說,絕對不希望從App商店下載偷窺隱射資料、後臺造成手機耗電等問題。傳統的Linux IPC無任何保護措施,完全由上層協議來確保。而在Android中,作業系統為每個安裝好的應用程式分配了自己的UID,通過這個UID可以鑑別程式身份。同時Android系統對外只暴露Client端,Client端將任務傳送給Server端,Server端會根據許可權控制策略判斷UID/PID是否滿足訪問許可權。也就是說Binder機制對於通訊雙方的身份是核心進行校驗支援的。例如Socket方式只需要指導地址就可以連線,他們的安全機制需要上層協議來假設
  • 從語言角度:Linux是基於C的,而Android是基於Java的,而Binder是符合物件導向思想的。它的實體位於一個程式中,而它的引用遍佈與系統的各個程式之中,它是一個跨程式引用的物件,模糊了程式邊界,淡化了程式通訊的過程,整個系統彷彿執行於同一個物件導向的程式之中。
  • 從公司角度:Linux核心是開源的,GPL協議保護,受它保護的Linux Kernel是執行在核心控制元件,對於上層的任何類庫、服務等只要進行系統呼叫,呼叫到底層Kernel,那麼也必須遵循GPL協議。而對於Android來說,Google巧妙地將GPL協議控制在核心控制元件,將使用者控制元件的協議採用Apache-2.0協議(允許基於Android的開發商不向社群反饋原始碼)。

反射

剛才談到Binder的時候提了一下效率的問題,那這裡就不得不講到反射了。

反射它允許一個類在執行過程中獲得任意類的任意方法,這個是Java語言的一個很重要的特性。它方便了程式設計師的編寫,但是降低了效率。

實際上,對於只要不是特別大的專案(非Android),反射對於效率的影響微乎其微,而與之對比的開發成本來說就更划算了。
但是,Android是一個用於手機的,它的硬體設施有限,我們必須要考慮到它的這個因素,使用者體驗是最重要的。以前看到過國外的一項統計。在一個APP中的Splash中使用了反射,結果執行時間增加了一秒,這個已經算是很嚴重的效率影響了。

為什麼反射影響效率呢

這裡就需要提到一個東西,JIT編譯器。JIT編譯器它可以把位元組碼檔案轉換為機器碼,這個是可以直接讓處理器使用的,經過它處理的位元組碼效率提升非常大,但是它有一個缺點,就是把位元組碼轉換成機器碼的過程很慢,有的時候甚至還超過了不轉換的程式碼效率(轉換之後存在一個複用的問題,對於轉換了的機器碼,使用的次數越多就越值的)。因此,在JVM虛擬機器中,也就產生了一個機制,把常用的、使用頻率高的位元組碼通過JIT編譯器轉換,而頻率低的就不管它。而反射的話則是直接越過了JIT編譯器,不管是常用的還是非常用的位元組碼一律沒有經過JIT編譯器的轉化,所以效率就會低。

而在Android裡面,5.0之前使用的是Davlik虛擬機器,它就是上面的機制,而在Android5.0之後Google使用了一個全新的ART虛擬機器全面代替Davlik虛擬機器。

ART虛擬機器會在程式安裝時直接把所有的位元組碼全部轉化為機器碼,雖然這樣會導致安裝時間邊長,但是程式執行的效率提升非常大。
【疑問:那在Android5.0之後的系統上,反射會不會沒影響了?由於現在做專案的時候更多考慮的是向下相容,單獨考慮5.0的情況還沒有,等以後有需求或者是有機會的時候再深入瞭解一下,以後更新】

繼續斷點4

剛才總結了Android的訊息處理機制和IPC通訊,那麼我們主執行緒的訊息處理機制是什麼時候開始的呢?因為我們知道在主執行緒中我們是不需要手動呼叫Looper.prepare()和Looper.loop()的。

Android的主執行緒就是ActivityThread,主執行緒的入口方法是main方法,在main方法中系統會通過Looper.prepareMainLooper()來建立主執行緒的Looper以及MessageQueue,並通過Looper.loop來開啟訊息迴圈,所以這一步實際上是系統已經為我們做了,我們就不再需要自己來做。

ActivityThread通過AppplicationThread和AMS進行程式件通訊,AMS以程式間通訊的方式完成ActivityThread的請求後會回撥ApplicationThread中的Binder方法,然後ApplicationThread會向Handler傳送訊息,Handler收到訊息後會將ApplicationThread中的邏輯切換到主執行緒中去執行,這個過程就是主執行緒的訊息迴圈模型。

上面總結到了APP開始執行,依次呼叫onCreate/onStart/onResume等方法,那麼在onCreate方法中我們經常使用的setContentView和findViewById做了什麼事呢?

Activity介面顯示

首先,就考慮到第一個問題,也就是setContentView這個東西做了什麼事,這裡就要對你當前繼承的Activity分類了,如果是繼承的Activity,那麼setContentView原始碼是這樣的:

    /**
     * Set the activity content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the activity.
     *
     * @param layoutResID Resource ID to be inflated.
     *
     * @see #setContentView(android.view.View)
     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
     */
    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

    /**
     * Set the activity content to an explicit view.  This view is placed
     * directly into the activity's view hierarchy.  It can itself be a complex
     * view hierarchy.  When calling this method, the layout parameters of the
     * specified view are ignored.  Both the width and the height of the view are
     * set by default to {@link ViewGroup.LayoutParams#MATCH_PARENT}. To use
     * your own layout parameters, invoke
     * {@link #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)}
     * instead.
     *
     * @param view The desired content to display.
     *
     * @see #setContentView(int)
     * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
     */
    public void setContentView(View view) {
        getWindow().setContentView(view);
        initWindowDecorActionBar();
    }

    /**
     * Set the activity content to an explicit view.  This view is placed
     * directly into the activity's view hierarchy.  It can itself be a complex
     * view hierarchy.
     *
     * @param view The desired content to display.
     * @param params Layout parameters for the view.
     *
     * @see #setContentView(android.view.View)
     * @see #setContentView(int)
     */
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getWindow().setContentView(view, params);
        initWindowDecorActionBar();
    }

這裡面存在著3個過載函式,而不管你呼叫哪一個,最後都會呼叫到initWindowDecorActionBar()這個方法。

而對於新的一個AppcompatActivity,這個Activity裡面包含了一些新特性,現在我做的專案裡基本都是使用AppcompatActivity代替掉原來的Activity,當然也並不是一定的,還是要根據專案的實際情況來選擇。

在AppcompatActivity中,setContentView是這樣的:

 @Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }

    @Override
    public void setContentView(View view) {
        getDelegate().setContentView(view);
    }

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getDelegate().setContentView(view, params);
    }

一樣的3個過載函式,只是裡面沒有了上面的那個init方法,取而代之的是一個getDelegate().setContentView,這個delegate從字面上可以瞭解到它是一個委託的物件,原始碼是這樣的:

 /**
     * @return The {@link AppCompatDelegate} being used by this Activity.
     */
    @NonNull
    public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }

而在AppCompatDelegate.Create方法中,則會返回一個很有意思的東西:

/**
     * Create a {@link android.support.v7.app.AppCompatDelegate} to use with {@code activity}.
     *
     * @param callback An optional callback for AppCompat specific events
     */
    public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
        return create(activity, activity.getWindow(), callback);
    }

private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        final int sdk = Build.VERSION.SDK_INT;
        if (sdk >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (sdk >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else if (sdk >= 11) {
            return new AppCompatDelegateImplV11(context, window, callback);
        } else {
            return new AppCompatDelegateImplV7(context, window, callback);
        }
    }

這裡會根據SDK的等級來返回不同的東西,這樣的話就不深究了,底層的話我撇了一下,應該原理和Activity是一樣的,可能存在一些區別。這裡就用Activity來談談它的setContentView方法做了什麼事。

在setContentView上面有段註釋:

Set the activity content from a layout resource. The resource will be inflated, adding all top-level views to the activity.

這裡就介紹了它的功能,它會按照一個佈局資源去設定Activity的內容,而這個佈局資源將會被引入然後新增所有頂級的Views到這個Activity當中。

這是個啥意思勒。

下面從網上扒了一張圖:

這裡寫圖片描述

這裡是整個Activity的層級,最外面一層是我們的Activity,它包含裡面的所有東西。

再上一層是一個PhoneWindow,這個PhoneWindow是由Window類派生出來的,每一個PhoneWindow中都含有一個DecorView物件,Window是一個抽象類。

再上面一層就是一個DecorView,我理解這個DecorView就是一個ViewGroup,就是裝View的。

而在DecoreView中,最上面的View就是我們的TitleActionBar,下面就是我們要設定的content。所以在上面的initWindowDecorActionBar就能猜到是什麼意思了吧。

而在initWindowDecorActionBar方法中,有一段程式碼:

 /**
     * Creates a new ActionBar, locates the inflated ActionBarView,
     * initializes the ActionBar with the view, and sets mActionBar.
     */
    private void initWindowDecorActionBar() {
        Window window = getWindow();

        // Initializing the window decor can change window feature flags.
        // Make sure that we have the correct set before performing the test below.
        window.getDecorView();

        if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) {
            return;
        }

        mActionBar = new WindowDecorActionBar(this);
        mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);

        mWindow.setDefaultIcon(mActivityInfo.getIconResource());
        mWindow.setDefaultLogo(mActivityInfo.getLogoResource());
    }

注意上面的window.getDecoreView()方法的註釋,該方法會設定一些window的標誌位,而當這個方法執行完之後,就再也不能更改了,這也就是為什麼很多第三方SDK設定window的標誌位時一定要求要在setContentView方法前呼叫。

findViewById

我們通過一個findViewById方法可以實現物件的繫結,那它底層究竟是怎麼實現的呢?

findViewById根據繼承的Activity型別的不同也存在著區別,老規矩,還是以Activity的來。

/**
     * Finds a view that was identified by the id attribute from the XML that
     * was processed in {@link #onCreate}.
     *
     * @return The view if found or null otherwise.
     */
    @Nullable
    public View findViewById(@IdRes int id) {
        return getWindow().findViewById(id);
    }

從原始碼來看,findViewById也是經過了一層層的呼叫,它的功能如同它上面的註釋一樣,通過一個view的id屬性查詢view,這裡也可以看到一個熟悉的getWindow方法,說明findViewById()實際上Activity把它也是交給了自己的window來做

/**
     * Finds a view that was identified by the id attribute from the XML that
     * was processed in {@link android.app.Activity#onCreate}.  This will
     * implicitly call {@link #getDecorView} for you, with all of the
     * associated side-effects.
     *
     * @return The view if found or null otherwise.
     */
    @Nullable
    public View findViewById(@IdRes int id) {
        return getDecorView().findViewById(id);
    }

而在這裡面,又呼叫了getDecorView的findViewById()方法,這也相當於是一個層層傳遞的過程,因為DecorView我理解為就是一個ViewGroup,而當執行getDecorView().findViewById()方法時,就會執行View裡面的findViewById方法。它會使用這個被給予的id匹配子View的Id,如果匹配,就返回這個View,完成View的繫結

/**
     * Look for a child view with the given id.  If this view has the given
     * id, return this view.
     *
     * @param id The id to search for.
     * @return The view that has the given id in the hierarchy or null
     */
    @Nullable
    public final View findViewById(@IdRes int id) {
        if (id < 0) {
            return null;
        }
        return findViewTraversal(id);
    }

    /**
     * {@hide}
     * @param id the id of the view to be found
     * @return the view of the specified id, null if cannot be found
     */
    protected View findViewTraversal(@IdRes int id) {
        if (id == mID) {
            return this;
        }
        return null;
    }

最後總結一下(Activity中),findViewById的過程是這樣的:

Activity -> Window -> DecorView -> View

相關文章