Android 面試準備進行曲-Android基礎進階

語言: CN / TW / HK

View相關

View的繪製流程

自定義控制元件: 1、組合控制元件。這種自定義控制元件不需要我們自己繪製,而是使用原生控制元件組合成的新控制元件。如標題欄。 2、繼承原有的控制元件。這種自定義控制元件在原生控制元件提供的方法外,可以自己新增一些方法。如製作圓角,圓形圖片。 3、完全自定義控制元件:這個View上所展現的內容全部都是我們自己繪製出來的。比如說製作水波紋進度條。

View的繪製流程:OnMeasure()——>OnLayout()——>OnDraw()

第一步:OnMeasure():測量檢視大小。從頂層父View到子View遞迴呼叫measure方法,measure方法又回撥OnMeasure。

第二步:OnLayout():確定View位置,進行頁面佈局。從頂層父View向子View的遞迴呼叫view.layout方法的過程,即父View根據上一步measure子View所得到的佈局大小和佈局引數,將子View放在合適的位置上。

第三步:OnDraw():繪製檢視。ViewRoot建立一個Canvas物件,然後呼叫OnDraw()。

1.webp

View,ViewGroup事件分發

Touch事件分發中只有兩個主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三個相關事件。View包含dispatchTouchEvent、onTouchEvent兩個相關事件。其中ViewGroup又繼承於View。

2.ViewGroup和View組成了一個樹狀結構,根節點為Activity內部包含的一個ViwGroup。

3.觸控事件由Action_Down、Action_Move、Aciton_UP組成,其中一次完整的觸控事件中,Down和Up都只有一個,Move有若干個,可以為0個。

4.當Acitivty接收到Touch事件時,將遍歷子View進行Down事件的分發。ViewGroup的遍歷可以看成是遞迴的。分發的目的是為了找到真正要處理本次完整觸控事件的View,這個View會在onTouchuEvent結果返回true。

5.當某個子View返回true時,會中止Down事件的分發,同時在ViewGroup中記錄該子View。接下去的Move和Up事件將由該子View直接進行處理。由於子View是儲存在ViewGroup中的,多層ViewGroup的節點結構時,上級ViewGroup儲存的會是真實處理事件的View所在的ViewGroup物件:如ViewGroup0-ViewGroup1-TextView的結構中,TextView返回了true,它將被儲存在ViewGroup1中,而ViewGroup1也會返回true,被儲存在ViewGroup0中。當Move和UP事件來時,會先從ViewGroup0傳遞至ViewGroup1,再由ViewGroup1傳遞至TextView。

6.當ViewGroup中所有子View都不捕獲Down事件時,將觸發ViewGroup自身的onTouch事件。觸發的方式是呼叫super.dispatchTouchEvent函式,即父類View的dispatchTouchEvent方法。在所有子View都不處理的情況下,觸發Acitivity的onTouchEvent方法。

7.onInterceptTouchEvent有兩個作用:1.攔截Down事件的分發。2.中止Up和Move事件向目標View傳遞,使得目標View所在的ViewGroup捕獲Up和Move事件。

2.webp

view 事件分發流程

3.webp

ViewGroup 時間分發流程

4.webp

整體Activity - ViewGroup - view 分發流程

5.webp

View 事件分發及原始碼講解

MeasureSpec 相關知識

MeasureSpec 是一個32位int值,高2位代表SpecMode(測量模式),低30位代表SpecSize( 某種測量模式下的規格大小)。通過寬測量值widthMeasureSpec和高測量值heightMeasureSpec決定View的大小 SpecMode 代表的三種測量模式分別為: 1. UNSPECIFIED:父容器不對View有任何限制,要多大有多大。常用於系統內部。

  1. EXACTLY(精確模式):父檢視為子檢視指定一個確切的尺寸SpecSize。對應LyaoutParams中的match_parent或具體數值。

  2. AT_MOST(最大模式):父容器為子檢視指定一個最大尺寸SpecSize,View的大小不能大於這個值。對應LayoutParams中的wrap_content。

決定因素:值由子View的佈局引數LayoutParams和父容器的MeasureSpec值共同決定。見下圖:

6.webp

參考圖片 及講解地址

SurfaceView和View的區別

SurfaceView是從View基類中派生出來的顯示類,他和View的區別有:

  • View需要在UI執行緒對畫面進行重新整理,而SurfaceView可在子執行緒進行頁面的重新整理,View適用於主動更新的情況,View頻繁重新整理會阻塞主執行緒,導致介面卡頓

  • SurfaceView在底層已實現雙緩衝機制,而View沒有,因此SurfaceView更適用於被動更新,需要頻繁重新整理、重新整理時資料處理量很大的頁面,而SurfaceView適用於

