Android面試題集錦(一)
2016.7.19開更...........................................................................
(1):事件分發機制概述
首先應該搞清楚兩個問題:事件分發機制分發的是什麼?怎麼進行分發?
分發的是MotionEvent事件了,因而我們討論的問題就成了當MotionEvent事件生成之後,事件是怎麼傳遞到某一個View控制元件上面並且得到處理的過程;
android事件產生後的傳遞過程是從Activity--->Window--->View的,即隧道式傳遞,而View又分為不包含子View的View以及包含子View的ViewGroup,事件產生之後首先傳遞到Activity上面,而Activity接著會傳遞到PhoneWindow上,PhoneWindow會傳遞給RootView,而RootView其實就是DecorView了,接下來便是從DecorView到View上的分發過程了,具體就可以分成ViewGroup和View的分發兩種情況了;
對於ViewGroup而言,當事件分發到當前ViewGroup上面的時候,首先會呼叫他的dispatchTouchEvent方法,在dispatchTouchEvent方法裡面會呼叫onInterceptTouchEvent來判斷是否要攔截當前事件,如果要攔截的話,就會呼叫ViewGroup自己的onTouchEvent方法了,如果onInterceptTouchEvent返回false的話表示不攔截當前事件,那麼事件將會繼續往當前ViewGroup的子View上面傳遞了,如果他的子View是ViewGroup的話,則重複ViewGroup事件分發過程,如果子View就是View的話,則轉到下面的View分發過程;
對於View而言,事件傳遞過來首先當然也是執行他的dispatchTouchEvent方法了,如果我們為當前View設定了onTouchListener監聽器的話,首先就會執行他的回撥方法onTouch了,這個方法的返回值將決定事件是否要繼續傳遞下去了,如果返回false的話,表示事件沒有被消費,還會繼續傳遞下去,如果返回true的話,表示事件已經被消費了,不再需要向下傳遞了;如果返回false,那麼將會執行當前View的onTouchEvent方法,如果我們為當前View設定了onLongClickListener監聽器的話,則首先會執行他的回撥方法onLongClick,和onTouch方法類似,如果該方法返回true表示事件被消費,不會繼續向下傳遞,返回false的話,事件會繼續向下傳遞,為了分析,我們假定返回false,如果我們設定了onClickListener監聽器的話,則會執行他的回撥方法onClick,該方法是沒有返回值的,所以也是我們事件分發機制中最後執行的方法了;可以注意到的一點就是隻要你的當前View是clickable或者longclickable的,View的onTouchEvent方法預設都會返回true,也就是說對於事件傳遞到View上來說,系統預設是由View來消費事件的,但是ViewGroup就不是這樣了;
上面的事件分發過程只是正常情況下的,如果有這樣一種情況,比如事件傳遞到最裡層的View之後,呼叫該View的oonTouchEvent方法返回了false,那麼這時候事件將通過冒泡式的方式向他的父View傳遞,呼叫它父View的onTouchEvent方法,如果正好他的父View的onTouchEvent方法也返回false的話,這個時候事件最終將會傳遞到Activity的onTouchEvent方法了,也就是最終就只能由Activity自己來處理了;
事件分發機制需要注意的幾點:
(1):如果說除Activity之外的View都沒有消費掉DOWN事件的話,那麼事件將不再會傳遞到Activity裡面的子View了,將直接由Activity自己呼叫自己的onTouchEvent方法來處理了;
(2):一旦一個ViewGroup決定攔截事件,那麼這個事件序列剩餘的部分將不再會由該ViewGroup的子View去處理了,即事件將在此ViewGroup層停止向下傳遞,同時隨後的事件序列將不再會呼叫onInterceptTouchEvent方法了;
(3):如果一個View開始處理事件但是沒有消費掉DOWN事件,那麼這個事件序列隨後的事件將不再由該View來處理,通俗點講就是你自己沒能力就別瞎BB,要不以後的事件就都不給你了;
(4):View的onTouchEvent方法是否執行是和他的onTouchListener回撥方法onTouch的返回值息息相關的,onTouch返回false,onTouchEvent方法不執行;onTouch返回false,onTouchEvent方法執行,因為onTouchEvent裡面會執行onClick,所以造成了onClick是否執行和onTouch的返回值有了關係;
(2):View檢視繪製過程原理
View檢視繪製需要搞清楚兩個問題,一個是從哪裡開始繪製,一個是怎麼繪製?
先說從哪裡開始繪製的問題:我們平常在使用Activity的時候,都會呼叫setContentView來設定佈局檔案,沒錯,檢視繪製就是從這個方法開始的;
再來說說怎麼繪製的:
在我們的Activity中呼叫了setContentView之後,會轉而執行PhoneWindow的setContentView,在這個方法裡面會判斷我們存放內容的ViewGroup(這個ViewGroup可以是DecorView也可以是DecorView的子View)是否存在。不存在的話則會建立一個DecorView出來,並且會創建出相應的窗體風格,存在的話則會刪除原先ViewGroup上面已有的View,接著會呼叫LayoutInflater的inflate方法以pull解析的方式將當前佈局檔案中存在的View通過addView的方式新增到ViewGroup上面來,接著在addView方法裡面就會執行我們常見的invalidate方法了,這個方法不只是在View檢視繪製的過程中經常用到,其實動畫的實現原理也是不斷的呼叫這個方法來實現檢視不斷重繪的,執行這個方法的時候會呼叫他的父View的invalidateChild方法,這個方法是屬於ViewParent的,ViewGroup以及ViewRootImpl中都對他進行了實現,invalidateChild裡面主要做的事就是通過do while迴圈一層一層計算出當前View的四個點所對應的矩陣在ViewRoot中所對應的位置,那麼有了這個矩陣的位置之後最終都會執行到ViewRootImpl的invalidateChildInParent方法,執行這個方法的時候首先會檢查當前執行緒是不是主執行緒,因為我們要開始準備更新UI了,不是主執行緒的話是不允許更新UI的,接著就會執行scheduleTraversals方法了,這個方法會通過handler來執行doTraversal方法,在這個方法裡面就見到了我們平常所熟悉的View檢視繪製的起點方法performTraversals了;
那麼接下來就是真正的檢視繪製流程了,大體上講View的繪製經歷了Measure測量、Layout佈局以及Draw繪製三個過程,具體來講是從ViewRootImpl的performTraversals方法開始,首先執行的將是performMeasure方法,這個方法裡面會傳入兩個MeasureSpec型別的引數,他在很大程度上決定了View的尺寸規格,對於DecorView來說寬高的MeasureSpec值的獲取與視窗尺寸以及自身的LayoutParams有關,對於普通View來說其寬高的MeasureSpec值的獲取由父容器以及自身的LayoutParams屬性共同決定,在performMeasure裡面會執行measure方法,在measure方法裡面會執行onMeasure方法,到這裡Measure測量過程對View與ViewGroup來說是沒有區別的,但是從onMeasure開始兩者有差別了,因為View本身已經不存在子View了,所以他onMeasure方法將執行setMeasuredDimension方法,該方法會設定View的測量值,但是對於ViewGroup來說,因為它裡面還存在著子View,那麼我們就需要繼續測量它裡面的子View了,呼叫的方法是measureChild方法,該方法內部又會執行measure方法,而measure方法轉而又會執行onMeasure方法,這樣不斷的遞迴進行下去,知道整個View樹測量結束,這樣performMeasure方法執行結束了;接著便是執行performLayout方法了,performMeasure只是測量出View樹中View的大小了,但是還不知道View的位置,所以也就出現了performLayout方法了,performLayout方法首先會執行layout方法,以確定View自身的位置,如果當前View是ViewGroup的話,則會執行onLayout方法。在onLayout方法裡面又會遞迴的執行layout方法,直到當前遍歷到的View不再是ViewGroup為止,這樣整個layout佈局過程就結束了;在View樹中View的大小以及位置都確定之後,接下來就是真正的繪製View顯示在介面的過程了,該過程首先從performDraw方法開始,performDraw方法首先執行draw方法,在draw方法中首先繪製背景、接著呼叫onDraw方法繪製自己,如果當前View是ViewGroup的話,還要呼叫dispatchDraw方法繪製當前ViewGroup的子View,而dispatchDraw方法裡面實際上是通過drawChild方法間接呼叫draw方法形成遞迴繪製整個View樹,直到當前View不再是ViewGroup為止,這樣整個View的繪製過程就結束了;
(3):解決滑動衝突的方式
在自定義View的過程經常會遇到滑動衝突問題,一般滑動衝突的型別有三種:(1)外部View滑動方向和內部View滑動方向不一致;(2)外部View滑動方向和內部View滑動方向一致;(3)上述兩種情況的巢狀;
一般我們解決滑動衝突都是利用的事件分發機制,有兩種方式外部攔截法和內部攔截法:
外部攔截法:實現思路是事件首先是通過父容器的攔截處理,如果父容器不需要該事件的話,則不攔截,將事件傳遞到子View上面,如果父容器決定攔截的話,則在父容器的onTouchEvent裡面直接處理該事件,這種方法符合事件分發機制;具體實現措施是修改父容器的onInterceptTouchEvent方法,在達到某一條件的時候,讓該方法直接返回true就可以把事件攔截下來進而呼叫自己的onTouchEvent方法來處理了,但是有一點需要注意的是如果想要讓子View能夠收到事件,我們需要在onInterceptTouchEvent方法裡面判斷如果是DOWN事件的話,返回false,這樣後續的MOVE以及UP事件才有機會傳遞到子View上面,如果你直接在onInterceptTouchEvent方法裡面DOWN情況下返回了true,那麼後續的MOVE以及UP事件將由當前View的onTouchEvent處理了,這樣你的攔截將根本沒有意義的,攔截只是在滿足一定條件才會攔截,並不是所有情況下都攔截;
內部攔截法:實現思路是事件從父容器傳遞到子View上面,父容器不做任何干預性的措施,所有的事件都會傳遞到子View上面,如果子元素需要改事件,那麼就由子元素消耗掉了,該事件也就不會回傳了,如果子元素不需要該事件,那麼他就會回傳給父容器來處理了;具體實現措施需要藉助於requestDisallowInterceptTouchEvent方法,該方法用來告訴父容器要不要攔截當前事件,為了配合子View能夠呼叫這個方法成功,父容器必須預設能夠攔截除了DOWN事件以外的事件,為什麼要除了DOWN事件以外呢?因為如果一旦父容器攔截了DOWN事件,那麼後續事件將不再會傳遞到子元素了,內部攔截法也就失去作用了;
個人認為外部攔截法是符合正常邏輯的,按照事件隧道式分發過程,如果父容器需要就直接攔截,不需要則傳遞到子View;內部攔截法相當於人為干預分發這個過程,我會保證事件先都到子View上面,至於子View需不需要就要看我自己了,如果我不需要就回傳給父容器了,需要的話自己就消耗掉了;感覺這兩種方式只是父容器和子View處理事件的優先順序不同而已;
(4):Android動畫原理
Android動畫可以分為View動畫、幀動畫、屬性動畫,其中View動畫又可以分為平移(Translate)、縮放(Scale)、旋轉(Rotate)、透明度(Alpha)四種,幀動畫可以認為是View動畫的一種,實現原理類似於放電影,通過一幀一幀的圖片進行播放來達到動畫的效果,正是因為這點需要注意他可能會出現OOM異常,屬性動畫是3.0之後出現的,他也可以實現View動畫的效果;
在講解動畫原理之前需要明白兩個概念,插值器和估值器:
插值器:根據時間流逝的百分比來計算出屬性值改變的百分比,對應的介面是Interpolator;
估值器:根據屬性改變的百分比計算出屬性的改變值,對應的介面是TypeEvaluator;
先來說說View動畫實現原理,其實如果你看View動畫實現過程的原始碼的話就會發現,View動畫其實就是在不斷的呼叫View的invalidate方法來進行View的繪製以達到動畫的效果的,所以理解View動畫的核心其實應該是首先理解View的繪製過程;
我們使用View動畫都是通過View的startAnimation方法開始的,那麼分析View動畫原理自然應該從這個方法開始了,這個方法裡面會呼叫setAnimation設定當前View的動畫,並且隨後呼叫了我們經常見的invalidate方法,這個方法具體執行過程上面已經說過了,最後都會執行到ViewRootImpl的performTraversals方法,該方法就是我們進行檢視繪製經常見到的開始方法了,經過一系列的measure測量以及layout佈局過程執行到draw繪畫階段,這個階段是我們動畫比較關心的階段,畢竟要在介面顯示嘛,沒有draw怎麼做到,呼叫draw方法之後繪製流程是這樣的:首選繪製背景,接著繪製自己,隨後呼叫dispatchDraw繪製自己的孩子,在呼叫每個子View的draw方法之前,需要繪製的View的繪製位置是Canvas通過translate方法切換了,這點也看出來View動畫實際上一直在動的是畫布,而並不是View本身,最後還要繪製滾動條等修飾內容,這裡呼叫了dispatchDraw方法,但是View沒有實現這個方法,ViewGroup作為View的子類實現了這個方法,在ViewGroup的dispatchDraw方法中會執行drawChild方法來繪製當前ViewGroup的子View,drawChild方法實際上呼叫的就是View的draw方法了,這個draw方法是不同於前面ViewGroup繪製自己的draw方法,這個draw方法中有一個時間引數和畫布引數Canvas,具體的繪製就是通過這個畫布引數實現的,但是ChildView的畫布是由其ParentView提供的,ParentView會根據ChildView在其內部的佈局來調整Canvas,當子View呼叫,在該draw方法中會通過getAnimation獲取到我們設定到View上的動畫,接著便執行了drawAnimation方法來進行動畫繪製了,在drawAnimation方法裡面首先通過執行getChildTransformation方法獲得子View的Transformation值,那麼Transformation是什麼呢?它主要進行的是矩陣運算的,其中有兩個比較關鍵的屬性其中之一是Matrix用於儲存View的平移、縮放、旋轉資訊,還有一個alpha屬性,主要儲存的是View的透明度資訊的,接著就會執行getTransformation方法,把剛剛獲取的Transformation值以及當前時間作為引數傳入,在getTransformation方法裡面會通過當前時間計算出時間流逝的百分比,並且將該百分比作為引數呼叫插值器的getInterpolation方法,獲得時間流逝百分比對應的屬性改變的百分比,當然這裡你可以使用自己定義的插值器,有了屬性改變百分比之後我們就可以呼叫applyTransformation方法來進行具體的動畫實現了,當然如果你自己想要實現自己定義的動畫,可以重寫applyTransformation方法,這樣View動畫的第一幀就繪製好了,那麼後續的幀該怎麼繪製呢?如果你細心的話會發現getTransformation有一個boolean型別的返回值,沒錯就是靠這個返回值來進行後續幀繪製的,檢視getTransformation方法文件說明會發現返回真表示還有後續幀存在,具體判別方法當然就是通過比較當前時間是否超過動畫要求最遲時間了,返回true則會繼續執行invalidate方法,相當於又回到最開始處進行遞迴的繪製,返回false的話則動畫結束,這就是View動畫的執行過程了;
幀動畫因為可以理解為電影的放映過程,所以他的一幀一幀過程是我們自己提供的,因為系統本身只需要切換我們提供的資源圖片就可以了,沒有多大原理需要解釋;
屬性動畫實現原理:
既然名字上有屬性兩個字,那麼肯定是通過改變View的屬性來達到動畫效果的,這點和View動畫是有很大差別的,View動畫只是ParentView不斷的調整ChildView的畫布來實現動畫的,本質上View的屬性是沒有發生變化的,所以當你對移動到某個地方的View進行一些比如點選或者觸控操作的時候是根本不會執行當前移動過來的View的事件方法的,原因就在於你移動過去的只是原先View的影像而已,而屬性動畫就不一樣了,他是實實在在的改變View屬性真正的在移動的;屬性動畫要求動畫作用的物件必須提供想要改變屬性的set方法,如果你沒有傳遞初始值的話還需要提供該屬性的get方法,屬性動畫會根據你傳入的該屬性的初始值和最終值以動畫的效果(也就是計算出某一時刻屬性需要改變的值)多次通過反射呼叫set方法動態的改變作用物件的屬性值,隨著時間的推移,這個值將越來越接近設定的最終值,以達到動畫的效果;
Android動畫注意點:
在使用幀動畫的時候,因為動畫圖片資源是我們自己提供的,所以一定要注意可能會出現的OOM異常;
View動畫只是ParentView不斷調整ChildView的畫布實現的,只是對View的影響做動畫,而不是真正的改變View的狀態;
View動畫在移動之後,移動的位置處的View不能觸發事件,但是屬性動畫是可以的;
2016.7.20更新...........................................................................
(5):簡單說Activity生命週期
Activity常用到的生命週期方法包括:onCreate、onstart、onResume、onRestart、onPause、onStop、onDestroy七種;
另外還有兩個Activity被異常銷燬恢復的生命週期方法:onSaveInstanceState、onRestoreInstanceState
下面從不同環境條件下分析執行的生命週期方法:
(1):第一次啟動某一Activity
onCreate----->onstart----->onResume
(2):從當前Activity跳轉到某一Activity或者按下Home鍵
onPause----->onStop
(3):再次回到原來的Activity
onRestart----->onStart----->onResume
那麼將(2)和(3)連起來理解就有一個問題出現了,再次返回原先Activity是先執行原先Activity的onResume方法呢,還是先執行當前Activity的onPause方法呢?這個有點涉及到Activity棧的知識,你想想肯定是現在的Activity在棧頂了,那肯定是先執行當前Activity的onPause方法了,這樣他暫停之後才會執行棧內其他Activity的onResume方法了;
(4):在當前Activity介面按下Back鍵
onPause----->onStop----->onDestroy
(5):在當前Activity介面按下鎖屏鍵進行鎖屏操作
onPause----->onStop
(6):從鎖屏狀態返回到之前的Activity
onRestart----->onStart----->onResume
(7):在當前Activity窗體中以彈窗的形式顯示另一個Activity
只會執行當前Activity的onPause方法,並不會執行onStop方法,如果此時點選Back鍵退出彈窗Activity顯示出原先的Activity,則直接執行onResume方法,連onRestart與onStart方法都不執行;
(8):在當前Activty上通過按鈕點選的形式彈出一個AlertDialog窗體,發現根本不會對Activity生命週期有任何影響,說明一點,AlertDialog其實是附在Activity上面的;
(9):接下來說說onSaveInstanceState與onRestoreInstanceState,這兩個生命週期方法和onStop和onStart方法的執行順序是:
但是onSaveInstanceState和onPause之間是沒有先後關係的;
如果我們的Activity被異常關閉,比如你進行了橫豎屏切換或者當前Activity因為優先順序比較低被系統殺死,系統就會呼叫onSaveInstanceState進行Activity狀態的儲存,比如說Edittext裡面的值或者你ListView上面滑動到哪個Item資訊,當該Activity重新建立的時候就會呼叫onRestoreInstanceState方法恢復之前在onSaveInstanceState裡面的資料了,注意的是onSaveInstanceState方法僅僅會出現在Activity被異常關閉的情況下,正常情況下是不會執行這個方法的,也就是正常情況下我們通過onCreate建立Activity的時候,他的Bundle引數是null的;
關於Activity生命週期的一點總結:
onCreate和onDestroy是一對相對的方法,分別標誌Activity的建立和銷燬;
onStart和onStop是一對相對的方法,表示Activity是否可見;
onPause和onResume是一對相對的方法,表示Activity是否處於前臺;
在正常的銷燬Activity情況下是不會執行onSaveInstanceState的;
(6):橫豎屏切換對Activity生命週期的影響
正常情況下,如果不進行特殊設定的話,橫豎屏切換會導致Activity重新建立,也就是會重新執行onCreate方法,在之前Activity是異常銷燬的時候會執行他的onSaveInstanceState方法(正常銷燬的話該方法是不會執行的),該方法會儲存之前Activity已經有的一些資訊,比如EditText的內容啊,ListView滾動的位置啊等等,那麼這次呼叫onCreate的時候和普通的直接建立Activity呼叫onCreate方法是有區別的,直接建立的話onCreate方法的引數等於null,但是橫豎屏切換之後再執行的onCreate方法引數裡面是有值的,我們可以拿到這些值來恢復之前Activity的一些已有狀態,當然如果沒有在onCreate中恢復的話,系統會自動回撥onRestoreInstanceState來進行恢復;
如果我們不想在橫豎屏切換的時候進行Activity生命週期的重新載入,那麼就需要配置Activity的android:configChanges了,orientation表示消除橫豎屏的影響,keyboardHidden表示消除鍵盤的影響,screenSize表示消除螢幕大小的影響,適當的設定之後將不再會導致Activity生命週期的重新載入,每次橫豎屏切換的時候將會回撥onConfigurationChanged方法了;
當然可以通過設定Activity的android:screenOrientation屬性,直接遮蔽掉橫豎屏切換操作,這樣橫豎屏功能將不能使用,這和android:configChanges是有區別的,一個是可以用但是不會導致Activity生命週期重新載入,一個是乾脆不能用;
(7):ThreadLocal工作原理
為了理解清楚Handler的訊息處理機制,首先需要了解的知識就是ThreadLocal了,這個類並不是Android所特有的,它來自於java,ThreadLocal主要用來幹什麼呢?答案是用於如果某些資料是以執行緒作為作用域,但是每個執行緒又還想要該資料的副本的情況下,通俗點可以這樣理解,有一塊空菜地,你和你鄰居都想在裡面種菜,但是如果這塊菜地分給你的話你鄰居要想在這塊菜地裡面種菜那肯定會影響到你種菜,反之你會影響你鄰居,那麼怎麼能解決這個問題呢?給你和你鄰居都分一塊菜地,自己種自己的,這樣就不互相影響了,這就是ThreadLocal乾的事了;java中的ThreadLocal實現原理是採用ThreadLocalMap的方式來儲存當前執行緒用到的各ThreadLocal軟引用及其對應值的,而android中ThreadLocal實現方式上區別於java,他的每個執行緒中都有一個Values型別的變數,而Values型別物件中有一個Object型別陣列,陣列大小隻能是2的指數倍數,這個陣列就是用於儲存我們的ThreadLocal軟引用及其對應值的,具體儲存方式是ThreadLocal軟引用的儲存位置位於其值儲存位置的前一個位置;
可能你會想使用ThreadLocal和使用synchronized有什麼區別呢?個人認為區別挺大的,ThreadLocal的話,每個執行緒做自己的事,兩者之間不互相影響,只是他們的ThreadLocal初始化值是相等的而已,而synchronized實際上是同一時間只有一個執行緒能夠修改某一個共享變數的值而已,修改之後的值是會影響到另一個執行緒開始修改的該變數的值的;
(8):Handler工作機制
鑑於Android的UI執行緒不是執行緒安全的,這點也很好理解,如果有多個執行緒更改UI介面顯示的元素的話,最終介面到底會顯示出什麼將是不確定的,這點會讓人感覺莫名其妙,因而Android只規定主執行緒可以更新UI了,那麼如果我的子執行緒想要更新UI該怎麼辦呢?難道就不能更新了嗎?No,這就是Handler出現的原因了,雖然我們通常將Handler用在子執行緒需要更新UI的場景下,但是他的作用不止這點,他可以使用在不同執行緒之間的切換,而不僅僅是切換到主執行緒更新UI這麼侷限;
Handler工作原理:
先要弄清楚Handler訊息處理中用到的一些概念,Message用於封裝將要傳送的資料內容,MessageQueue訊息佇列用於暫存那些需要處理的Message訊息,Looper用於不斷的從MessageQueue中取出訊息進行處理,Handler訊息的封裝者和處理者,通過他進行Message訊息的生成,通過他接收Looper傳來的訊息並且進行處理,有點類似於統領者的角色,那麼Looper是什麼鬼,好端端的冒出來他幹什麼呢?MessageQueue只是Message訊息的存放者,Handler怎麼知道什麼時候需要處理訊息呢?答案就是靠Looper了,他會不斷的檢視MessageQueue,有訊息的話就交給Handler來處理了,如此看來Android訊息處理中的角色分工真的好明確啊!!注意一點,一個Handler要想真正起作用的話,他所在的執行緒中必須存在一個Looper,而在建立Looper的過程中就會建立一個MessageQueue出來,也就是Looper和MessageQueue是一一對應的;
我們一般想要更新UI的話,都是在主執行緒中建立一個Handler物件,接著在子執行緒中使用它的sendMessage方法傳送一條訊息,然後該訊息就會回撥handler的handleMessage方法進行相應的更新操作了;
那我們分析Handler機制首先就該從主執行緒開始了,在Activity啟動的時候會執行ActivityThread裡面的main方法,在該方法裡面會通過prepareMainLooper建立一個Looper物件出來,相應的也就建立了MessageQueue訊息隊列了,並且會將當前Looper物件儲存到當前執行緒的ThreadLocal裡面,也就是儲存到主執行緒的ThreadLocal裡面了,所以這也就是解釋了你在主執行緒建立Handler的時候並沒有自己建立Looper出來程式不會報錯的原因了,因為主執行緒在Activity啟動的時候就建立好了,接著我們便是在主執行緒建立Handler物件了,在建立Handler物件的構造方法裡面會獲取到在ActivityThread的main方法裡面建立的Looper物件及其對應的MessageQueue物件,接著我們會在子執行緒中通過主執行緒的Handler物件呼叫他的sendMessage方法,該方法會傳入封裝有需要傳遞給主執行緒的資料的Message物件,sendMessage實際執行的操作是呼叫enqueueMessage方法將訊息加入到MessageQueue訊息佇列中,除此之外在ActivityThread的main裡面發現會呼叫Looper.loop(),也就是會讓當前Looper運轉起來,loop方法裡面存在一個死迴圈會不斷的去檢視MessageQueue裡面有沒有訊息存在,有的話則進行出隊操作,獲取到隊頭訊息,並且獲取到處理該訊息所對應的Handler,具體來說其實就是Message的target屬性值了,然後呼叫target也就是Handler物件的dispatchMessage方法將訊息分發出去,dispatchMessage轉而會執行handleMessage方法,這也就回到了我們主執行緒中了,所以我們可以在handleMessage裡面獲取到訊息中封裝的資料進而進行一些介面上元素的修改了,這就是在主執行緒中使用Handler的訊息執行流程了;
那麼如果想要使用Handler一個執行緒傳遞資料到另一個執行緒中,但是兩個執行緒都不是主執行緒該怎麼辦呢?很明顯這種使用情況將不同於上面了,我們就該自己建立Looper物件以及其對應的MessageQueue隊列了,具體做法是:在接收資料的執行緒中通過Looper.prepare建立一個Looper物件及其對應的MrssageQueue佇列,接著呼叫Looper.loop方法讓該Looper運轉起來,可以在MessageQeueu裡面有訊息的時候進行處理,建立一個Handler物件用來進行訊息處理,並且在另一個執行緒中利用該訊息進行訊息傳送即可,這裡有一點需要注意,就是我們的loop方法是個死迴圈,他又是位於執行緒內部的,如果loop方法不結束的話,執行緒將一直處於執行狀態,這會帶來一個問題,就是我們已經明確知道訊息佇列裡面的訊息已經處理結束了,沒有訊息要處理了,Looper還是會不斷的檢視有沒有訊息存在,這會帶來效能上的損失,解決這個問題的唯一方法就是想辦法能讓loop方法結束掉,檢視loop方法的原始碼會發現,當Looper獲取到的訊息為null時就會執行return結束掉死迴圈,那麼我們就該找到什麼時候會向訊息佇列中插入一條null訊息了,答案就是在Looper的quit方法裡面了,所以我們如果在某一時刻已經明確知道MessageQueue佇列沒有訊息的話呼叫Looper的quit方法結束掉loop方法進而結束掉當前執行緒,避免效能丟失;
(9):HandlerThread原理剖析
在(8)中我們分析了Handler訊息處理機制,知道Handler要想真正起到作用的話需要藉助於Looper,而Looper裡面會建立一個MessageQueue物件出來,在主執行緒中使用Handler的時候我們完全不用考慮建立Looer以及其對應MessageQueue訊息佇列,以及Looper執行起來這些的事情,但是要想在子執行緒之間使用Handler,我們就必須通過Looper.prepare來建立Looper物件及其對應的MessageQueue物件,通過Looper.loop方法使得當前建立的Looper運轉起來了,這點本來就已經能夠滿足我們在子執行緒之間使用Handler的要求了,但是google為了能減少開發人員在子執行緒中使用Handler的麻煩,提供了HanderThread,他的實現原理其實就是我剛剛說的那些,只不過做了封裝而已,我們在建立Handler之前會先建立一個HandlerThread物件,並且呼叫它的start方法,這個start方法就比較重要了,他會呼叫HandlerThread的run方法,為什麼呢?因為HandlerThread歸根結底也是Thread嘛,呼叫start之後輾轉都會執行到run方法,在run方法裡面就會通過Looper.prepare建立Looper物件及其對應的MessageQueue訊息隊列了,同時會呼叫Looper.loop方法讓當前Looper運轉起來,所以這個run方法是最重要的了,之後建立Handler傳送訊息和接收訊息的過程就和在主執行緒使用Handler一致了,當然和我們自己在子執行緒中建立Looper使用Looper出現的問題一樣,通過HandlerThread方式使用Handler同樣也會帶來Looper物件的loop方法一直執行不會結束的情況,解決方法是呼叫HandlerThread的quit方法,該方法實際上還是呼叫的Looper的quit方法;
(10):IntentService原理分析
上面我們分析了Handler訊息處理機制以及HandlerThread裡面所涉及到的一些知識點,知道HandlerThread其實就是為了我們在子執行緒中減少自己建立Looper以及運轉Looper而出現的,那麼這次的IntentService其實封裝的更巧妙,使用HandlerThread的時候我們還需要建立Handler物件出來,但是使用IntentService連Handler物件也不用我們建立了,可見google為了讓程式設計師使用簡便做了多少工作,先來說說IntentService是幹什麼的,他是一個抽象類,因而我們在使用的時候需要建立一個實現他的類出來,它裡面僅有一個抽象方法就是onHandleIntent了,我們可以在這個方法裡面做一些處理Intent的操作了,作為Service的一種,IntentService自然也是在後臺執行的,也是通過startService啟動的,他的優先順序要高於一般的執行緒,那麼IntentService有什麼用處呢?適合於執行一些高優先順序的後臺耗時任務,高優先順序的後臺任務是Service的特點,但是由於Service是處於主執行緒的,他不適合處理耗時任務,但IntentService卻可以,原因就在於IntentService在建立的時候就會開啟一個執行緒出來,耗時任務是在該執行緒中進行的,具體點說這裡的執行緒其實就是HandlerThread了,在耗時任務處理結束之後該Service會自動停止;
我們來看看IntentService具體是怎麼做到封裝了Handler來處理耗時任務的,在IntentService的構造方法裡面你會看到建立了一個HandlerThread執行緒出來,並且呼叫了他的start方法啟動了該執行緒,上面HandlerThread中已經講過會在該執行緒的run方法裡面建立Looper物件並且呼叫loop將Looper運轉起來,接著會通過建立的Looper物件建立一個ServiceHandler出來,其實就是Handler物件而已,該物件裡面有handleMessage方法,在我們通過startService方法啟動IntentService的時候會回撥onStartCommand方法,該方法會執行IntentService的onStart方法,而正是在onStart方法裡面會將我們startService傳入的intent物件封裝成Message物件通過在建構函式中建立的ServiceHandler型別handler物件的sendMessage方法傳送出去,那麼緊接著就會回撥ServiceHandler的handleMessage方法了,handleMessage方法實際上執行的是onHandleIntent方法,也就是我們在實現IntentService抽象類的時候需要實現的方法,具體實現對Intent的操作,操作結束之後handleMessage方法會執行stopSelf方法結束當前IntentService;
(11):使用new Message()和obtainMessage兩種方式得到Message物件有什麼區別?
我們在平常使用Handler sendMessage方法的時候都要傳遞Message引數進去,通常建立Message物件有兩種方式,一種就是常用的通過建構函式的方式建立物件,一種就是通過Handler的obtainMessage了,既然都能new了說明Message的建構函式是public的,那麼還來個obtainMessage幹嘛呢?答案就是為了節省記憶體資源,如果你檢視Message的定義的話,會發現它裡面有一個next欄位,這個欄位的屬性值是Message型別的,所以從這種角度看的話Message本身就可以作為連結串列存在,我們的Message訊息池其實就是儲存著第一個Message訊息而已,之後的訊息都是通過next欄位連結到一起的,使用obtainMessage首先會去檢視當前訊息池中有沒有訊息存在,存在的話則直接取到該訊息並且將該訊息從訊息池中刪除同時將訊息池大小減一即可,也就是將連結串列長度減一,如果訊息池中不存在訊息的話才會通過new Message的方式建立訊息出來,我們每次使用完訊息之後通過執行Message的recycle會將當前使用過的訊息物件新增到訊息池中,也就是加入連結串列中,當然在加入之前需要將原訊息中的內容資訊全部置位,這樣有效減緩了你頻繁通過new Message方式建立訊息的記憶體開銷,保證了只有在當前訊息池不再存在可用訊息的情況下才去建立訊息出來,so perfect!!!
(12):AsyncTask工作原理淺析
要想理解清楚AsyncTask的工作原理首先就應該搞清楚Handler的工作機制,前面已經分析過啦,那我們就直接開始了,我們平常使用AsyncTask是建立AsyncTask物件之後執行execute,建立AsyncTask物件的時候會同時建立一個WorkerRunnable物件,並且以這個WorkerRunnable物件為引數會建立一個FutureTask物件,那麼分析AsyncTask的原理就該從execute方法開始了,執行execute方法首先會執行executeOnExecutor方法,並且傳入一個SerialExecutor型別的物件,SerialExecutor是一個序列執行緒池,一個執行緒裡面的所有AsyncTask全部都在這個序列的執行緒池中排隊執行,在executeOnExecutor裡面首先會執行onPreExecute方法,該方法是在我們建立AsyncTask物件的時候自己實現的,執行在主執行緒中,我們可以在這個方法裡面進行任務開始的提示性操作,接著執行緒池開始執行,也就是從這一步開始切換到了子執行緒中,傳入的物件就是我們建立AsyncTask物件的時候生成的FutureTask物件,在SerialExecutor執行緒池的execute方法中首先會把當前FutureTask物件插入到任務佇列中,如果當前任務佇列中沒有正在活動的AsyncTask任務的話,則會執行scheduleNext方法從佇列中取得一個AsyncTask任務,同時當一個AsyncTask任務執行結束之後會在finally中呼叫scheduleNext方法執行任務佇列中的下一個AsyncTask任務,從這裡也看出來預設情況下AsyncTask是序列執行的,那麼真正的執行操作就該在scheduleNext方法裡面了,可以看到這個方法裡面真正執行任務的執行緒池是THREAD_POOL_EXECUTOR,很多人都在想那剛剛的SerialExecutor執行緒池是用來幹嘛的呢,它主要是用來任務排隊的,保證預設情況下的序列執行而已,而THREAD_POOL_EXECUTOR才是真正的任務執行者,此外在AsyncTask裡面還有一個InternalHandler物件,其實他就是一個Handler物件而已,他存在的作用就是為了從子執行緒切換到主執行緒中,為了便於在子執行緒執行的過程中進行一些與介面元素的互動過程,比如下載進度條的更新等等,那麼也就必須要求該InternalHandler物件在主執行緒中建立了,檢視原始碼你會發現InternalHandler物件是static的,也就是在AsyncTask物件建立的時候他就會建立,因此只要保證AsyncTask物件在主執行緒中建立就可以了,因此我們使用AsyncTask的時候一定要注意在主執行緒中建立他的物件,扯的有點遠了,THREAD_POOL_EXECUTOR會執行他的execute方法,該方法實際上執行的是FutureTask的run方法,而FutureTask的run方法實際上執行的是建立FutureTask物件的時候傳入的引數WorkerRunnable物件的call方法,檢視call方法可以看到執行了doInBackground方法,該方法也是需要我們在建立AsyncTask物件的時候自己實現的,我們可以在這個方法裡面執行一些比較耗時的操作,它執行在子執行緒中,在該方法中我們可以通過publishProgress來發送一些耗時任務已經處理的進度資訊,該方法執行在子執行緒中,該方法中會通過InternalHandler將進度訊息傳送出去,接著在InternalHandler裡面的handleMessage裡面會發現是通過onProgressUpdate進行訊息處理的,該方法執行在主執行緒中,可以進行更新進度條的一些操作,在doInBackground方法執行結束後會將返回結果作為引數傳遞給postResult方法,該方法同樣會通過InternalHandler傳送訊息,最後在InternalHandler裡面的handleMessage裡面處理該訊息,呼叫的是finish方法,也就是將執行緒切換到了主執行緒中了,在finish方法中會根據主執行緒有沒有被暫停來執行onCancelled或者onPostExecute方法,這兩個方法是執行在主執行緒的,到這裡AsyncTask的執行結束了;
關於AsyncTask中需要注意的幾點:
(1):預設情況下AsyncTask之間是序列執行的;
(2):AsyncTask裡面存在兩個執行緒池,一個是SerialExecutor型別的,它主要是用來進行AsyncTask任務排隊的,一個是THREAD_POOL_EXECUTOR執行緒池,它才是任務的真正執行者;
(3):AsyncTask內部有一個InternalHandler型別的變數,主要用於在任務執行的過程中主執行緒和子執行緒之間的切換的,因此他必須在主執行緒中建立,因為他在AsyncTask中是static修飾的,因此在AsyncTask載入的時候他就被建立了,因此間接要求AsyncTask在主執行緒建立了;
(13):AsyncTask中各個方法哪些在主執行緒執行哪些在子執行緒執行?
onPreExecute在主執行緒執行;
doInBackground方法在子執行緒中執行;
publishProgress在子執行緒中執行;
onProgressUpdate在主執行緒中執行;
onCancelled在主執行緒中執行;
onPostExecute在主執行緒中執行;
(14):AsyncTask可以並行執行嗎?
可以的,從Android3.0開始,我們可以通過直接呼叫AsyncTask方法的executeOnExecutor方法傳入自己定義的執行緒池,沒錯,這個方法也是預設情況下呼叫AsyncTask的execute方法真正執行的方法,但是預設情況下傳入的是SerialExecutor型別的執行緒池,他會對AsyncTask任務進行排隊,雖然THREAD_POOL_EXECUTOR執行緒池本身是可以並行處理任務的,但是因為任務都是靠SerialExecutor執行緒池序列取出來的,所以也就造成了AsyncTask預設情況下序列執行的特點了;但是如果我們直接傳入自己定義的執行緒池的話,預設執行緒池是可以並行處理的,你也可以傳入AsyncTask內部已經定義的THREAD_POOL_EXECUTOR執行緒池,這樣也行;
(15):IntentService和Service的對比
(1):首先從名字上看兩者都是Service,所以呢都有Service的特點,都可以處理後臺任務;
(2):IntentService是可以處理耗時任務的,原因在於在建立他的時候建立了一個HandlerThread型別的執行緒;而Service本身是不可以處理耗時任務的,因為它執行在主執行緒中,也就是說你在Servicve裡面進行耗時操作會出現ANR異常,但是IntentService裡面是不會的;
(3):IntentService在他的所有任務執行結束之後會自動呼叫stopSelf來結束該IntentService,但是Service卻需要我們通過stopService方式來結束;
(4):IntentService是以序列的方式執行任務的,原因在於IntentService內部訊息處理的實現原理是通過Handler加MessageQueue加Looper來實現的;2017.7.21更新...........................................................................
(16):Activity啟動模式
Activity的啟動模式分為:standard、singleTop、singleTask、singleInstance,可以在Activity的標籤下通過android;launchMode來進行設定,為什麼要有Activity的啟動模式呢?預設情況下我們多次啟動同一個Activity的時候預設會建立多個例項放入到任務棧中,這樣重複建立例項的做法顯然有點不太科學,我們有時候完全可以直接利用之前建立的例項就行了,Activity的啟動模式就是做這個的;
standard:每次啟動Activity都會建立新的Activity例項,即使在當前Activity棧中已經存在同樣的Activity例項,這是預設的啟動模式;
singleTop:通俗點就是棧頂複用模式,每次在啟動Activity的時候首先會去檢視當前Activity棧的棧頂位置的Activity例項是否是需要啟動的Activity,如果是的話,則Activity將不再會建立,即不會執行onCreate、onStart方法,同時它的onNewIntent方法將會被呼叫;如果棧頂不是要啟用的Activity的話,則會建立一個Activity出來,並且將它壓入棧頂;
singleInTask:通俗點講是棧內複用模式,什麼意思呢?就是相當於棧內的單例模式吧,如果當前要啟動的Activity在當前Activity棧存在的話,那麼他會把棧內該Activity上面的所有Activity全部出棧,知道要用的Activity位於棧頂為止,然後呼叫它就可以了;如果當前Activity棧中不存在將要啟用的Activity的話,則建立新的Activity出來,並且將該Activity壓入到棧中;
singleInstance:單例項模式,可以認為是singleTask的加強模式,處於該模式的Activity只能單獨位於一個任務棧中,也就是說該任務棧中將只有一個Activity例項;
上面多次提到了任務棧,判斷一個Activity到底屬於哪個任務棧這點會涉及到Activity的TaskAffinity屬性,我們可以在Activity的標籤下通過指定android:affinity來進行設定,預設情況下不進行設定的話Activity任務棧的名字就是當前Activity所在的包名;
(17):子執行緒中更新UI的方式
我們都知道只有主執行緒可以更新UI,那麼如果子執行緒想要更新UI怎麼辦呢?只能是藉助於Handler來實現了,那麼具體的實現方式有哪些呢?
(1):最常見的方式就是通過Handler的sendMessage和handleMessage來進行處理了,這個比較簡單,不再舉例;
(2):通過Handler的post方法,這種執行方式需要在post方法中傳入執行耗時任務的執行緒,接著在執行post方法的時候,會將該執行任務的執行緒封裝到Message裡面的callback屬性,之後當Handler裡面的Looper迴圈檢視MessageQueue訊息佇列的時候會取到這條訊息,取到訊息中的執行耗時操作的執行緒,直接執行他的run方法就可以了,我們可以在該run方法中進行更新UI操作的;
(3):通過View的post方法,這種方法實質上是通過Handler的post方法完成的;
(4):通過Activity的runOnUiThread方法,該方法同樣還是通過的Handler的post方法實現的;
詳情可以看這篇部落格
(18):Android中assets資料夾與raw資料夾的區別
相同點:兩者目錄下的檔案在打包之後都會原封不動的打包在apk檔案中,不會被編譯成二進位制檔案;
不同點:(1):res/raw中的檔案會在R.java中生成ID,但是assets資料夾中的內容不會在R.java中生成ID;
(2):因為res/raw中的檔