1. 程式人生 > >Android面試整理(附答案)

Android面試整理(附答案)

面試,無非都是問上面這些問題(挺多的 - -!),聘請中高階的安卓開發會往深的去問,並且會問一延伸二。以下我先提出幾點重點,是面試官基本必問的問題,請一定要去了解!

  • 基礎知識 – 四大元件(生命週期,使用場景,如何啟動)

  • java基礎 – 資料結構,執行緒,mvc框架

  • 通訊 – 網路連線(HttpClient,HttpUrlConnetion),Socket

  • 資料持久化 – SQLite,SharedPreferences,ContentProvider

  • 效能優化 – 佈局優化,記憶體優化,電量優化

  • 安全 – 資料加密,程式碼混淆,WebView/Js呼叫,https

  • UI– 動畫

  • 其他 – JNI,AIDL,Handler,Intent等

  • 開源框架 – Volley,Gilde,RxJava等(簡歷上寫你會的,用過的)

  • 拓展 – Android6.0/7.0/8.0/9.0特性,kotlin語言,I/O大會

急急忙忙投簡歷,趕面試,還不如沉澱一兩天時間,再過一遍以上內容。想穩妥拿到一個offer,最好能理解實現原理,並且知道使用場景了。不要去背!要去理解!面試官聽了一天這些內容是很厭倦的,最好能說出一些自己的見解。

Java中引用型別的區別,具體的使用場景

Java中引用型別分為四類:強引用、軟引用、弱引用、虛引用。

強引用:強引用指的是通過new物件建立的引用,垃圾回收器即使是記憶體不足也不會回收強引用指向的物件。

軟引用:軟引用是通過SoftRefrence實現的,它的生命週期比強引用短,在記憶體不足,丟擲OOM之前,垃圾回收器會回收軟引用引用的物件。軟引用常見的使用場景是儲存一些記憶體敏感的快取,當記憶體不足時會被回收。

弱引用:弱引用是通過WeakRefrence實現的,它的生命週期比軟引用還短,GC只要掃描到弱引用的物件就會回收。弱引用常見的使用場景也是儲存一些記憶體敏感的快取。

虛引用:虛引用是通過FanttomRefrence實現的,它的生命週期最短,隨時可能被回收。如果一個物件只被虛引用引用,我們無法通過虛引用來訪問這個物件的任何屬性和方法。它的作用僅僅是保證物件在finalize後,做某些事情。虛引用常見的使用場景是跟蹤物件被垃圾回收的活動,當一個虛引用關聯的物件被垃圾回收器回收之前會收到一條系統通知。

Exception和Error的區別

Exception和Error都繼承於Throwable,在Java中,只有Throwable型別的物件才能被throw或者catch,它是異常處理機制的基本組成型別。

Exception和Error體現了Java對不同異常情況的分類。Exception是程式正常執行中,可以預料的意外情況,可能並且應該被捕獲,進行相應的處理。

Error是指在正常情況下,不大可能出現的情況,絕大部分Error都會使程式處於非正常、不可恢復的狀態。既然是非正常,所以不便於也不需要捕獲,常見的OutOfMemoryError就是Error的子類。

Exception又分為checked Exception和unchecked Exception。checked Exception在程式碼裡必須顯式的進行捕獲,這是編譯器檢查的一部分。unchecked Exception也就是執行時異常,類似空指標異常、陣列越界等,通常是可以避免的邏輯錯誤,具體根據需求來判斷是否需要捕獲,並不會在編譯器強制要求。

volatile

一般提到volatile,就不得不提到記憶體模型相關的概念。我們都知道,在程式執行中,每條指令都是由CPU執行的,而指令的執行過程中,勢必涉及到資料的讀取和寫入。程式執行中的資料都存放在主存中,這樣會有一個問題,由於CPU的執行速度是要遠高於主存的讀寫速度,所以直接從主存中讀寫資料會降低CPU的效率。為了解決這個問題,就有了快取記憶體的概念,在每個CPU中都有快取記憶體,它會事先從主存中讀取資料,在CPU運算之後在合適的時候重新整理到主存中。

這樣的執行模式在單執行緒中是沒有任何問題的,但在多執行緒中,會導致快取一致性的問題。舉個簡單的例子:i=i+1 ,在兩個執行緒中執行這句程式碼,假設i的初始值為0。我們期望兩個執行緒執行後得到2,那麼有這樣的一種情況,兩個執行緒都從主存中讀取i到各自的快取記憶體中,這時候兩個執行緒中的i都為0。線上程1執行完畢得到i=1,將之重新整理到主存後,執行緒2開始執行,由於執行緒2中的i是快取記憶體中的0,所以在執行完執行緒2之後重新整理到主存的i仍舊是1。

所以這就導致了對共享變數的快取一致性的問題,那麼為了解決這個問題,提出了快取一致性協議:當CPU在寫資料時,如果發現操作的是共享變數,它會通知其他CPU將它們內部的這個共享變數置為無效狀態,當其他CPU讀取快取中的共享變數時,發現這個變數是無效的,它會從新從主存中讀取最新的值。

在Java的多執行緒開發中,有三個重要概念:原子性、可見性、有序性。 原子性:一個或多個操作要麼都不執行,要麼都執行。 可見性:一個執行緒中對共享變數(類中的成員變數或靜態變數)的修改,在其他執行緒立即可見。 有序性:程式執行的順序按照程式碼的順序執行。 把一個變數宣告為volatile,其實就是保證了可見性和有序性。 可見性我上面已經說過了,在多執行緒開發中是很有必要的。這個有序性還是得說一下,為了執行的效率,有時候會發生指令重排,這在單執行緒中指令重排之後的輸出與我們的程式碼邏輯輸出還是一致的。但在多執行緒中就可能發生問題,volatile在一定程度上可以避免指令重排。