invalidate()和postInvalidate()的區別

invalidate()與postInvalidate()都用於重新整理View,主要區別是invalidate()在主執行緒中呼叫,若在子執行緒中使用需要配合handler;而postInvalidate()可在子執行緒中直接呼叫。 我們通過 postInvalidate 如何在子執行緒中更新的

```java // 系統程式碼 public void postInvalidateDelayed(long delayMilliseconds) { // We try only with the AttachInfo because there's no point in invalidating // if we are not attached to our window final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds); } }

``` 接下來我們看下

```java // 系統程式碼 public void dispatchInvalidateDelayed(View view, long delayMilliseconds) { Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view); mHandler.sendMessageDelayed(msg, delayMilliseconds); }

``` 我們可以看到 postInvalidate它是向主執行緒傳送個Message,然後handleMessage時,呼叫了invalidate()函式。(系統幫我們 寫好了 Handle部分)

Android 動畫

Android中的幾種動畫

幀動畫:指通過指定每一幀的圖片和播放時間,有序的進行播放而形成動畫效果,比如想聽的律動條。

補間動畫:指通過指定View的初始狀態、變化時間、方式,通過一系列的演算法去進行圖形變換,從而形成動畫效果,主要有Alpha、Scale、Translate、Rotate四種效果。注意:只是在檢視層實現了動畫效果,並沒有真正改變View的屬性,比如滑動列表,改變標題欄的透明度。

屬性動畫:在Android3.0的時候才支援,通過不斷的改變View的屬性,不斷的重繪而形成動畫效果。相比於檢視動畫,View的屬性是真正改變了。比如view的旋轉,放大,縮小。

屬性動畫和補間動畫區別

  • 補間動畫僅僅是 Parents View 對子View裡面的畫布進行操作,新位置並不響應點選事件,原位置響應。
  • 屬性動畫是通過修改view屬性實現動畫,新位置響應點選事件

屬性動畫為何在新位置還能響應事件

ViewGroup 在 getTransformedMotionEvent() 方法中會通過子 View 的 hasIdentityMatrix() 方法來判斷子 View 是否應用過位移、縮放、旋轉之類的屬性動畫。如果應用過的話,那還會呼叫子 View 的 getInverseMatrix() 做「反平移」操作,然後再去判斷處理後的觸控點是否在子 View 的邊界範圍內。

屬性動畫點選解密

屬性動畫原理

屬性動畫要求 動畫作用的物件提供該屬性的set方法,屬性動畫根據你傳遞的該熟悉的初始值和最終值,以動畫的效果多次去呼叫set方法,每次傳遞給set方法的值都不一樣,確切來說是隨著時間的推移,所傳遞的值越來越接近最終值。如果動畫的時候沒有傳遞初始值,那麼還要提供get方法,因為系統要去拿屬性的初始值。

java // 系統程式碼 void setAnimatedValue(Object target) { if (mProperty != null) { mProperty.set(target, getAnimatedValue()); } if (mSetter != null) { try { mTmpValueArray[0] = getAnimatedValue(); mSetter.invoke(target, mTmpValueArray); } catch (InvocationTargetException e) { Log.e("PropertyValuesHolder", e.toString()); } catch (IllegalAccessException e) { Log.e("PropertyValuesHolder", e.toString()); } } } 屬性動畫原始碼解析

Handler 詳解

Handler的原理

Android中主執行緒是不能進行耗時操作的,子執行緒是不能進行更新UI的。所以就有了handler,它的作用就是實現執行緒之間的通訊。 handler整個流程中,主要有四個物件,handlerMessage,MessageQueue,Looper。當應用建立的時候,就會在主執行緒中建立handler物件。

對於Message

線上程之間傳遞的訊息,它的內部持有Handler和Runnable的引用以及訊息型別。可以使用what、arg1、arg2欄位攜帶一些整型資料,使用obj欄位攜帶Object物件;其中有一個obtain()方法,該方法的內部是先通過訊息池獲取訊息,沒有再建立,實現了對message物件的複用。其內部有一個target引用,就是對Handler物件的引用,在Looper.loop方法中的訊息處理就是通過message的target引用來呼叫Handler的dispatchMessage()方法來實現訊息的處理。

對於Message Queue:

指的是訊息佇列,是通過一個 單鏈表 的資料結構維護訊息列表的,在插入和刪除有優勢。其中主要包括兩個操作:插入和讀取,讀取操作本身伴隨著刪除操作。插入操作是enqueueMessage()方法,就是插入一條訊息到MessageQueue中;讀取操作是next()方法,它是一個無限迴圈,如果有訊息就返回並從單鏈表中移除;沒有訊息就一直阻塞(此時主執行緒會釋放CPU進入休眠狀態)。

