2019 Android 高階面試題總結

codeGoogle發表於2019-03-12

說下你所知道的設計模式與使用場景

a.建造者模式:

將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示。

使用場景比如最常見的AlertDialog,拿我們開發過程中舉例,比如Camera開發過程中,可能需要設定一個初始化的相機配置,設定攝像頭方向,閃光燈開閉,成像質量等等,這種場景下就可以使用建造者模式

裝飾者模式:動態的給一個物件新增一些額外的職責,就增加功能來說,裝飾模式比生成子類更為靈活。裝飾者模式可以在不改變原有類結構的情況下曾強類的功能,比如Java中的BufferedInputStream 包裝FileInputStream,舉個開發中的例子,比如在我們現有網路框架上需要增加新的功能,那麼再包裝一層即可,裝飾者模式解決了繼承存在的一些問題,比如多層繼承程式碼的臃腫,使程式碼邏輯更清晰

觀察者模式: 代理模式: 門面模式: 單例模式: 生產者消費者模式:

java語言的特點與OOP思想

這個通過對比來描述,比如物件導向和麵向過程的對比,針對這兩種思想的對比,還可以舉個開發中的例子,比如播放器的實現,程式導向的實現方式就是將播放視訊的這個功能分解成多個過程,比如,載入視訊地址,獲取視訊資訊,初始化解碼器,選擇合適的解碼器進行解碼,讀取解碼後的幀進行視訊格式轉換和音訊重取樣,然後讀取幀進行播放,這是一個完整的過程,這個過程中不涉及類的概念,而物件導向最大的特點就是類,封裝繼承和多型是核心,同樣的以播放器為例,一物件導向的方式來實現,將會針對每一個功能封裝出一個物件,吧如說Muxer,獲取視訊資訊,Decoder,解碼,格式轉換器,視訊播放器,音訊播放器等,每一個功能對應一個物件,由這個物件來完成對應的功能,並且遵循單一職責原則,一個物件只做它相關的事情

說下java中的執行緒建立方式,執行緒池的工作原理。

java中有三種建立執行緒的方式,或者說四種

1.繼承Thread類實現多執行緒 2.實現Runnable介面 3.實現Callable介面 4.通過執行緒池

執行緒池的工作原理:執行緒池可以減少建立和銷燬執行緒的次數,從而減少系統資源的消耗,當一個任務提交到執行緒池時

a. 首先判斷核心執行緒池中的執行緒是否已經滿了,如果沒滿,則建立一個核心執行緒執行任務,否則進入下一步

b. 判斷工作佇列是否已滿,沒有滿則加入工作佇列,否則執行下一步

c. 判斷執行緒數是否達到了最大值,如果不是,則建立非核心執行緒執行 任務,否則執行飽和策略,預設丟擲異常

說下 handler 原理

Handler,Message,looper 和 MessageQueue 構成了安卓的訊息機制,handler建立後可以通過 sendMessage 將訊息加入訊息佇列,然後 looper不斷的將訊息從 MessageQueue 中取出來,回撥到 Hander 的 handleMessage方法,從而實現執行緒的通訊。

從兩種情況來說,第一在UI執行緒建立Handler,此時我們不需要手動開啟looper,因為在應用啟動時,在ActivityThread的main方法中就建立了一個當前主執行緒的looper,並開啟了訊息佇列,訊息佇列是一個無限迴圈,為什麼無限迴圈不會ANR?因為可以說,應用的整個生命週期就是執行在這個訊息迴圈中的,安卓是由事件驅動的,Looper.loop不斷的接收處理事件,每一個點選觸控或者Activity每一個生命週期都是在Looper.loop的控制之下的,looper.loop一旦結束,應用程式的生命週期也就結束了。我們可以想想什麼情況下會發生ANR,第一,事件沒有得到處理,第二,事件正在處理,但是沒有及時完成,而對事件進行處理的就是looper,所以只能說事件的處理如果阻塞會導致ANR,而不能說looper的無限迴圈會ANR

另一種情況就是在子執行緒建立Handler,此時由於這個執行緒中沒有預設開啟的訊息佇列,所以我們需要手動呼叫looper.prepare(),並通過looper.loop開啟訊息