volatile的原理是在生成的彙編程式碼中多了一個lock字首指令,這個字首指令相當於一個記憶體屏障,這個記憶體屏障有3個作用:

  • 確保指令重排的時候不會把屏障後的指令排在屏障前,確保不會把屏障前的指令排在屏障後。

  • 修改快取中的共享變數後立即重新整理到主存中。

  • 當執行寫操作時會導致其他CPU中的快取無效。

網路相關面試題

http 狀態碼

http 與 https 的區別?https 是如何工作的?

http是超文字傳輸協議,而https可以簡單理解為安全的http協議。https通過在http協議下添加了一層ssl協議對資料進行加密從而保證了安全。https的作用主要有兩點:建立安全的資訊傳輸通道,保證資料傳輸安全;確認網站的真實性。

http與https的區別主要如下:

  • https需要到CA申請證書,很少免費,因而需要一定的費用

  • http是明文傳輸,安全性低;而https在http的基礎上通過ssl加密,安全性高

  • 二者的預設埠不一樣,http使用的預設埠是80;https使用的預設埠是443

https的工作流程

提到https的話首先要說到加密演算法,加密演算法分為兩類:對稱加密和非對稱加密。

對稱加密:加密和解密用的都是相同的祕鑰,優點是速度快,缺點是安全性低。常見的對稱加密演算法有DES、AES等等。

非對稱加密:非對稱加密有一個祕鑰對,分為公鑰和私鑰。一般來說,私鑰自己持有,公鑰可以公開給對方,優點是安全性比對稱加密高,缺點是資料傳輸效率比對稱加密低。採用公鑰加密的資訊只有對應的私鑰可以解密。常見的非對稱加密包括RSA等。

在正式的使用場景中一般都是對稱加密和非對稱加密結合使用,使用非對稱加密完成祕鑰的傳遞,然後使用對稱祕鑰進行資料加密和解密。二者結合既保證了安全性,又提高了資料傳輸效率。

https的具體流程如下:

  1. 客戶端(通常是瀏覽器)先向伺服器發出加密通訊的請求
  • 支援的協議版本,比如TLS 1.0版

  • 一個客戶端生成的隨機數 random1,稍後用於生成”對話金鑰”

  • 支援的加密方法,比如RSA公鑰加密

  • 支援的壓縮方法

  1. 伺服器收到請求,然後響應
  • 確認使用的加密通訊協議版本,比如TLS 1.0版本。如果瀏覽器與伺服器支援的版本不一致,伺服器關閉加密通訊

  • 一個伺服器生成的隨機數random2,稍後用於生成”對話金鑰”

  • 確認使用的加密方法,比如RSA公鑰加密

  • 伺服器證書

  1. 客戶端收到證書之後會首先會進行驗證
  • 首先驗證證書的安全性

  • 驗證通過之後,客戶端會生成一個隨機數pre-master secret,然後使用證書中的公鑰進行加密,然後傳遞給伺服器端

  1. 伺服器收到使用公鑰加密的內容,在伺服器端使用私鑰解密之後獲得隨機數pre-master secret,然後根據radom1、radom2、pre-master secret通過一定的演算法得出一個對稱加密的祕鑰,作為後面互動過程中使用對稱祕鑰。同時客戶端也會使用radom1、radom2、pre-master secret,和同樣的演算法生成對稱祕鑰。

  2. 然後再後續的互動中就使用上一步生成的對稱祕鑰對傳輸的內容進行加密和解密。

TCP三次握手流程

Android面試題

程序間通訊的方式有哪幾種

AIDL 、廣播、檔案、socket、管道

廣播靜態註冊和動態註冊的區別

  1. 動態註冊廣播不是常駐型廣播,也就是說廣播跟隨Activity的生命週期。注意在Activity結束前,移除廣播接收器。 靜態註冊是常駐型,也就是說當應用程式關閉後,如果有資訊廣播來,程式也會被系統呼叫自動執行。

  2. 當廣播為有序廣播時:優先順序高的先接收(不分靜態和動態)。同優先順序的廣播接收器,動態優先於靜態

  3. 同優先順序的同類廣播接收器,靜態:先掃描的優先於後掃描的,動態:先註冊的優先於後註冊的。

  4. 當廣播為預設廣播時:無視優先順序,動態廣播接收器優先於靜態廣播接收器。同優先順序的同類廣播接收器,靜態:先掃描的優先於後掃描的,動態:先註冊的優先於後冊的。

Android效能優化工具使用(這個問題建議配合Android中的效能優化)

Android中常用的效能優化工具包括這些:Android Studio自帶的Android Profiler、LeakCanary、BlockCanary

Android自帶的Android Profiler其實就很好用,Android Profiler可以檢測三個方面的效能問題:CPU、MEMORY、NETWORK。

LeakCanary是一個第三方的檢測記憶體洩漏的庫,我們的專案整合之後LeakCanary會自動檢測應用執行期間的記憶體洩漏,並將之輸出給我們。

BlockCanary也是一個第三方檢測UI卡頓的庫,專案整合後Block也會自動檢測應用執行期間的UI卡頓,並將之輸出給我們。

Android中的類載入器

PathClassLoader,只能載入系統中已經安裝過的apk DexClassLoader,可以載入jar/apk/dex,可以從SD卡中載入未安裝的apk

Android中的動畫有哪幾類,它們的特點和區別是什麼

Android中動畫大致分為3類:幀動畫、補間動畫(View Animation)、屬性動畫(Object Animation)。

  • 幀動畫:通過xml配置一組圖片,動態播放。很少會使用。

  • 補間動畫(View Animation):大致分為旋轉、透明、縮放、位移四類操作。很少會使用。

  • 屬性動畫(Object Animation):屬性動畫是現在使用的最多的一種動畫,它比補間動畫更加強大。屬性動畫大致分為兩種使用型別,分別是ViewPropertyAnimator和ObjectAnimator。前者適合一些通用的動畫,比如旋轉、位移、縮放和透明,使用方式也很簡單通過View.animate()即可得到ViewPropertyAnimator,之後進行相應的動畫操作即可。後者適合用於為我們的自定義控制元件新增動畫,當然首先我們應該在自定義View中新增相應的getXXX()和setXXX()相應屬性的getter和setter方法,這裡需要注意的是在setter方法內改變了自定義View中的屬性後要呼叫invalidate()來重新整理View的繪製。之後呼叫ObjectAnimator.of屬性型別()返回一個ObjectAnimator,呼叫start()方法啟動動畫即可。

