初冬太冷?聖誕無處可去?那跟我一起來學Android吧

又到年底了,每到這個時候,我們都會慢慢反思,這一年都做了什麼?有什麼進步?年初的計劃都實現了嗎?明年年初有跳槽的底氣了嗎?況且今年的互聯網環境太差,需要自己有足夠的知識儲備,才能夠應對這凌冽的寒風。

本文主要是整理了中高級安卓需要會的(或者說面試被頻繁問到的內容),主要作爲參考大綱,之後會陸續更新每個詳細部分,供大家參考,互相學習。

面試板塊(PDF版如下):

  • BAT面試合集(Binder,組件化插件化,熱修復,AOP,QQ換膚,虛擬機,https,線程池原理,音視頻原理)
  • 算法合集(Hash,KMP 等)
  • 中小廠面試合集(內存泄漏,Handler,View,MVC.MVP.MVVM,)
  • 大廠相關更新技術(Glide,數據庫,NDK)
  • 面試小知識(java小知識)
  • 設計模式(設計模式原則和分類)
  • 數據結構(數據結構等等)
  • 網絡編程(三次握手和四次握手,Volley,OKHttps,Retrofit)
  • 源碼解析(屬性動畫實現原理等)
  • 多線程解析(線程同步,進程線程)
  • 性能優化(Webview,內存泄漏和內存溢出等)

初冬太冷?聖誕無處可去?那跟我一起來學Android吧
順手留下GitHub鏈接,需要獲取相關面試或者面試寶典核心筆記PDF等內容的可以自己去找
https://github.com/xiangjiana/Android-MS

一丶設計模式與使用場景

(如)建造者模式:

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

使用場景比如最常見的 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 中取出來,回調到 HanderhandleMessage 方法,從而實現線程的通信。

從兩種情況來說,第一在 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),一律採用 ApplicationContext,單例模式是最常見的發生此泄漏的場景,比如傳入一個 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中所佔據的pixeldata 內存區域,而不是去問內存重新申請一塊區域來存放 Bitmap。利用這種特性,即使是上千張的圖片,也只會僅僅只需要佔用屏幕所能夠顯示的圖片數量的內存大小,但複用存在一些限制,具體體現在:在 Android 4.4 之前只能重用相同大小的 Bitmap 的內存,而 Android 4.4 及以後版本則只要後來的 Bitmap 比之前的小即可。使用 inBitmap 參數前,每創建一個 Bitmap 對象都會分配一塊內存供其使用,而使用了 inBitmap 參數後,多個 Bitmap 可以複用一塊內存,這樣可以提高性能

4.StringBuilder 替代 String: 在有些時候,代碼中會需要使用到大量的字符串拼接的操作,這種時候有必要考慮使用 StringBuilder 來替代頻繁的“+”
5.避免在類似 onDraw 這樣的方法中創建對象,因爲它會迅速佔用大量內存,引起頻繁的 GC 甚至內存抖動
6.減少內存泄漏也是一種避免 OOM 的方法

七丶如何實現進程保活

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 方法都走完了後最後才進行 contentViewmeasure/layout/draw 顯示在界面上

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

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

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

1、減少 onCreate()方法的工作量
2、不要讓 Application 參與業務的操作
3、不要在 Application 進行耗時操作
4、不要以靜態變量的方式在 Application 保存數據
5、減少佈局的複雜度和層級
6、減少主線程耗時

九丶爲什麼冷啓動會有白屏黑屏問題?

原因在於加載主題樣式 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 的方式開啓IntentServiceIntentService 通過 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 底層實現原理:

LruCacheLru 算法的實現就是通過 LinkedHashMap 來實現的。LinkedHashMap繼承於HashMap,它使用了一個雙向鏈表來存儲 Map 中的 Entry 順序關係,對於 get、put、remove 等操作,LinkedHashMap 除了要做 HashMap 做的事情,還做些調整 Entry 順序鏈表的工作。

LruCache 中將 LinkedHashMap 的順序設置爲 LRU 順序來實現 LRU 緩存,每次調用 get(也就是從內存緩存中取圖片),則將該對象移到鏈表的尾端。調用 put 插入新的對象也是存儲在鏈表尾端,這樣當內存緩存達到設定的最大值時,將鏈表頭部的對象(近期最少用到的)移除。

十四丶JNIEnv 和 和 M JavaVM 理解?

1.JavaVm

JavaVM 是虛擬機在 JNI 層的代表,一個進程只有一個 JavaVM,所有的線程共用一個 JavaVM

2.JNIEnv

JNIEnv 表示 Java 調用 native 語言的環境,是一個封裝了幾乎全部 JNI 方法的指針。JNIEnv 只在創建它的線程生效,不能跨線程傳遞,不同線程的 JNIEnv 彼此獨立。native 環境中創建的線程,如果需要訪問 JNI,必須要調用 AttachCurrentThread關聯,並使用 DetachCurrentThread 解除鏈接。

十五丶Serializable 與 e Parcable 的區別?

1.Serializable (java 自帶)

方法: 對象繼承 Serializable 類即可實現序列化,就是這麼簡單,也是它最吸引我們的地方

2.Parcelable(Android 專用):

Parcelable 方式的實現原理是將一個完整的對象進行分解,用起來比較麻煩
1)在使用內存的時候,Parcelable 比 Serializable 性能高,所以推薦使用 Parcelable。
2)Serializable 在序列化的時候會產生大量的臨時變量,從而引起頻繁的 GC。
3)Parcelable 不能使用在要將數據存儲在磁盤上的情況,因爲 Parcelable 不能很好的保證數據的持續性,在外界有變化的情況下。儘管 Serializable 效率低點,但此時還是建議使用 Serializable
4)android 上應該儘量採用 Parcelable,效率至上,效率遠高於 Serializable

順手留下GitHub鏈接,需要獲取相關面試等內容的可以自己去找
https://github.com/xiangjiana/Android-MS

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章