主執行緒Looper從訊息佇列讀取訊息,當讀完所有訊息時,主執行緒阻塞。子執行緒往訊息佇列傳送訊息,並且往管道檔案寫資料,主執行緒即被喚醒,從管道檔案讀取資料,主執行緒被喚醒只是為了讀取訊息,當訊息讀取完畢,再次睡眠。因此loop的迴圈並不會對CPU效能有過多的消耗。

記憶體洩漏的場景和解決辦法

1.非靜態內部類的靜態例項

非靜態內部類會持有外部類的引用,如果非靜態內部類的例項是靜態的,就會長期的維持著外部類的引用,組織被系統回收,解決辦法是使用靜態內部類

2.多執行緒相關的匿名內部類和非靜態內部類

匿名內部類同樣會持有外部類的引用,如果線上程中執行耗時操作就有可能發生記憶體洩漏,導致外部類無法被回收,直到耗時任務結束,解決辦法是在頁面退出時結束執行緒中的任務

3.Handler記憶體洩漏

Handler導致的記憶體洩漏也可以被歸納為非靜態內部類導致的,Handler內部message是被儲存在MessageQueue中的,有些message不能馬上被處理,存在的時間會很長,導致handler無法被回收,如果handler是非靜態的,就會導致它的外部類無法被回收,解決辦法是1.使用靜態handler,外部類引用使用弱引用處理2.在退出頁面時移除訊息佇列中的訊息

4.Context導致記憶體洩漏

根據場景確定使用Activity的Context還是Application的Context,因為二者生命週期不同,對於不必須使用Activity的Context的場景(Dialog),一律採用Application的Context,單例模式是最常見的發生此洩漏的場景,比如傳入一個Activity的Context被靜態類引用,導致無法回收

5.靜態View導致洩漏

使用靜態View可以避免每次啟動Activity都去讀取並渲染View,但是靜態View會持有Activity的引用,導致無法回收,解決辦法是在Activity銷燬的時候將靜態View設定為null(View一旦被載入到介面中將會持有一個Context物件的引用,在這個例子中,這個context物件是我們的Activity,宣告一個靜態變數引用這個View,也就引用了activity)

6.WebView導致的記憶體洩漏

WebView只要使用一次,記憶體就不會被釋放,所以WebView都存在記憶體洩漏的問題,通常的解決辦法是為WebView單開一個程式,使用AIDL進行通訊,根據業務需求在合適的時機釋放掉

7.資源物件未關閉導致

如Cursor,File等,內部往往都使用了緩衝,會造成記憶體洩漏,一定要確保關閉它並將引用置為null

8.集合中的物件未清理

集合用於儲存物件,如果集合越來越大,不進行合理的清理,尤其是入股集合是靜態的

9.Bitmap導致記憶體洩漏

bitmap是比較佔記憶體的,所以一定要在不使用的時候及時進行清理,避免靜態變數持有大的bitmap物件

10.監聽器未關閉

很多需要register和unregister的系統服務要在合適的時候進行unregister,手動新增的listener也需要及時移除

如何避免OOM?

1.使用更加輕量的資料結構:如使用ArrayMap/SparseArray替代HashMap,HashMap更耗記憶體,因為它需要額外的例項物件來記錄Mapping操作,SparseArray更加高效,因為它避免了Key Value的自動裝箱,和裝箱後的解箱操作

2.便面列舉的使用,可以用靜態常量或者註解@IntDef替代

3.Bitmap優化:

a.尺寸壓縮:通過InSampleSize設定合適的縮放 b.顏色質量:設定合適的format,ARGB_6666/RBG_545/ARGB_4444/ALPHA_6,存在很大差異 c.inBitmap:使用inBitmap屬性可以告知Bitmap解碼器去嘗試使用已經存在的記憶體區域,新解碼的Bitmap會嘗試去使用之前那張Bitmap在Heap中所佔據的pixel data記憶體區域,而不是去問記憶體重新申請一塊區域來存放Bitmap。利用這種特性,即使是上千張的圖片,也只會僅僅只需要佔用螢幕所能夠顯示的圖片數量的記憶體大小,但複用存在一些限制,具體體現在:在Android 4.4之前只能重用相同大小的Bitmap的記憶體,而Android 4.4及以後版本則只要後來的Bitmap比之前的小即可。使用inBitmap引數前,每建立一個Bitmap物件都會分配一塊記憶體供其使用,而使用了inBitmap引數後,多個Bitmap可以複用一塊記憶體,這樣可以提高效能