補間動畫與屬性動畫的區別:

  • 補間動畫是父容器不斷的繪製view,看起來像移動了效果,其實view沒有變化,還在原地。

  • 是通過不斷改變view內部的屬性值,真正的改變view。

Handler機制

說到Handler,就不得不提與之密切相關的這幾個類:Message、MessageQueue,Looper。

  • Message。Message中有兩個成員變數值得關注:target和callback。target其實就是傳送訊息的Handler物件,callback是當呼叫handler.post(runnable)時傳入的Runnable型別的任務。post事件的本質也是建立了一個Message,將我們傳入的這個runnable賦值給建立的Message的callback這個成員變數。

  • MessageQueue。訊息佇列很明顯是存放訊息的佇列,值得關注的是MessageQueue中的next()方法,它會返回下一個待處理的訊息。

  • Looper。Looper訊息輪詢器其實是連線Handler和訊息佇列的核心。首先我們都知道,如果想要在一個執行緒中建立一個Handler,首先要通過Looper.prepare()建立Looper,之後還得呼叫Looper.loop()開啟輪詢。我們著重看一下這兩個方法。

prepare()。這個方法做了兩件事:首先通過ThreadLocal.get()獲取當前執行緒中的Looper,如果不為空,則會丟擲一個RunTimeException,意思是一個執行緒不能建立2個Looper。如果為null則執行下一步。第二步是建立了一個Looper,並通過ThreadLocal.set(looper)。將我們建立的Looper與當前執行緒繫結。這裡需要提一下的是訊息佇列的建立其實就發生在Looper的構造方法中。

loop()。這個方法開啟了整個事件機制的輪詢。它的本質是開啟了一個死迴圈,不斷的通過MessageQueue的next()方法獲取訊息。拿到訊息後會呼叫msg.target.dispatchMessage()來做處理。其實我們在說到Message的時候提到過,msg.target其實就是傳送這個訊息的handler。這句程式碼的本質就是呼叫handler的dispatchMessage()。

  • Handler。上面做了這麼多鋪墊,終於到了最重要的部分。Handler的分析著重在兩個部分:傳送訊息和處理訊息。

傳送訊息。其實發送訊息除了sendMessage之外還有sendMessageDelayed和post以及postDelayed等等不同的方式。但它們的本質都是呼叫了sendMessageAtTime。在sendMessageAtTime這個方法中呼叫了enqueueMessage。在enqueueMessage這個方法中做了兩件事:通過msg.target = this實現了訊息與當前handler的繫結。然後通過queue.enqueueMessage實現了訊息入隊。

處理訊息。訊息處理的核心其實就是dispatchMessage()這個方法。這個方法裡面的邏輯很簡單,先判斷msg.callback是否為null,如果不為空則執行這個runnable。如果為空則會執行我們的handleMessage方法。

Android效能優化

Android中的效能優化在我看來分為以下幾個方面:記憶體優化、佈局優化、網路優化、安裝包優化。

記憶體優化:下一個問題就是。

佈局優化:佈局優化的本質就是減少View的層級。常見的佈局優化方案如下

  • 在LinearLayout和RelativeLayout都可以完成佈局的情況下優先選擇RelativeLayout,可以減少View的層級

  • 將常用的佈局元件抽取出來使用 < include > 標籤

  • 通過 < ViewStub > 標籤來載入不常用的佈局

  • 使用 < Merge > 標籤來減少佈局的巢狀層次

網路優化:常見的網路優化方案如下

  • 儘量減少網路請求,能夠合併的就儘量合併

  • 避免DNS解析,根據域名查詢可能會耗費上百毫秒的時間,也可能存在DNS劫持的風險。可以根據業務需求採用增加動態更新IP的方式,或者在IP方式訪問失敗時切換到域名訪問方式。

  • 大量資料的載入採用分頁的方式

  • 網路資料傳輸採用GZIP壓縮

  • 加入網路資料的快取,避免頻繁請求網路

  • 上傳圖片時,在必要的時候壓縮圖片

安裝包優化:安裝包優化的核心就是減少apk的體積,常見的方案如下

  • 使用混淆,可以在一定程度上減少apk體積,但實際效果微乎其微

  • 減少應用中不必要的資原始檔,比如圖片,在不影響APP效果的情況下儘量壓縮圖片,有一定的效果

  • 在使用了SO庫的時候優先保留v7版本的SO庫,刪掉其他版本的SO庫。原因是在2018年,v7版本的SO庫可以滿足市面上絕大多數的要求,可能八九年前的手機滿足不了,但我們也沒必要去適配老掉牙的手機。實際開發中減少apk體積的效果是十分顯著的,如果你使用了很多SO庫,比方說一個版本的SO庫一共10M,那麼只保留v7版本,刪掉armeabi和v8版本的SO庫,一共可以減少20M的體積。

Android記憶體優化

Android的記憶體優化在我看來分為兩點:避免記憶體洩漏、擴大記憶體,其實就是開源節流。

其實記憶體洩漏的本質就是較長生命週期的物件引用了較短生命週期的物件。