對於Looper:

Looper在訊息機制中進行訊息迴圈,像一個泵,不斷地從MessageQueue中檢視是否有新訊息並提取,交給handler處理。Handler機制一定要Looper,線上程中通過Looper.prepare()為當前執行緒建立一個Looper,並使用Looper.loop()來開啟訊息的讀取。為什麼在平常Activity主執行緒使用時沒有使用到Looper呢?因為對於主執行緒(UI執行緒),會自動建立一個Looper 驅動訊息佇列獲取訊息,所以Looper可以通過getMainLooper獲取到主執行緒的Looper。 通過quit/quitSafely可以退出Looper,區別在於quit會直接退出,quitSafely會把訊息佇列已有的訊息處理完畢後才退出Looper。

對於Handler

Handler可以傳送和接收訊息。傳送訊息(就是往MessageQueue裡面插入一條Message)通過post方法和send方法,而post方法最終也是通過send方法來發送的,最終就會呼叫sendMessageAtTime這個方法(內部就是呼叫MessageQueue的enqueueMessage()方法,往MessageQueue裡面插入一條訊息),同時也會給msg的target賦值為handler本身,進入MessageQueue中。處理訊息就是Looper呼叫loop()方法進入無限迴圈,獲取到訊息後就會呼叫msg.target(Handler本身)的dispatchMessage()方法,進而呼叫handlerMessage()方法處理訊息。

Handler導致記憶體洩露問題

一般我們寫Handler:

java Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { mImageView.setImageBitmap(mBitmap); } }

當使用內部類(包括匿名類)來建立Handler的時候,Handler物件會隱式地持有一個外部類物件(通常是一個Activity)的引用,而常常在Activity退出後,訊息佇列還有未被處理完的訊息,此時activity依然被handler引用,導致記憶體無法回收而記憶體洩露。

在Handler中增加一個對Activity的弱引用(WeakReference):

```java static class MyHandler extends Handler { WeakReference mActivityReference;

MyHandler(Activity activity) {
    mActivityReference= new WeakReference(activity);
}

@Override
public void handleMessage(Message msg) {
    final Activity activity = mActivityReference.get();
    if (activity != null) {
        mImageView.setImageBitmap(mBitmap);
    }
}

}

```

如果在非自定義 Handler 情況下,還可以通過 Activity 生命週期來及時清除訊息,從而及時回收 Activity

kotlin override fun onDestroy() { super.onDestroy() if (mHandler != null){ mHandler.removeCallbacksAndMessages(null) } }

Handler的post方法原理

java mHandler.post(new Runnable() { @Override public void run() { Log.e(“TAG”, Thread.currentThread().getName()); mTxt.setText(“yoxi”); } }); 然後run方法中可以寫更新UI的程式碼,其實這個Runnable並沒有建立什麼執行緒,而是傳送了一條訊息,下面看原始碼:

java public final boolean post(Runnable r) { return sendMessageDelayed(getPostMessage(r), 0); } 最終和handler.sendMessage一樣,呼叫了sendMessageAtTime,然後呼叫了enqueueMessage方法,給msg.target賦值為handler,最終加入MessagQueue.