4.StringBuilder替代String: 在有些時候,程式碼中會需要使用到大量的字串拼接的操作,這種時候有必要考慮使用StringBuilder來替代頻繁的“+”

5.避免在類似onDraw這樣的方法中建立物件,因為它會迅速佔用大量記憶體,引起頻繁的GC甚至記憶體抖動

6.減少記憶體洩漏也是一種避免OOM的方法

說下 Activity 的啟動模式,生命週期,兩個 Activity 跳轉的生命週期,如果一個 Activity 跳轉另一個 Activity 再按下 Home 鍵在回到 Activity 的生命週期是什麼樣的

啟動模式

Standard 模式:Activity 可以有多個例項,每次啟動 Activity,無論任務棧中是否已經有這個Activity的例項,系統都會建立一個新的Activity例項

SingleTop模式:當一個singleTop模式的Activity已經位於任務棧的棧頂,再去啟動它時,不會再建立新的例項,如果不位於棧頂,就會建立新的例項

SingleTask模式:如果Activity已經位於棧頂,系統不會建立新的Activity例項,和singleTop模式一樣。但Activity已經存在但不位於棧頂時,系統就會把該Activity移到棧頂,並把它上面的activity出棧

SingleInstance模式:singleInstance 模式也是單例的,但和singleTask不同,singleTask 只是任務棧內單例,系統裡是可以有多個singleTask Activity例項的,而 singleInstance Activity 在整個系統裡只有一個例項,啟動一singleInstanceActivity 時,系統會建立一個新的任務棧,並且這個任務棧只有他一個Activity

生命週期

onCreate onStart onResume onPause onStop onDestroy 兩個 Activity 跳轉的生命週期

1.啟動A onCreate - onStart - onResume

2.在A中啟動B ActivityA onPause ActivityB onCreate ActivityB onStart ActivityB onResume ActivityA onStop

3.從B中返回A(按物理硬體返回鍵) ActivityB onPause ActivityA onRestart ActivityA onStart ActivityA onResume ActivityB onStop ActivityB onDestroy

4.繼續返回

ActivityA onPause ActivityA onStop ActivityA onDestroy

onRestart 的呼叫場景

(1)按下home鍵之後,然後切換回來,會呼叫onRestart()。 (2)從本Activity跳轉到另一個Activity之後,按back鍵返回原來Activity,會呼叫onRestart(); (3)從本Activity切換到其他的應用,然後再從其他應用切換回來,會呼叫onRestart();

說下 Activity 的橫豎屏的切換的生命週期,用那個方法來儲存資料,兩者的區別。觸發在什麼時候在那個方法裡可以獲取資料等。
是否瞭解SurfaceView,它是什麼?他的繼承方式是什麼?他與View的區別(從原始碼角度,如載入,繪製等)。

SurfaceView中採用了雙緩衝機制,保證了UI介面的流暢性,同時 SurfaceView 不在主執行緒中繪製,而是另開闢一個執行緒去繪製,所以它不妨礙UI執行緒;

SurfaceView 繼承於View,他和View主要有以下三點區別:

(1)View底層沒有雙緩衝機制,SurfaceView有;

(2)view主要適用於主動更新,而SurfaceView適用與被動的更新,如頻繁的重新整理

(3)view會在主執行緒中去更新UI,而SurfaceView則在子執行緒中重新整理;

SurfaceView的內容不在應用視窗上,所以不能使用變換(平移、縮放、旋轉等)。也難以放在ListView或者ScrollView中,不能使用UI控制元件的一些特性比如View.setAlpha()

View:顯示檢視,內建畫布,提供圖形繪製函式、觸屏事件、按鍵事件函式等;必須在UI主執行緒內更新畫面,速度較慢。