常見的記憶體洩漏:

  • 單例模式導致的記憶體洩漏。最常見的例子就是建立這個單例物件需要傳入一個Context,這時候傳入了一個Activity型別的Context,由於單例物件的靜態屬性,導致它的生命週期是從單例類載入到應用程式結束為止,所以即使已經finish掉了傳入的Activity,由於我們的單例物件依然持有Activity的引用,所以導致了記憶體洩漏。解決辦法也很簡單,不要使用Activity型別的Context,使用Application型別的Context可以避免記憶體洩漏。

  • 靜態變數導致的記憶體洩漏。靜態變數是放在方法區中的,它的生命週期是從類載入到程式結束,可以看到靜態變數生命週期是非常久的。最常見的因靜態變數導致記憶體洩漏的例子是我們在Activity中建立了一個靜態變數,而這個靜態變數的建立需要傳入Activity的引用this。在這種情況下即使Activity呼叫了finish也會導致記憶體洩漏。原因就是因為這個靜態變數的生命週期幾乎和整個應用程式的生命週期一致,它一直持有Activity的引用,從而導致了記憶體洩漏。

  • 非靜態內部類導致的記憶體洩漏。非靜態內部類導致記憶體洩漏的原因是非靜態內部類持有外部類的引用,最常見的例子就是在Activity中使用Handler和Thread了。使用非靜態內部類建立的Handler和Thread在執行延時操作的時候會一直持有當前Activity的引用,如果在執行延時操作的時候就結束Activity,這樣就會導致記憶體洩漏。解決辦法有兩種:第一種是使用靜態內部類,在靜態內部類中使用弱引用呼叫Activity。第二種方法是在Activity的onDestroy中呼叫handler.removeCallbacksAndMessages來取消延時事件。

  • 使用資源未及時關閉導致的記憶體洩漏。常見的例子有:操作各種資料流未及時關閉,操作Bitmap未及時recycle等等。

  • 使用第三方庫未能及時解綁。有的三方庫提供了註冊和解綁的功能,最常見的就是EventBus了,我們都知道使用EventBus要在onCreate中註冊,在onDestroy中解綁。如果沒有解綁的話,EventBus其實是一個單例模式,他會一直持有Activity的引用,導致記憶體洩漏。同樣常見的還有RxJava,在使用Timer操作符做了一些延時操作後也要注意在onDestroy方法中呼叫disposable.dispose()來取消操作。

  • 屬性動畫導致的記憶體洩漏。常見的例子就是在屬性動畫執行的過程中退出了Activity,這時View物件依然持有Activity的引用從而導致了記憶體洩漏。解決辦法就是在onDestroy中呼叫動畫的cancel方法取消屬性動畫。

  • WebView導致的記憶體洩漏。WebView比較特殊,即使是呼叫了它的destroy方法,依然會導致記憶體洩漏。其實避免WebView導致記憶體洩漏的最好方法就是讓WebView所在的Activity處於另一個程序中,當這個Activity結束時殺死當前WebView所處的程序即可,我記得阿里釘釘的WebView就是另外開啟的一個程序,應該也是採用這種方法避免記憶體洩漏。

擴大記憶體,為什麼要擴大我們的記憶體呢?有時候我們實際開發中不可避免的要使用很多第三方商業的SDK,這些SDK其實有好有壞,大廠的SDK可能記憶體洩漏會少一些,但一些小廠的SDK質量也就不太靠譜一些。那應對這種我們無法改變的情況,最好的辦法就是擴大記憶體。

擴大記憶體通常有兩種方法:一個是在清單檔案中的Application下新增largeHeap=”true”這個屬性,另一個就是同一個應用開啟多個程序來擴大一個應用的總記憶體空間。第二種方法其實就很常見了,比方說我使用過個推的SDK,個推的Service其實就是處在另外一個單獨的程序中。

Android中的記憶體優化總的來說就是開源和節流,開源就是擴大記憶體,節流就是避免記憶體洩漏。

Binder機制

在Linux中,為了避免一個程序對其他程序的干擾,程序之間是相互獨立的。在一個程序中其實還分為使用者空間和核心空間。這裡的隔離分為兩個部分,程序間的隔離和程序內的隔離。

既然程序間存在隔離,那其實也是存在著互動。程序間通訊就是IPC,使用者空間和核心空間的通訊就是系統呼叫。

Linux為了保證獨立性和安全性,程序之間不能直接相互訪問,Android是基於Linux的,所以也是需要解決程序間通訊的問題。

其實Linux程序間通訊有很多方式,比如管道、socket等等。為什麼Android程序間通訊採用了Binder而不是Linux已有的方式,主要是有這麼兩點考慮:效能和安全

效能。在移動裝置上對效能要求是比較嚴苛的。Linux傳統的程序間通訊比如管道、socket等等程序間通訊是需要複製兩次資料,而Binder則只需要一次。所以Binder在效能上是優於傳統程序通訊的。

安全。傳統的Linux程序通訊是不包含通訊雙方的身份驗證的,這樣會導致一些安全性問題。而Binder機制自帶身份驗證,從而有效的提高了安全性。

Binder是基於CS架構的,有四個主要組成部分。

  • Client。客戶端程序。

  • Server。服務端程序。

  • ServiceManager。提供註冊、查詢和返回代理服務物件的功能。

  • Binder驅動。主要負責建立程序間的Binder連線,程序間的資料互動等等底層操作。

Binder機制主要的流程是這樣的:

  • 服務端通過Binder驅動在ServiceManager中註冊我們的服務。

  • 客戶端通過Binder驅動查詢在ServiceManager中註冊的服務。

  • ServiceManager通過Binder驅動返回服務端的代理物件。

  • 客戶端拿到服務端的代理物件後即可進行程序間通訊。

LruCache的原理

LruCache的核心原理就是對LinkedHashMap的有效利用,它的內部存在一個LinkedHashMap成員變數。值得我們關注的有四個方法:構造方法、get、put、trimToSize。

構造方法:在LruCache的構造方法中做了兩件事,設定了maxSize、建立了一個LinkedHashMap。這裡值得注意的是LruCache將LinkedHashMap的accessOrder設定為了true,accessOrder就是遍歷這個LinkedHashMap的輸出順序。true代表按照訪問順序輸出,false代表按新增順序輸出,因為通常都是按照新增順序輸出,所以accessOrder這個屬性預設是false,但我們的LruCache需要按訪問順序輸出,所以顯式的將accessOrder設定為true。