Handler 其他問題

  1. Looper.loop()和MessageQueue.next()取延時訊息時,主執行緒中使用死迴圈為什麼不會卡死?

    答: 在MessageQueue在取訊息時,如果是延時訊息就會計算得到該延時訊息還需要延時多久nextPollTimeoutMillis。然後再繼續迴圈的時候,發現nextPollTimeoutMillis不等於0,就會執行nativePollOnce阻塞執行緒nextPollTimeoutMillis毫秒,而阻塞了之後被喚醒的時機就是阻塞的時間到了或者又有新的訊息新增進來執行enqueueMessage方法呼叫nativeWake喚醒阻塞執行緒,再繼續執行獲取訊息的程式碼,如果有訊息就返回,如果還是需要延時就繼續和上邊一樣阻塞。而Android所有的事件要在主執行緒中改變的都會通過主執行緒的Handler傳送訊息處理,所以就完全保證了不會卡死。

    其中nativePollOnce的位置也有考究,剛好在synchronized的外邊,所以在阻塞的時候也能保證新增訊息是可以執行的,而取訊息 時新增訊息就需要等待。

  2. MessageQueue是佇列嗎? 答: MessageQueue不是佇列,它內部使用一個Message連結串列實現訊息的存和取。

  3. Handler的postDelay,時間準嗎?它用的是system.currentTime嗎? 答: 不準,因為looper裡邊從MessageQueue裡取出來的訊息執行也是序列的,如果前一個訊息是比較耗時的,那麼等到執行之前延時的訊息時時間難免可能會超過延時的時間。postDelay時用的是System.uptimeMillis,也就是開機時間。

  4. 子執行緒run方法中如何使用Handler? 答 : 先要使用Lopper.prepare方法,然後使用該looper建立一個Handler,最後呼叫Looper.loop方法;Looper.loop方法之後就不要執行寫程式碼了,因為是loop是死迴圈除非退出,所以Handler的建立也必須寫在loop之前。

  5. ThreadLocal是如何實現一個執行緒一個Looper的? 答: Looper的使用最終都需要執行loop方法,而loop方法中去獲取的Looper是從sThreadLocal中獲取的,所以Looper就需要和sThreadLocal建立關係,在不考慮反射的情況下,就只能通過Looper的prepare方法進行關聯,這裡邊就會引入一個threadLocalMap,該物件又是和thread一一對應,而threadLocal的get方法實際使用的就是threadLocalMap的get方法,而key就是Looper中的靜態變數sThreadLocal,value則就是當前looper物件,而prepare方法只能被執行一次,也就保證了一個執行緒只有一個looper。ThreadLocalMap對key和value的存取和hashMap類似。

  6. 假設先 postDelay 10ms, 再postDelay 1ms,這兩個訊息會有什麼不同的經歷。 答: 先傳入一個延時為10ms的訊息進入MessageQueue中,因為該訊息延時,假設當前訊息佇列中沒有訊息,則會直接將訊息放入佇列,因為loop一直在取訊息,但是這裡有延時就會阻塞10ms,當然這不考慮程式碼執行的時間;然後延時1ms的訊息進入時,會和之前的10ms的訊息進行比較,根據延時的大小進行排序插入,延時小的在前邊,所以這時候就把1ms的訊息放在10ms的前邊,然後喚醒,不阻塞,繼續執行取訊息的操作,發現還是有延時1ms,所以也會繼續阻塞1ms,直到阻塞1ms之後或者又有新的訊息進入佇列喚醒,直到獲取到1ms延時訊息,在loop中,通過呼叫handler的dispatchMessage方法,判斷訊息的callback或者Handler的callback不為null就回調對應的callback,否則就執行handler的handleMessage方法,我們就可以根據情況處理訊息了;10ms的延時訊息的處理也是一致,延時的時間到了就交給返回給looper,然後給handler處理。

  7. HandlerThread ? 答: HandlerThread是Thread的一個子類,只是內部建立了一個Handler,這個Handler是子執行緒的handler,其中子執行緒的looper的建立和管理也提供了方法方便使用。

  8. 你對 Message.obtain() 瞭解嗎? 答: Message.obtain其實是從緩衝的訊息池中取出第一個訊息來使用,避免訊息物件的頻繁建立和銷燬;訊息池其實是使用Message連結串列結構實現,在訊息在loop中被handler分發消費之後會執行回收的操作,將該訊息內部資料清空並新增到訊息連結串列最前邊。

多執行緒相關問題

如何建立多執行緒

  1. 繼承Thread類,重寫run函式方法
  2. 實現Runnable介面,重寫run函式方法
  3. 實現Callable介面,重寫call函式方法
  4. HandlerThread
  5. AsyncTask很老的一種= =

多執行緒間同步問題

  1. volatile關鍵字,在get和set的場景下是可以的,由於get和set的時候都加了讀寫記憶體屏障,在資料可見性上保證資料同步。但是對於++這種非原子性操作,資料會出現不同步

  2. synchronized對程式碼塊或方法加鎖,結合wait,notify排程保證資料同步

  3. reentrantLock加鎖結合Condition條件設定,線上程排程上保障資料同步

  4. CountDownLatch簡化版的條件鎖,線上程排程上保障資料同步
  5. cas=compare and swap(set), 在保證操作原子性上,確保資料同步
  6. 參照UI執行緒更新UI的思路,使用handler把多執行緒的資料更新都集中在一個執行緒上,避免多執行緒出現髒讀

  7. 當然如果只是部分變數存在多執行緒修改的可能性 建議使用 原子類AtomicInteger AtomicBoolean等 這樣會更方便一點。

Android 優化