SurfaceView:基於view檢視進行擴充的檢視類,更適合2D遊戲的開發;是view的子類,類似使用雙緩機制,在新的執行緒中更新畫面所以重新整理介面速度比view快,Camera預覽介面使用SurfaceView。

GLSurfaceView:基於SurfaceView檢視再次進行擴充的檢視類,專用於3D遊戲開發的檢視;是SurfaceView的子類,openGL專用。 如何實現程式保活

a: Service 設定成 START_STICKY kill 後會被重啟(等待5秒左右),重傳Intent,保持與重啟前一樣

b: 通過 startForeground將程式設定為前臺程式, 做前臺服務,優先順序和前臺應用一個級別,除非在系統記憶體非常缺,否則此程式不會被 kill

c: 雙程式Service: 讓2個程式互相保護對方,其中一個Service被清理後,另外沒被清理的程式可以立即重啟程式

d: 用C編寫守護程式(即子程式) : Android系統中當前程式(Process)fork出來的子程式,被系統認為是兩個不同的程式。當父程式被殺死的時候,子程式仍然可以存活,並不受影響(Android5.0以上的版本不可行)聯絡廠商,加入白名單

e.鎖屏狀態下,開啟一個一畫素Activity 說下冷啟動與熱啟動是什麼,區別,如何優化,使用場景等。 app冷啟動: 當應用啟動時,後臺沒有該應用的程式,這時系統會重新建立一個新的程式分配給該應用, 這個啟動方式就叫做冷啟動(後臺不存在該應用程式)。冷啟動因為系統會重新建立一個新的程式分配給它,所以會先建立和初始化Application類,再建立和初始化MainActivity類(包括一系列的測量、佈局、繪製),最後顯示在介面上。

app熱啟動: 當應用已經被開啟, 但是被按下返回鍵、Home鍵等按鍵時回到桌面或者是其他程式的時候,再重新開啟該app時, 這個方式叫做熱啟動(後臺已經存在該應用程式)。熱啟動因為會從已有的程式中來啟動,所以熱啟動就不會走Application這步了,而是直接走MainActivity(包括一系列的測量、佈局、繪製),所以熱啟動的過程只需要建立和初始化一個MainActivity就行了,而不必建立和初始化Application

冷啟動的流程

當點選app的啟動圖示時,安卓系統會從Zygote程式中fork建立出一個新的程式分配給該應用,之後會依次建立和初始化Application類、建立MainActivity類、載入主題樣式Theme中的windowBackground等屬性設定給MainActivity以及配置Activity層級上的一些屬性、再inflate佈局、當onCreate/onStart/onResume方法都走完了後最後才進行contentView的measure/layout/draw顯示在介面上

冷啟動的生命週期簡要流程:

Application構造方法 –> attachBaseContext()–>onCreate –>Activity構造方法 –> onCreate() –> 配置主體中的背景等操作 –>onStart() –> onResume() –> 測量、佈局、繪製顯示

冷啟動的優化主要是視覺上的優化,解決白屏問題,提高使用者體驗,所以通過上面冷啟動的過程。能做的優化如下:

  • 減少 onCreate()方法的工作量
  • 不要讓 Application 參與業務的操作
  • 不要在 Application 進行耗時操作
  • 不要以靜態變數的方式在 Application 儲存資料
  • 減少佈局的複雜度和層級
  • 減少主執行緒耗時

為什麼冷啟動會有白屏黑屏問題?原因在於載入主題樣式Theme中的windowBackground等屬性設定給MainActivity發生在inflate佈局當onCreate/onStart/onResume方法之前,而windowBackground背景被設定成了白色或者黑色,所以我們進入app的第一個介面的時候會造成先白屏或黑屏一下再進入介面。解決思路如下

1.給他設定 windowBackground 背景跟啟動頁的背景相同,如果你的啟動頁是張圖片那麼可以直接給 windowBackground 這個屬性設定該圖片那麼就不會有一閃的效果了

<style name=``"Splash_Theme"` `parent=``"@android:style/Theme.NoTitleBar"``>`
    <item name=``"android:windowBackground"``>@drawable/splash_bg</item>`
    <item name=``"android:windowNoTitle"``>``true``</item>`
</style>`
複製程式碼