get方法:本質上是呼叫LinkedHashMap的get方法,由於我們將accessOrder設定為了true,所以每呼叫一次get方法,就會將我們訪問的當前元素放置到這個LinkedHashMap的尾部。

put方法:本質上也是呼叫了LinkedHashMap的put方法,由於LinkedHashMap的特性,每呼叫一次put方法,也會將新加入的元素放置到LinkedHashMap的尾部。新增之後會呼叫trimToSize方法來保證新增後的記憶體不超過maxSize。

trimToSize方法:trimToSize方法的內部其實是開啟了一個while(true)的死迴圈,不斷的從LinkedHashMap的首部刪除元素,直到刪除之後的記憶體小於maxSize之後使用break跳出迴圈。

其實到這裡我們可以總結一下,為什麼這個演算法叫 最近最少使用 演算法呢?原理很簡單,我們的每次put或者get都可以看做一次訪問,由於LinkedHashMap的特性,會將每次訪問到的元素放置到尾部。當我們的記憶體達到閾值後,會觸發trimToSize方法來刪除LinkedHashMap首部的元素,直到當前記憶體小於maxSize。為什麼刪除首部的元素,原因很明顯:我們最近經常訪問的元素都會放置到尾部,那首部的元素肯定就是 最近最少使用 的元素了,因此當記憶體不足時應當優先刪除這些元素。

DiskLruCache原理

設計一個圖片的非同步載入框架

設計一個圖片載入框架,肯定要用到圖片載入的三級快取的思想。三級快取分為記憶體快取、本地快取和網路快取。

記憶體快取:將Bitmap快取到記憶體中,執行速度快,但是記憶體容量小。 本地快取:將圖片快取到檔案中,速度較慢,但容量較大。 網路快取:從網路獲取圖片,速度受網路影響。

如果我們設計一個圖片載入框架,流程一定是這樣的:

  • 拿到圖片url後首先從記憶體中查詢BItmap,如果找到直接載入。

  • 記憶體中沒有找到,會從本地快取中查詢,如果本地快取可以找到,則直接載入。

  • 記憶體和本地都沒有找到,這時會從網路下載圖片,下載到後會載入圖片,並且將下載到的圖片放到記憶體快取和本地快取中。

上面是一些基本的概念,如果是具體的程式碼實現的話,大概需要這麼幾個方面的檔案:

  • 首先需要確定我們的記憶體快取,這裡一般用的都是LruCache。

  • 確定本地快取,通常用的是DiskLruCache,這裡需要注意的是圖片快取的檔名一般是url被MD5加密後的字串,為了避免檔名直接暴露圖片的url。

  • 記憶體快取和本地快取確定之後,需要我們建立一個新的類MemeryAndDiskCache,當然,名字隨便起,這個類包含了之前提到的LruCache和DiskLruCache。在MemeryAndDiskCache這個類中我們定義兩個方法,一個是getBitmap,另一個是putBitmap,對應著圖片的獲取和快取,內部的邏輯也很簡單。getBitmap中按記憶體、本地的優先順序去取BItmap,putBitmap中先快取記憶體,之後快取到本地。

  • 在快取策略類確定好之後,我們建立一個ImageLoader類,這個類必須包含兩個方法,一個是展示圖片displayImage(url,imageView),另一個是從網路獲取圖片downloadImage(url,imageView)。在展示圖片方法中首先要通過ImageView.setTag(url),將url和imageView進行繫結,這是為了避免在列表中載入網路圖片時會由於ImageView的複用導致的圖片錯位的bug。之後會從MemeryAndDiskCache中獲取快取,如果存在,直接載入;如果不存在,則呼叫從網路獲取圖片這個方法。從網路獲取圖片方法很多,這裡我一般都會使用OkHttp+Retrofit。當從網路中獲取到圖片之後,首先判斷一下imageView.getTag()與圖片的url是否一致,如果一致則載入圖片,如果不一致則不載入圖片,通過這樣的方式避免了列表中非同步載入圖片的錯位。同時在獲取到圖片之後會通過MemeryAndDiskCache來快取圖片。

Android中的事件分發機制

在我們的手指觸控到螢幕的時候,事件其實是通過 Activity -> ViewGroup -> View 這樣的流程到達最後響應我們觸控事件的View。

說到事件分發,必不可少的是這幾個方法:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent。接下來就按照 Activity -> ViewGroup -> View 的流程來大致說一下事件分發機制。

我們的手指觸控到螢幕的時候,會觸發一個Action_Down型別的事件,當前頁面的Activity會首先做出響應,也就是說會走到Activity的dispatchTouchEvent()方法內。在這個方法內部簡單來說是這麼一個邏輯:

  • 呼叫getWindow.superDispatchTouchEvent()。

  • 如果上一步返回true,直接返回true;否則就return自己的onTouchEvent()。 這個邏輯很好理解,getWindow().superDispatchTouchEvent()如果返回true代表當前事件已經被處理,無需呼叫自己的onTouchEvent;否則代表事件並沒有被處理,需要Activity自己處理,也就是呼叫自己的onTouchEvent。

getWindow()方法返回了一個Window型別的物件,這個我們都知道,在Android中,PhoneWindow是Window的唯一實現類。所以這句本質上是呼叫了PhoneWindow中的superDispatchTouchEvent()。

而在PhoneWindow的這個方法中實際呼叫了mDecor.superDispatchTouchEvent(event)。這個mDecor就是DecorView,它是FrameLayout的一個子類,在DecorView中的superDispatchTouchEvent()中呼叫的是super.dispatchTouchEvent()。到這裡就很明顯了,DecorView是一個FrameLayout的子類,FrameLayout是一個ViewGroup的子類,本質上呼叫的還是ViewGroup的dispatchTouchEvent()。

分析到這裡,我們的事件已經從Activity傳遞到了ViewGroup,接下來我們來分析下ViewGroup中的這幾個事件處理方法。