OOM

  1. 根據java的記憶體模型會出現記憶體溢位的記憶體有堆記憶體、方法區記憶體、虛擬機器棧記憶體、native方法區記憶體;一般說的OOM基本都是針對堆記憶體;

  2. 對於堆記憶體溢位主的根本原因有兩種 (1)app程序記憶體達到上限 (2)手機可用記憶體不足,這種情況並不是我們app消耗了很多記憶體,而是整個手機記憶體不足

而我們需要解決的主要是 app的記憶體達到上限

  1. 對於app記憶體達到上限只有兩種情況 (1)申請記憶體的速度超出gc釋放記憶體的速度 (2)記憶體出現洩漏,gc無法回收洩漏的記憶體,導致可用記憶體越來越少

  2. 對於申請記憶體速度超出gc釋放記憶體的速度主要有2種情況 (1)往記憶體中載入超大檔案 (2)迴圈建立大量物件

  3. 一般申請記憶體的速度超出gc釋放記憶體基本不會出現,記憶體洩漏才是出現問題的關鍵所在

導致記憶體洩漏情況

記憶體洩漏的根本原因在於生命週期長的物件持有了生命週期短的物件的引用

  1. 資源物件沒關閉造成的記憶體洩漏(如: Cursor、File等)

  2. 全域性集合類強引用沒清理造成的記憶體洩漏( static 修飾的集合)

  3. 接收器、監聽器註冊沒取消造成的記憶體洩漏,如廣播,eventsbus

  4. Activity 的 Context 造成的洩漏,可以使用 ApplicationContext

  5. Handler 造成的記憶體洩漏問題(一般由於 Handler 生命週期比其外部類的生命週期長引起的)

注1:ListView 的 Adapter 中快取用的 ConvertView ,主要快取的是 移除螢幕外的View,就算沒有複用,暫時 只會 記憶體溢位,和洩漏還是有區別的。

注2 :Bitmap 物件到底要不要呼叫 recycle() 釋放記憶體。結論 Android 3.0 以前需要,因為畫素資料與物件本身分開儲存,畫素資料儲存在native層;物件儲存在java層。 3.0之後 畫素資料與Bitmap物件資料一起關聯儲存在Dalvik堆中。所以,這個時候,就可以考慮用GC來自動回收。所以我們不用的時候直接 將Bitmap物件設定為Null 即可。參考部落格地址

我們列舉了 大部分常見的 記憶體洩漏出現的時機,那麼我也簡要的列舉下 常見的避免記憶體洩漏的方法(僅供參考);

  1. 為應用申請更大記憶體,把manifest上的largdgeheap設定為true

  2. 減少記憶體的使用 ①使用優化後的集合物件,比如SpaseArray; ②使用微信的mmkv替代sharedpreference; ③對於經常打log的地方使用StringBuilder來組拼,替代String拼接 ④統一帶有快取的基礎庫,特別是圖片庫,如果用了兩套不一樣的圖片載入庫就會出現2個圖片各自維護一套圖片快取 ⑤給ImageView設定合適尺寸的圖片,列表頁顯示縮圖,檢視大圖顯示原圖 ⑥優化業務架構設計,比如省市區資料分批載入,需要載入省就載入省,需要載入市就載入失去,避免一下子載入所有資料

  3. 避免記憶體洩漏

    編碼規範上:

    ①資源物件用完一定要關閉,最好加finally ②靜態集合物件用完要清理 ③接收器、監聽器使用時候註冊和取消成對出現 ④context使用注意生命週期,如果是靜態類引用直接用ApplicationContext ⑤使用靜態內部類 ⑥結合業務場景,設定軟引用,弱引用,確保物件可以在合適的時機回收

建設記憶體監控體系:

線下監控:

①使用ArtHook檢測圖片尺寸是否超出imageview自身寬高的2倍

②編碼階段Memery Profile看app的記憶體使用情況,是否存在記憶體抖動,記憶體洩漏,結合Mat分析記憶體洩漏

線上監控:

①上報app使用期間待機記憶體、重點模組記憶體、OOM率

②上報整體及重點模組的GC次數,GC時間

③使用LeakCannery自動化記憶體洩漏分析

ANR

Android系統中,AMS和WMS會檢測App的響應時間,如果App在主執行緒進行耗時操作,導致使用者的操作沒得到及時的響應 就會報出 Application Not Response 的問題 即ANR 。

  1. activity 、鍵盤輸入事件和觸控事件超過五秒
  2. 前臺廣播10秒沒有完成 後臺60秒
  3. 服務前臺20秒 後臺200秒

主要的 規避方案

解決籠統一下儘量使用 子執行緒,避免死鎖 的出現,使用子執行緒來處理耗時操作或阻塞任務。服務內容提供者儘量不要執行太長時間的任務。