2.採用世面的處理方法,設定背景是透明的,給人一種延遲啟動的感覺。,將背景顏色設定為透明色,這樣當使用者點選桌面APP圖片的時候,並不會"立即"進入APP,而且在桌面上停留一會,其實這時候APP已經是啟動的了,只是我們心機的把Theme裡的windowBackground 的顏色設定成透明的,強行把鍋甩給了手機應用廠商(手機反應太慢了啦)

<style name=``"Splash_Theme"` `parent=``"@android:style/Theme.NoTitleBar"``>`
    <item name=``"android:windowIsTranslucent"``>``true``</item>`
    <item name=``"android:windowNoTitle"``>``true``</item>`
</style>`
複製程式碼

3.以上兩種方法是在視覺上顯得更快,但其實只是一種表象,讓應用啟動的更快,有一種思路,將 Application 中的不必要的初始化動作實現懶載入,比如,在SpashActivity 顯示後再傳送訊息到 Application,去初始化,這樣可以將初始化的動作放在後邊,縮短應用啟動到使用者看到介面的時間

Android 中的執行緒有那些,原理與各自特點

AsyncTask,HandlerThread,IntentService

AsyncTask原理:內部是Handler和兩個執行緒池實現的,Handler用於將執行緒切換到主執行緒,兩個執行緒池一個用於任務的排隊,一個用於執行任務,當AsyncTask執行execute方法時會封裝出一個FutureTask物件,將這個物件加入佇列中,如果此時沒有正在執行的任務,就執行它,執行完成之後繼續執行佇列中下一個任務,執行完成通過Handler將事件傳送到主執行緒。AsyncTask必須在主執行緒初始化,因為內部的Handler是一個靜態物件,在AsyncTask類載入的時候他就已經被初始化了。在Android3.0開始,execute方法序列執行任務的,一個一個來,3.0之前是並行執行的。如果要在3.0上執行並行任務,可以呼叫executeOnExecutor方法

HandlerThread原理:繼承自 Thread,start開啟執行緒後,會在其run方法中會通過Looper 建立訊息佇列並開啟訊息迴圈,這個訊息佇列執行在子執行緒中,所以可以將HandlerThread 中的 Looper 例項傳遞給一個 Handler,從而保證這個 Handler 的 handleMessage 方法執行在子執行緒中,Android 中使用 HandlerThread的一個場景就是 IntentService IntentService原理:繼承自Service,它的內部封裝了 HandlerThread 和Handler,可以執行耗時任務,同時因為它是一個服務,優先順序比普通執行緒高很多,所以更適合執行一些高優先順序的後臺任務

HandlerThread底層通過Looper訊息佇列實現的,所以它是順序的執行每一個任務。可以通過Intent的方式開啟IntentService,IntentService通過handler將每一個intent加入HandlerThread子執行緒中的訊息佇列,通過looper按順序一個個的取出並執行,執行完成後自動結束自己,不需要開發者手動關閉

ANR的原因

1.耗時的網路訪問 2.大量的資料讀寫 3.資料庫操作 4.硬體操作(比如camera) 5.呼叫thread的join()方法、sleep()方法、wait()方法或者等待執行緒鎖的時候 6.service binder的數量達到上限 7.system server中發生WatchDog ANR 8.service忙導致超時無響應 9.其他執行緒持有鎖,導致主執行緒等待超時 10.其它執行緒終止或崩潰導致主執行緒一直等待

三級快取原理

當 Android 端需要獲得資料時比如獲取網路中的圖片,首先從記憶體中查詢(按鍵查詢),記憶體中沒有的再從磁碟檔案或sqlite中去查詢,若磁碟中也沒有才通過網路獲取

LruCache 底層實現原理:

LruCache 中 Lru 演算法的實現就是通過 LinkedHashMap 來實現的。LinkedHashMap 繼承於 HashMap,它使用了一個雙向連結串列來儲存 Map中的Entry順序關係

對於get、put、remove等操作,LinkedHashMap除了要做HashMap做的事情,還做些調整Entry順序連結串列的工作。