在ViewGroup中的dispatchTouchEvent()中的邏輯大致如下:

  • 通過onInterceptTouchEvent()判斷當前ViewGroup是否攔截事件,預設的ViewGroup都是不攔截的;

  • 如果攔截,則return自己的onTouchEvent();

  • 如果不攔截,則根據 child.dispatchTouchEvent()的返回值判斷。如果返回true,則return true;否則return自己的onTouchEvent(),在這裡實現了未處理事件的向上傳遞。

通常情況下ViewGroup的onInterceptTouchEvent()都返回false,也就是不攔截。這裡需要注意的是事件序列,比如Down事件、Move事件……Up事件,從Down到Up是一個完整的事件序列,對應著手指從按下到擡起這一系列的事件,如果ViewGroup攔截了Down事件,那麼後續事件都會交給這個ViewGroup的onTouchEvent。如果ViewGroup攔截的不是Down事件,那麼會給之前處理這個Down事件的View傳送一個Action_Cancel型別的事件,通知子View這個後續的事件序列已經被ViewGroup接管了,子View恢復之前的狀態即可。

這裡舉一個常見的例子:在一個Recyclerview鐘有很多的Button,我們首先按下了一個button,然後滑動一段距離再鬆開,這時候Recyclerview會跟著滑動,並不會觸發這個button的點選事件。這個例子中,當我們按下button時,這個button接收到了Action_Down事件,正常情況下後續的事件序列應該由這個button處理。但我們滑動了一段距離,這時Recyclerview察覺到這是一個滑動操作,攔截了這個事件序列,走了自身的onTouchEvent()方法,反映在螢幕上就是列表的滑動。而這時button仍然處於按下的狀態,所以在攔截的時候需要傳送一個Action_Cancel來通知button恢復之前狀態。

事件分發最終會走到View的dispatchTouchEvent()中。在View的dispatchTouchEvent()中沒有onInterceptTouchEvent(),這也很容易理解,View不是ViewGroup,不會包含其他子View,所以也不存在攔截不攔截這一說。忽略一些細節,View的dispatchTouchEvent()中直接return了自己的onTouchEvent()。如果onTouchEvent()返回true代表事件被處理,否則未處理的事件會向上傳遞,直到有View處理了事件或者一直沒有處理,最終到達了Activity的onTouchEvent()終止。

這裡經常有人問onTouch和onTouchEvent的區別。首先,這兩個方法都在View的dispatchTouchEvent()中,是這麼一個邏輯:

  • 如果touchListener不為null,並且這個View是enable的,而且onTouch返回的是true,滿足這三個條件時會直接return true,不會走onTouchEvent()方法。

  • 上面只要有一個條件不滿足,就會走到onTouchEvent()方法中。所以onTouch的順序是在onTouchEvent之前的。

View的繪製流程

檢視繪製的起點在ViewRootImpl類的performTraversals()方法,在這個方法內其實是按照順序依次呼叫了mView.measure()、mView.layout()、mView.draw()

View的繪製流程分為3步:測量、佈局、繪製,分別對應3個方法measure、layout、draw。

測量階段。measure方法會被父View呼叫,在measure方法中做一些優化和準備工作後會呼叫onMeasure方法進行實際的自我測量。onMeasure方法在View和ViewGroup做的事情是不一樣的:

  • View。View中的onMeasure方法會計算自己的尺寸並通過setMeasureDimension儲存。

  • ViewGroup。ViewGroup中的onMeasure方法會呼叫所有子View的measure方法進行自我測量並儲存。然後通過子View的尺寸和位置計算出自己的尺寸並儲存。

佈局階段。layout方法會被父View呼叫,layout方法會儲存父View傳進來的尺寸和位置,並呼叫onLayout進行實際的內部佈局。onLayout在View和ViewGroup中做的事情也是不一樣的:

  • View。因為View是沒有子View的,所以View的onLayout裡面什麼都不做。

  • ViewGroup。ViewGroup中的onLayout方法會呼叫所有子View的layout方法,把尺寸和位置傳給他們,讓他們完成自我的內部佈局。

繪製階段。draw方法會做一些排程工作,然後會呼叫onDraw方法進行View的自我繪製。draw方法的排程流程大致是這樣的:

  • 繪製背景。對應drawBackground(Canvas)方法。

  • 繪製主體。對應onDraw(Canvas)方法。

  • 繪製子View。對應dispatchDraw(Canvas)方法。

  • 繪製滑動相關和前景。對應onDrawForeground(Canvas)。

Android原始碼中常見的設計模式以及自己在開發中常用的設計模式

Android與js是如何互動的

在Android中,Android與js的互動分為兩個方面:Android呼叫js裡的方法、js呼叫Android中的方法。

Android調js。Android調js有兩種方法:

  • WebView.loadUrl(“javascript:js中的方法名”)。這種方法的優點是很簡潔,缺點是沒有返回值,如果需要拿到js方法的返回值則需要js呼叫Android中的方法來拿到這個返回值。

  • WebView.evaluateJavaScript(“javascript:js中的方法名”,ValueCallback)。這種方法比loadUrl好的是可以通過ValueCallback這個回撥拿到js方法的返回值。缺點是這個方法Android4.4才有,相容性較差。不過放在2018年來說,市面上絕大多數App都要求最低版本是4.4了,所以我認為這個相容性問題不大。

js調Android。js調Android有三種方法:

熱修復原理

Activity啟動過程

SparseArray原理

SparseArray,通常來講是Android中用來替代HashMap的一個數據結構。 準確來講,是用來替換key為Integer型別,value為Object型別的HashMap。需要注意的是SparseArray僅僅實現了Cloneable介面,所以不能用Map來宣告。 從內部結構來講,SparseArray內部由兩個陣列組成,一個是int[]型別的mKeys,用來存放所有的鍵;另一個是Object[]型別的mValues,用來存放所有的值。 最常見的是拿SparseArray跟HashMap來做對比,由於SparseArray內部組成是兩個陣列,所以佔用記憶體比HashMap要小。我們都知道,增刪改查等操作都首先需要找到相應的鍵值對,而SparseArray內部是通過二分查詢來定址的,效率很明顯要低於HashMap的常數級別的時間複雜度。提到二分查詢,這裡還需要提一下的是二分查詢的前提是陣列已經是排好序的,沒錯,SparseArray中就是按照key進行升序排列的。 綜合起來來說,SparseArray所佔空間優於HashMap,而效率低於HashMap,是典型的時間換空間,適合較小容量的儲存。 從原始碼角度來說,我認為需要注意的是SparseArray的remove()、put()和gc()方法。

  • remove()。SparseArray的remove()方法並不是直接刪除之後再壓縮陣列,而是將要刪除的value設定為DELETE這個SparseArray的靜態屬性,這個DELETE其實就是一個Object物件,同時會將SparseArray中的mGarbage這個屬性設定為true,這個屬性是便於在合適的時候呼叫自身的gc()方法壓縮陣列來避免浪費空間。這樣可以提高效率,如果將來要新增的key等於刪除的key,那麼會將要新增的value覆蓋DELETE。

  • gc()。SparseArray中的gc()方法跟JVM的GC其實完全沒有任何關係。gc()方法的內部實際上就是一個for迴圈,將value不為DELETE的鍵值對往前移動覆蓋value為DELETE的鍵值對來實現陣列的壓縮,同時將mGarbage置為false,避免記憶體的浪費。

  • put()。put方法是這麼一個邏輯,如果通過二分查詢在mKeys陣列中找到了key,那麼直接覆蓋value即可。如果沒有找到,會拿到與陣列中與要新增的key最接近的key索引,如果這個索引對應的value為DELETE,則直接把新的value覆蓋DELETE即可,在這裡可以避免陣列元素的移動,從而提高了效率。如果value不為DELETE,會判斷mGarbage,如果為true,則會呼叫gc()方法壓縮陣列,之後會找到合適的索引,將索引之後的鍵值對後移,插入新的鍵值對,這個過程中可能會觸發陣列的擴容。

圖片載入如何避免OOM

我們知道記憶體中的Bitmap大小的計算公式是:長所佔畫素 *寬所佔畫素 *每個畫素所佔記憶體。想避免OOM有兩種方法:等比例縮小長寬、減少每個畫素所佔的記憶體。

  • 等比縮小長寬。我們知道Bitmap的建立是通過BitmapFactory的工廠方法,decodeFile()、decodeStream()、decodeByteArray()、decodeResource()。這些方法中都有一個Options型別的引數,這個Options是BitmapFactory的內部類,儲存著BItmap的一些資訊。Options中有一個屬性:inSampleSize。我們通過修改inSampleSize可以縮小圖片的長寬,從而減少BItmap所佔記憶體。需要注意的是這個inSampleSize大小需要是2的冪次方,如果小於1,程式碼會強制讓inSampleSize為1。

  • 減少畫素所佔記憶體。Options中有一個屬性inPreferredConfig,預設是ARGB_8888,代表每個畫素所佔尺寸。我們可以通過將之修改為RGB_565或者ARGB_4444來減少一半記憶體。

大圖載入

載入高清大圖,比如清明上河圖,首先螢幕是顯示不下的,而且考慮到記憶體情況,也不可能一次性全部載入到記憶體。這時候就需要區域性載入了,Android中有一個負責區域性載入的類:BitmapRegionDecoder。使用方法很簡單,通過BitmapRegionDecoder.newInstance()建立物件,之後呼叫decodeRegion(Rect rect, BitmapFactory.Options options)即可。第一個引數rect是要顯示的區域,第二個引數是BitmapFactory中的內部類Options。

Android三方庫的原始碼分析

由於原始碼分析篇幅太大,所以這裡之貼出我的原始碼分析的連結(掘金)。

OkHttp

OkHttp原始碼分析

Retrofit

Retrofit原始碼分析1 Retrofit原始碼分析2 Retrofit原始碼分析3

RxJava

RxJava原始碼分析

Glide

Glide原始碼分析

EventBus

EventBus原始碼分析

大致是這麼一個流程: register:

  • 獲取訂閱者的Class物件

  • 使用反射查詢訂閱者中的事件處理方法集合

  • 遍歷事件處理方法集合,呼叫subscribe(subscriber,subscriberMethod)方法,在subscribe方法內:

  • 如果事件繼承性為true,遍歷這個Map型別的stickEvents,通過isAssignableFrom方法判斷當前事件是否是遍歷事件的父類,如果是則傳送事件

  • 如果事件繼承性為false,通過stickyEvents.get(eventType)獲取事件併發送

  • 如果事件型別集合為空則建立一個新的集合,這一步目的是延遲集合的初始化

  • 拿到事件型別集合後將新的事件型別加入到集合中

  • 如果Subscription集合為空則建立一個新的集合,這一步目的是延遲集合的初始化

  • 拿到Subscription集合後遍歷這個集合,通過比較事件處理的優先順序,將新的Subscription物件加入合適的位置

  • 通過subscriberMethod獲取處理的事件型別eventType

  • 將訂閱者subscriber和方法subscriberMethod綁在一起形成一個Subscription物件

  • 通過subscriptionsByEventType.get(eventType)獲取Subscription集合

  • 通過typesBySubscriber.get(subscriber)獲取事件型別集合

  • 判斷當前事件型別是否是sticky

  • 如果當前事件型別不是sticky(粘性事件),subscribe(subscriber,subscriberMethod)到此終結

  • 如果是sticky,判斷EventBus中的一個事件繼承性的屬性,預設是true