LruCache中將LinkedHashMap的順序設定為LRU順序來實現LRU快取,每次呼叫get(也就是從記憶體快取中取圖片),則將該物件移到連結串列的尾端。 呼叫put插入新的物件也是儲存在連結串列尾端,這樣當記憶體快取達到設定的最大值時,將連結串列頭部的物件(近期最少用到的)移除。

說下你對 Collection 這個類的理解。

Collection是集合框架的頂層介面,是儲存物件的容器,Colloction定義了介面的公用方法如add remove clear等等,它的子介面有兩個,List和Set,List的特點有元素有序,元素可以重複,元素都有索引(角標),典型的有

Vector:內部是陣列資料結構,是同步的(執行緒安全的)。增刪查詢都很慢。

ArrayList:內部是陣列資料結構,是不同步的(執行緒不安全的)。替代了Vector。查詢速度快,增刪比較慢。

LinkedList:內部是連結串列資料結構,是不同步的(執行緒不安全的)。增刪元素速度快。

而Set的是特點元素無序,元素不可以重複

HashSet:內部資料結構是雜湊表,是不同步的。

Set集合中元素都必須是唯一的,HashSet作為其子類也需保證元素的唯一性。

判斷元素唯一性的方式:

通過儲存物件(元素)的hashCode和equals方法來完成物件唯一性的。

如果物件的hashCode值不同,那麼不用呼叫equals方法就會將物件直接儲存到集合中;

如果物件的hashCode值相同,那麼需呼叫equals方法判斷返回值是否為true, 若為false, 則視為不同元素,就會直接儲存; 若為true, 則視為相同元素,不會儲存。

如果要使用HashSet集合儲存元素,該元素的類必須覆蓋hashCode方法和equals方法。一般情況下,如果定義的類會產生很多物件,通常都需要覆蓋equals,hashCode方法。建立物件判斷是否相同的依據。 TreeSet:保證元素唯一性的同時可以對內部元素進行排序,是不同步的。

判斷元素唯一性的方式:

根據比較方法的返回結果是否為0,如果為0視為相同元素,不存;如果非0視為不同元素,則存。

TreeSet對元素的排序有兩種方式:

方式一:使元素(物件)對應的類實現Comparable介面,覆蓋compareTo方法。這樣元素自身具有比較功能。

方式二:使TreeSet集合自身具有比較功能,定義一個比較器Comparator,將該類物件作為引數傳遞給TreeSet集合的建構函式

說下AIDL的使用與原理

  • 可以圍繞aidl是安卓中的一種程式間通訊方式
  • 說下你對廣播的理解
  • 說下你對服務的理解,如何殺死一個服務。服務的生命週期(start與bind)。

其他

  • 是否接觸過藍芽等開發
  • 設計一個ListView左右分頁排版的功能自定義View,說出主要的方法。
  • 說下binder序列化與反序列化的過程,與使用過程 是否接觸過JNI/NDK,java如何呼叫C語言的方法
  • 如何檢視模擬器中的SP與SQList檔案。如何視覺化檢視佈局巢狀層數與載入時間。
  • 你說用的程式碼管理工具什麼,為什麼會產生程式碼衝突,該如何解決
  • 說下你對後臺的程式設計有那些認識,聊些前端那些方面的知識。
  • 說下你對執行緒池的理解,如何建立一個執行緒池與使用。
  • 說下你用過那些註解框架,他們的原理是什麼。自己實現過,或是理解他的工作過程嗎?
  • 說下java虛擬機器的理解,回收機制,JVM是如何回收物件的,有哪些方法等
  • 一些java與Android原始碼相關知識等

大學成績

  • 大學那些專業,你哪方面學得好
  • 微控制器,嵌入式,電子線路。
  • 畢業設計什麼,幾個人實現的,主要功能是什麼
  • 還有些其他硬體相關知識
  • 自己的職業規劃與發展方向

閱讀更多

資本寒冬下的android面經

Android自定義View——啥是佩奇?

Flutter終將逆襲!1.2版本釋出,或將統一江湖

APK 的前世今生:從 Android 原始碼到 apk 的編譯打包流程

相關文章