post:

  • postSticky

  • 將事件加入到stickyEvents這個Map型別的集合中

  • 呼叫post方法

  • post

  • 事件繼承性為true,找到當前事件所有的父型別並呼叫postSingleEventForEventType方法傳送事件

  • 事件繼承性為false,只發送當前事件型別的事件

  • 在postToSubscription中分為四種情況

  • POSTING,呼叫invokeSubscriber(subscription, event)處理事件,本質是method.invoke()反射

  • MAIN,如果在主執行緒直接invokeSubscriber處理;反之通過handler切換到主執行緒呼叫invokeSubscriber處理事件

  • BACKGROUND,如果不在主執行緒直接invokeSubscriber處理事件;反之開啟一條執行緒,線上程中呼叫invokeSubscriber處理事件

  • ASYNC,開啟一條執行緒,線上程中呼叫invokeSubscriber處理事件

  • 在postSingleEventForEventType中,通過subscriptionsByEventType.get(eventClass)獲取Subscription型別集合

  • 遍歷這個集合,呼叫postToSubscription傳送事件

  • 將事件加入當前執行緒的事件佇列中

  • 通過while迴圈不斷從事件佇列中取出事件並呼叫postSingleEvent方法傳送事件

  • 在postSingleEvent中,判斷事件繼承性,預設為true

unregister:

  • 刪除subscriptionsByEventType中與訂閱者相關的所有subscription

  • 刪除typesBySubscriber中與訂閱者相關的所有型別

資料結構與演算法

手寫快排

手寫歸併排序

手寫堆以及堆排序

說一下排序演算法的區別(時間複雜度和空間複雜度)

1.Activity的啟動過程(不要回答生命週期)

http://blog.csdn.net/luoshengyang/article/details/6689748

1.2.Activity的啟動模式以及使用場景

(1)manifest設定,(2)startActivity flag http://blog.csdn.net/CodeEmperor/article/details/50481726 此處延伸:棧(First In Last Out)與佇列(First In First Out)的區別

3.Service的兩種啟動方式

(1)startService(),(2)bindService() http://www.jianshu.com/p/2fb6eb14fdec

4.Broadcast註冊方式與區別

(1)靜態註冊(minifest),(2)動態註冊 http://www.jianshu.com/p/ea5e233d9f43 此處延伸:什麼情況下用動態註冊

5.HttpClient與HttpUrlConnection的區別

http://blog.csdn.net/guolin_blog/article/details/12452307 此處延伸:Volley裡用的哪種請求方式(2.3前HttpClient,2.3後HttpUrlConnection)

6.http與https的區別

http://blog.csdn.net/whatday/article/details/38147103 此處延伸:https的實現原理

7.手寫演算法(選擇冒泡必須要會)

http://www.jianshu.com/p/ae97c3ceea8d

8.程序保活(不死程序)

http://www.jianshu.com/p/63aafe3c12af 此處延伸:程序的優先順序是什麼(下面這篇文章,都有說) https://segmentfault.com/a/1190000006251859

9.程序間通訊的方式

(1)AIDL,(2)廣播,(3)Messenger AIDL : https://www.jianshu.com/p/a8e43ad5d7d2 https://www.jianshu.com/p/0cca211df63c Messenger : http://blog.csdn.net/lmj623565791/article/details/47017485 此處延伸:簡述Binder , http://blog.csdn.net/luoshengyang/article/details/6618363/

10.載入大圖

PS:有家小公司(規模寫假的,給騙過去了),直接把專案給我看,讓我說實現原理。。 最讓我無語的一次面試,就一個點問的我底褲都快穿了,就差幫他們寫程式碼了。。 http://blog.csdn.net/lmj623565791/article/details/49300989

11.三級快取(各大圖片框架都可以扯到這上面來)

(1)記憶體快取,(2)本地快取,(3)網路 記憶體:http://blog.csdn.net/guolin_blog/article/details/9526203 本地:http://blog.csdn.net/guolin_blog/article/details/28863651

12.MVP框架(必問)

http://blog.csdn.net/lmj623565791/article/details/46596109 此處延伸:手寫mvp例子,與mvc之間的區別,mvp的優勢

13.講解一下Context

http://blog.csdn.net/lmj623565791/article/details/40481055

14.JNI

http://www.jianshu.com/p/aba734d5b5cd 此處延伸:專案中使用JNI的地方,如:核心邏輯,金鑰,加密邏輯

15.java虛擬機器和Dalvik虛擬機器的區別

http://www.jianshu.com/p/923aebd31b65

16.執行緒sleep和wait有什麼區別

http://blog.csdn.net/liuzhenwen/article/details/4202967

17.View,ViewGroup事件分發

http://blog.csdn.net/guolin_blog/article/details/9097463 http://blog.csdn.net/guolin_blog/article/details/9153747

18.儲存Activity狀態

onSaveInstanceState() http://blog.csdn.net/yuzhiboyi/article/details/7677026

19.WebView與js互動(呼叫哪些API)

http://blog.csdn.net/cappuccinolau/article/details/8262821/

20.記憶體洩露檢測,記憶體效能優化

http://blog.csdn.net/guolin_blog/article/details/42238627

21.佈局優化

http://blog.csdn.net/guolin_blog/article/details/43376527

22.自定義view和動畫

以下兩個講解都講得很透徹,這部分面試官多數不會問很深,要麼就給你一個效果讓你講原理。 (1)http://www.gcssloop.com/customview/CustomViewIndex (2)http://blog.csdn.net/yanbober/article/details/50577855

總結

工作中解決了什麼難題,做了什麼有成就感的專案(這個問題一定會問到,所以肯定要做準備)

這個些問題其實還是靠平時的積累,對我來說的話,最有成就感的就是開發了KCommon這個專案,它大大提升了我的開發效率。

參考 https://www.jianshu.com/p/564b3920697a

閱讀更多

除程式設計師,除了寫好程式碼,你更應該學會這些!

技術精華總結,說說我上半年都幹了什麼

一招教你打造一個滑動置頂的視覺特效

NDK專案實戰—高仿360手機助手之解除安裝監聽

如果您覺得不錯,轉發是對我很大的支援!

在這裡獲得的不僅僅是技術!

image