Android基礎知識面試30題及答案

Android基礎面試常見的30個題目,答案均為本人撰寫和整理,部分圖片和資料可能來源於網路,有部分題目沒有整理完全後面會陸續補上,如有錯誤請及時指出,部分資料整理於2017年,可能和現在有所出入,請大家諒解,如果需要下載請關注公眾號「IT大學生」回復「Android」進行下載。

1. Activity與Fragment的生命周期。

答案:

Activity:

Activity生命周期須知:

(1)onStart和onResume的區別:onStart實際上表示Activity已經可見了,只是我們還看不到還不能交互而已,因為它還處在後台。而onResume表示Activity已經顯示到前台可見了,並且可以進行交互。

(2)當用戶按下home鍵,Activity經歷onPause-onStop的過程,這時重新進入Activity經歷onRestart-onStart-onResume過程;如果按下back鍵,經歷onPause-onStop-onDestroy的過程。

(3)當在當前ActivityA中打開一個新的ActivityB,要注意的是只有A的onPause執行完成後B的onCreate-…過程才能開始,而A的onStop則是在之後才會進行的。所以不應該在onPause中做耗時的操作,應該儘快讓B顯示出來進行操作才行。

Fragment:

對比:

  1. onCreate過程
  2. 01-22 15:30:28.091: E/HJJ(10315): Activity &&&& onCreate...
  3. 01-22 15:30:28.091: E/HJJ(10315): ArrayListFragment **** onAttach...
  4. 01-22 15:30:28.091: E/HJJ(10315): ArrayListFragment **** onCreate...
  5. 01-22 15:30:28.115: E/HJJ(10315): ArrayListFragment **** onCreateView...
  6. 01-22 15:30:28.123: E/HJJ(10315): ArrayListFragment **** onActivityCreated...
  7. onStart過程
  8. 01-22 15:30:28.123: E/HJJ(10315): Activity &&&& onStart...
  9. 01-22 15:30:28.123: E/HJJ(10315): ArrayListFragment **** onStart...
  10. onResume過程
  11. 01-22 15:30:28.123: E/HJJ(10315): Activity &&&& onResume...
  12. 01-22 15:30:28.123: E/HJJ(10315): ArrayListFragment **** onResume...
  13. onPause過程
  14. 01-22 15:31:26.748: E/HJJ(10315): ArrayListFragment **** onPause...
  15. 01-22 15:31:26.748: E/HJJ(10315): Activity &&&& onPause...
  16. onStop過程
  17. 01-22 15:31:27.638: E/HJJ(10315): ArrayListFragment **** onStop...
  18. 01-22 15:31:27.638: E/HJJ(10315): Activity &&&& onStop...
  19. onStart過程
  20. 01-22 15:31:57.537: E/HJJ(10315): Activity &&&& onStart...
  21. 01-22 15:31:57.537: E/HJJ(10315): ArrayListFragment **** onStart...
  22. onResume過程
  23. 01-22 15:31:57.537: E/HJJ(10315): Activity &&&& onResume...
  24. 01-22 15:31:57.537: E/HJJ(10315): ArrayListFragment **** onResume...
  25. onPause過程
  26. 01-22 15:32:47.412: E/HJJ(10315): ArrayListFragment **** onPause...
  27. 01-22 15:32:47.412: E/HJJ(10315): Activity &&&& onPause...
  28. onStop過程
  29. 01-22 15:32:47.865: E/HJJ(10315): ArrayListFragment **** onStop...
  30. 01-22 15:32:47.865: E/HJJ(10315): Activity &&&& onStop...
  31. onDestroy過程
  32. 01-22 15:32:47.865: E/HJJ(10315): ArrayListFragment **** onDestroyView...
  33. 01-22 15:32:47.865: E/HJJ(10315): ArrayListFragment **** onDestroy...
  34. 01-22 15:32:47.865: E/HJJ(10315): ArrayListFragment **** onDetach...
  35. 01-22 15:32:47.865: E/HJJ(10315): Activity &&&& onDestroy...

2. Acitivty的四種啟動模式與特點。

(1)標準模式(Standard)

系統的默認模式,每次啟動一個Activity都會重新創建一個新的Activity實例,不管是否已經存在。並且誰啟動了這個Activity,那麼這個Activity就會運行在啟動它的那個Activity的任務棧中,如果我們用ApplicationContext來啟動標準模式的Activity就會報錯,因為它沒有所謂的任務棧,必須給Activity添加FLAG_ACTIVITY_NEW_TASK標誌位。

(2)棧頂復用模式(singleTop)

在這個模式中,Activity還是會創建在啟動它的Activity的任務棧中,但是如果它已經位於棧頂,那麼就不會重複創建,並且它的onNewIntent會被調用,但是要注意它的生命周期方法onCreate等等不會創建。並且如果它並不位於棧頂,如ABC,這時啟動B,還是會創建一個新的實例。

(3)棧內復用模式(singleTask)

在這個模式中,比如啟動Activity A,首先系統會尋找是否存在A想要的任務棧:

如果存在,就看任務棧中是否有A,如果有就有clearTop的效果,把A推到棧頂,並且調用onNewIntent,即CABD的任務棧,clearTop之後就是CA;如果沒有A,就創建A在棧頂。並且要注意一點,如果存在A的任務棧,那麼任務棧自動回切換到前台,如下圖所示:

Y的任務棧直接被切換到了前台,這點和微信的聊天界面是一樣的。

如果不存在,就創建A所需要的任務棧並且創建A入棧。

使用場景:

一般來說棧內復用適用於在一個應用里我們希望唯一存在的界面,比如聊天界面,比如微信的聊天界面,我們在一個聊天界面點擊了頭像進入了新的界面,這個時候來了一條新的消息並且用戶點擊了消息,這個時候如果是Standard模式就會創建一個新的實例,這樣用戶在新的界面點擊back,用戶期望回到的是主界面,而實際上回到了之前的頭像界面,這就不符合常規了。如果把聊天設為棧內唯一模式,那麼用戶點擊之後就會回到之前的聊天界面並且做一些改變,保證了聊天界面的唯一性。瀏覽器的例子也是這樣,我們肯定希望瀏覽器瀏覽的界面是唯一的,也需要用到這個模式。

(4)單實例模式(singleInstance)

啟動時,無論從哪裡啟動都會給A創建一個唯一的任務棧,後續的創建都不會再創建新的A,除非A被銷毀了。

我們看到從MainActivity跳轉到SecondActivity時,重新啟用了一個新的棧結構,來放置SecondActivity實例,然後按下後退鍵,再次回到原始棧結構;圖中下半部分顯示的在SecondActivity中再次跳轉到MainActivity,這個時候系統會在原始棧結構中生成一個MainActivity實例,這時點擊back發現一個神奇的現象,回到的還是MainActivity,然後再點擊一次,注意,並沒有退出,而是回到了SecondActivity,為什麼呢?是因為從SecondActivity跳轉到MainActivity的時候,在第一個返回棧中創建了新的實例,而Second所在的成為了後台棧,所以說singleInstance不要用於中間頁面,如果用於中間頁面,跳轉會有問題,比如:A -> B (singleInstance) -> C,完全退出後,再次啟動,首先打開的是B。

使用情況:

假設,我們希望我們的應用能和另一個應用共享某個Activity的實例。

這樣說,可能很難以理解,那麼舉例來說:

1.現在我們的手機上有「某某地圖」以及我們自己的應用。

2.我們希望在我們的應用里共享「某某地圖」當中的地圖功能界面。

那麼,假定的操作就應該為,例如:

1.首先,假設我們在「某某地圖」里已經做了一定操作,地圖界面被我們定位到了成都市。

2.我們返回了HOME界面,打開了自己的應用,在自己的應用里打開了」某某地圖」的地圖界面。

3.那麼,所謂共享該Activity實例,我們要達到的效果就是,當我們打開地圖界面,此時地圖上顯示的位置就應該是我們之前定位到的成都市。而不是地圖的初始化顯示方位。

那麼,顯然,通過此前的3種啟動模式,我們是實現不了的。因為:

我們最初在「某某地圖」中進行定位時,activity是位於該應用的返回棧里的。

當我們在自己的應用再次調用地圖界面,系統的操作是,在我們自己的應用的返回棧里新建一個地圖界面Activity的實例對象。

所以實際上兩個「地圖界面」是位於兩個不同應用的各自的返回棧里的,兩個毫無關聯的Activity實例。

MainActivity位於Task「8」當中,當我們在自己的應用MainActivity當中調用SecondActivity,系統新建了返回棧Task「9」,並在該返回棧中放入一個全新的SecondActivity的實例對象。

此時,當我們再打開SecondActivity本身所在的應用,調用SecondActivity,系統則會復用Task「9」當中的對象,而不會去做其他操作了。

從而,也就是實現了我們的目的,在兩個應用間共享某個Activity。

3. Activity緩存方法。

Activity的 onSaveInstanceState() 和 onRestoreInstanceState()並不是生命周期方法,它們不同於 onCreate()、onPause()等生命周期方法,它們並不一定會被觸發。當應用遇到意外情況(如:內存不足、用戶直接按Home鍵)由系統銷毀一個Activity,onSaveInstanceState() 會被調用。但是當用戶主動去銷毀一個Activity時,例如在應用中按返回鍵,onSaveInstanceState()就不會被調用。除非該activity是被用戶主動銷毀的,通常onSaveInstanceState()只適合用於保存一些臨時性的狀態,而onPause()適合用於數據的持久化保存。

異常情況的出現分為兩種可能:

(1)資源相關的系統配置發生改變導致Activity被殺死並重新創建

這就是我們所說的橫豎屏幕切換的情況,在橫豎屏切換時由於系統配置發生了改變,Activity會被銷毀,其onPause,onStop,onDestroy均被調用,同時onSaveInstanceState會被調用,它的調用時機發生在onStop之前,和onPause沒有時間上的先後關係。

(2)內存不足導致低優先順序的Activity被殺死

這種情況onSaveInstanceState會起作用,但是調用的時機不是被殺死時,因為被殺死時怎麼會有機會調用呢?注意點2介紹了這個情況。

有兩點需要注意:

(1)使用onRestoreInstanceState來進行之前狀態的獲取優於onCreate,因為onRestoreInstanceState只要被調用一定說明是有值的(之前已經調用過onSaveInstanceState),但是onCreate無論是否有值一定會調用,所以沒有使用onRestoreInstanceState好。

(2)系統正常銷毀時,onSaveInstanceState不會被調用,如用戶按back鍵。但是如果Activity有機會重新顯示出來,那麼onSaveInstanceState一定會調用,因為系統也不知道它是否一定會被殺死。系統不知道你按下HOME後要運行多少其他的程序,自然也不知道activity A是否會被銷毀,因此系統都會調用onSaveInstanceState(),讓用戶有機會保存某些非永久性的數據。以下幾種情況的分析都遵循該原則

  1. 當用戶按下HOME鍵時
  2. 長按HOME鍵,選擇運行其他的程序時
  3. 鎖屏時
  4. 從activity A中啟動一個新的activity時
  5. 屏幕方向切換時

4. Service的生命周期,兩種啟動方法,有什麼區別。

(1)A started service

  onCreate, onStartCommand, onBind 和 onDestroy。這幾個方法都是回調方法,都是由Android操作系統在合適的時機調用的,並且需要注意的是這幾個回調方法都是在主線程中被調用的。

1、onCreate: 執行startService方法時,如果Service沒有運行的時候會創建該Service並執行Service的onCreate回調方法;如果Service已經處於運行中,那麼執行startService方法不會執行Service的onCreate方法。也就是說如果多次執行了Context的startService方法啟動Service,Service方法的onCreate方法只會在第一次創建Service的時候調用一次,以後均不會再次調用!!!!我們可以在onCreate方法中完成一些Service初始化相關的操作。

2、onStartCommand: 在執行了startService方法之後,有可能會調用Service的onCreate方法,在這之後一定會執行Service的onStartCommand回調方法。也就是說,如果多次執行了Context的startService方法,那麼Service的onStartCommand方法也會相應的多次調用!!!onStartCommand方法很重要,我們在該方法中根據傳入的Intent參數進行實際的操作,比如會在此處創建一個線程用於下載數據或播放音樂等。

3、onBind: Service中的onBind方法是抽象方法,所以Service類本身就是抽象類,也就是onBind方法是必須重寫的,即使我們用不到。在通過startService使用Service時,我們在重寫onBind方法時,只需要將其返回null即可。onBind方法主要是用於給bindService方法調用Service時才會使用到。

4、onDestroy: 通過startService方法啟動的Service會無限期運行,只有當調用了Context的stopService或在Service內部調用stopSelf方法時,Service才會停止運行並銷毀,在銷毀的時候會執行Service回調函數。

(2)A bound service

被綁定的service是當其他組件(一個客戶)調用bindService()來創建的。客戶可以通過一個IBinder介面和service進行通信。客戶可以通過 unbindService()方法來關閉這種連接。一個service可以同時和多個客戶綁定,當多個客戶都解除綁定之後,系統會銷毀service。context.bindService()->onCreate()->onBind()->Service running-->onUnbind() -> onDestroy() ->Service stop,onBind將返回給客戶端一個IBind介面實例,IBind允許客戶端回調服務的方法,比如得到Service運行的狀態或其他操作。這個時候把調用者(Context,例如Activity)會和Service綁定在一起,當解除綁定時,只有完全沒有Client與它綁定時才會調用onUnbind和onDestroy,否則都不會調用。

注意:由於TestService已經處於運行狀態,所以ActivityB調用bindService時,不會重新創建TestService的實例,所以也不會執行TestService的onCreate回調方法,由於在ActivityA執行bindService的時候就已經執行了TestService的onBind回調方法而獲取IBinder實例,並且該IBinder實例在所有的client之間是共享的,所以當ActivityB執行bindService的時候,不會執行其onBind回調方法,而是直接獲取上次已經獲取到的IBinder實例!!!。並將其作為參數傳入ActivityB的ServiceConnection的onServiceConnected方法中,標誌著ActivityB與TestService建立了綁定連接,此時有兩個客戶單client(ActivityA和ActivityB)與TestService綁定。

(3)注意點:

第一:這兩條路徑並不是完全分開的。

  即是說,你可以和一個已經調用了 startService()而被開啟的service進行綁定。

比如,一個後台音樂service可能因調用 startService()方法而被開啟了,稍後,可能用戶想要控制播放器或者得到一些當前歌曲的信息,可以通過bindService()將一個activity和service綁定。這種情況下,stopService()或 stopSelf()實際上並不能停止這個service,除非所有的客戶都解除綁定。同樣,如果是startService開啟的服務及時所有客戶解除綁定,如果不調用stopService,也不能停止。

第二:除了onStartCommand可以多次調用,其他都不能

在Service每一次的開啟關閉過程中,只有onStart可被多次調用(通過多次startService調用),其他onCreate,onBind,onUnbind,onDestory在一個生命周期中只能被調用一次!!!!

5. 怎麼保證service不被殺死。

(1)進程生命周期:

官方文檔告訴我們,Android系統會盡量保持擁有service的進程運行,只要在該service已經被啟動(start)或者客戶端連接(bindService)到它。當內存不足時,需要保持,擁有service的進程具有較高的優先順序。

1. 如果service正在調用onCreate,onStartCommand或者onDestory方法,那麼用於當前service的進程則變為前台進程以避免被killed。

2. 如果當前service已經被啟動(start),擁有它的進程則比那些用戶可見的進程優先順序低一些,但是比那些不可見的進程更重要,這就意味著service一般不會被killed.

3. 如果客戶端已經連接到service (bindService),那麼擁有Service的進程則擁有最高的優先順序,可以認為service是可見的。

4. 如果service可以使用startForeground(int, Notification)方法來將service設置為前台狀態,那麼系統就認為是對用戶可見的,並不會在內存不足時killed。

5. 如果有其他的應用組件作為Service,Activity等運行在相同的進程中,那麼將會增加該進程的重要性。

(2)

方法一:onStartCommand中返回START_STICKY,如果內存不足被殺死,那麼等內存足夠是系統會自動重啟Service;

方法二:提升service優先順序,只是降低了被殺死的概率,但如果被殺死不會重啟;

方法三:Android中的進程是託管的,當系統進程空間緊張的時候,會依照優先順序自動進行進程的回收。Android將進程分為6個等級,它們按優先順序順序由高到低依次是:

1.前台進程( FOREGROUND_APP)

2.可視進程(VISIBLE_APP )

3. 次要服務進程(SECONDARY_SERVER )

4.後台進程 (HIDDEN_APP)

5.內容供應節點(CONTENT_PROVIDER)

6.空進程(EMPTY_APP)

當service運行在低內存的環境時,將會kill掉一些存在的進程。因此進程的優先順序將會很重要,可以使用startForeground將service放到前台狀態。這樣在低內存時被kill的幾率會低一些。

白色方法:放一個可見的Notification,使用startForeground,如網易雲音樂。

灰色方法:它是利用系統的漏洞來啟動一個前台的Service進程,與普通的啟動方式區別在於,它不會在系統通知欄處出現一個Notification,看起來就如同運行著一個後台Service進程一樣。這樣做帶來的好處就是,用戶無法察覺到你運行著一個前台進程(因為看不到Notification),但你的進程優先順序又是高於普通後台進程的。

方法四:監聽鎖屏事件或者主Activity被關閉時,顯示一個1像素的透明Activity,讓進程成為前台進程。

方法五:守護進程(Native層或者Java層),互相監聽。

6. 廣播的兩種註冊方法,有什麼區別。

(1)兩種註冊方法

BroadcastReceiver分為兩類:

  • 靜態廣播接收者:通過AndroidManifest.xml的標籤來申明的BroadcastReceiver。
  • 動態廣播接收者:通過AMS.registerReceiver()方式註冊的BroadcastReceiver,動態註冊更為靈活,可在不需要時通過unregisterReceiver()取消註冊。

(2)區別

1)靜態註冊:在AndroidManifest.xml註冊,android不能自動銷毀廣播接收器,也就是說當應用程序關閉後,還是會接收廣播。

2)動態註冊:在代碼中通過registerReceiver()手工註冊.當程序關閉時,該接收器也會隨之銷毀。當然,也可手工調用unregisterReceiver()進行銷毀

7. Intent的使用方法,可以傳遞哪些數據類型。

(1)使用方法

顯示Intent比較簡單,傳入類即可,不再贅述。隱式Intent則需要能夠匹配目標組件IntentFilter所設置的過濾信息,不匹配則無法進行調用。IntentFilter中的過濾信息有action,category,data。只有這三個都匹配成功才算完全匹配,只有完全匹配才能成功啟動。並且一個Activity可以有多個IntentFilter,只需要匹配成功任何一組就可以啟動。

1、action的匹配規則

action的要求是必須存在,action也是我們創建Intent是傳入構造函數的值。action只需要與過濾規則中的任意一個action相同即可,字元串必須完全一樣(大小寫也要一致)。

2、category的匹配規則

也是只需要與其中一個category相同即可,如果不手動添加則默認為DEFAULT,那麼想要啟動成功IntentFilter中必須要加DEFAULT。

3、data匹配規則

data的匹配規則與action類似,如果IntentFilter中定義了data,那麼Intent也必須定義可以匹配的data。data由兩部分組成,mimeType與URI,mimeType指的是媒體類型,比如image/jpeg等,而URI的組成就是它所定義的結構,分為scheme,host,port以及路徑信息。只有mimeType相同並且URI匹配才行,而URI本身是有默認值的,默認值為content和file。

(2)可以傳遞的數據類型

1.基本類型

2.可序列化的類型(自定義類型需要實現序列化介面)

8. ContentProvider使用方法。

ContentProvider主要是用於給外界提供數據訪問的方式,用於在應用間傳遞數據。使用方式並不複雜,只需要根據需求實現CRUD操作,然後註冊在mainfest中即可。主要注意點如下:

(1)如果要通過uri訪問不同數據,要做uri的解析工作;

(2)對於更新操作,要使用同步機制防止並發問題

(3)許可權控制、防止sql注入等技術

9. Thread、AsycTask、IntentService的使用場景與特點。

答案:

(1)AsyncTask

介紹:

AsyncTask是一種輕量級的非同步任務類,可以在後台線程池中執行後台的任務,然後把執行的進度和最終的結果傳遞給主線程並在主線程中更新UI。從實現上來說,AsyncTask封裝了Thread和Handler。但它並不適合特別耗時的任務,對於特別耗時的任務應該使用線程池。

它是一個泛型抽象類,Params表示參數的類型,Progress表示後台任務進度的類型,而Result表示結果的返回類。

使用特點:

(1)它必須在主線程中創建,execute方法必須在主線程中調用

(2)execute方法只能執行一次,雖然可以傳很多個參數(任務)

工作原理:

AsyncTask實際上是對線程池和Handler進行了封裝。

(1)任務執行:

3.0之前,AsyncTask是並行執行的,而3.0之後就變為了串列執行,並且開發者可以選擇進行並行執行。原理是什麼呢?實際上它內部有兩個線程池,sDefaultExecutor是一個自己實現的串列的線程池,它是static的,說明一個進程內的所有任務都是它來執行,它的任務很簡單,就是把任務放進一個隊列中,然後提醒另一個並行的線程池THREAD_POOL_EXECUTOR來取出執行,如果有可取的並且當前沒有任務在執行就會被這個並行的線程池來執行。如果有任務在執行自然不會執行,當這個任務執行完之後又會重新提醒並行的線程池THREAD_POOL_EXECUTOR來取出隊列中的任務進行執行。所以從這個原理我們看出來它是串列執行的,原因就是老版本是串列的並且很多代碼依賴於這個邏輯。

(2)任務結果分發

它的內部有一個static的handler,所以這也是它必須在UI線程中進行初始化的原因,這樣可以保證Handler被正常的初始化。當任務執行完成後,就會將結果發送給Handler,使得其在主線程被執行。

(2)IntentService

介紹:

僅僅是一個封裝了HandlerThread的Service而已,由於Service正常來說也是執行在主線程的,所以不能執行耗時的操作。而IntentService在內部維護有個HandlerThread,它擁有自身的Handler,對應於HandlerThread的Looper。當收到一個Intent時,它將Intent包裝到Message中直接發送給Handler來處理,從而避免了在主線程中進行操作。

使用:

重寫onHandleIntent,在其中處理Intent,並且這個是不在主線程中運行的。

10. 五種布局: FrameLayout 、 LinearLayout 、 AbsoluteLayout 、 RelativeLayout 、 TableLayout 各自特點及繪製效率對比。

11. Android的數據存儲形式。

(1)使用SharedPreferences存儲數據

特點:使用簡單,應用內數據共享,但只支持基本數據類型。

(2)使用文件存儲

(3)SQLite資料庫存儲

(4)使用ContentProvider存儲數據(原理還是123中的一種,只是可以對外共享)

12. Sqlite的基本操作。

13. Android中的MVC模式以及與MVP的對比。

(1)Android中的MVC模式

傳統的MVC如下圖所示:

當用戶出發事件的時候,view層會發送指令到controller層,接著controller去通知model層更新數據,model層更新完數據以後直接顯示在view層上,這就是MVC的工作原理。

對於原生的Android項目來說,layout.xml裡面的xml文件就對應於MVC的view層,裡面都是一些view的布局代碼,而各種java bean,還有一些類似repository類就對應於model層,至於controller層嘛,當然就是各種activity咯。比如你的界面有一個按鈕,按下這個按鈕去網路上下載一個文件,這個按鈕是view層的,是使用xml來寫的,而那些和網路連接相關的代碼寫在其他類里,比如你可以寫一個專門的networkHelper類,這個就是model層,那怎麼連接這兩層呢?是通過button.setOnClickListener()這個函數,這個函數就寫在了activity中,對應於controller層。相較於傳統的MVC模式,model層應該要通知View來更新數據才對,而Android中由於View層的控制能力實在太弱,通知view的代碼全部放在了activity中,導致activity既算controller又算view,這樣activity的代碼行數太多,維護起來太過於麻煩。MVC還有一個重要的缺陷,大家看上面那幅圖,view層和model層是相互可知的,這意味著兩層之間存在耦合,耦合對於一個大型程序來說是非常致命的,因為這表示開發,測試,維護都需要花大量的精力。

(2)Android中的MVP模式

MVP模式中完全把fragment看做是View層,它負責界面的更新和顯示操作,而Presenter層則負責了邏輯的實現,從圖中就可以看出,最明顯的差別就是view層和model層不再相互可知,完全的解耦,取而代之的presenter層充當了橋樑的作用,用於操作view層發出的事件傳遞到presenter層中,presenter層去操作model層,並且將數據返回給view層,整個過程中view層和model層完全沒有聯繫。一個典型的事件就是點擊事件產生,view交由presenter來進行處理,處理完成之後再調用view的界面更新介面進行界面更新。Activity只是作為一個控制器來進行一些全局的控制(因為android的很多操作終究還是依賴於activity的),創建界面的時候activity對view和presenter進行相應的初始化。

看到這裡大家可能會問,雖然view層和model層解耦了,但是view層和presenter層不是耦合在一起了嗎?其實不是的,對於view層和presenter層的通信,我們是可以通過介面實現的,具體的意思就是說我們的activity,fragment可以去實現實現定義好的介面,而在對應的presenter中通過介面調用方法。不僅如此,我們還可以編寫測試用的View,模擬用戶的各種操作,從而實現對Presenter的測試。這就解決了MVC模式中測試,維護難的問題。

MVP還有一個巨大優勢就是使用面向業務的方式來進行代碼編寫,代碼架構比較清晰。

14. Merge、ViewStub的作用。

15. Json有什麼優劣勢。

16. 動畫有哪兩類,各有什麼特點?

(1)View動畫(補間動畫)

View動畫的作用對象是View本身,支持四種動畫效果,分別是平移動畫、縮放動畫、旋轉動畫和透明度動畫。除了這四種,幀動畫也屬於View動畫,是一種特殊的View動畫。View動畫可以使用xml或者代碼來創建,使用set就可以把多個動畫組合在一起,調用startAnimation來進行動畫的調用。而幀動畫就好比是把動畫分為許多幀,使用xml來定義一個AnimationDrawable來進行幀動畫的使用,幀動畫的問題時圖片太多時——OOM。

View動畫的常見作用就是ListView的item的動畫,以及Activity之間變換的動畫,都是有View動畫實現的。而針對View動畫的自定義較難,涉及到矩陣變換等知識。

(2)屬性動畫

屬性動畫相較於View,更加的靈活和多樣。屬性動畫完成的效果其實就是在一個事件間隔內完成對象從一個屬性值到另一個屬性值的改變,效果更佳更自由。

1、使用注意點

屬性動畫要求提供屬性的get和set方法,可是如果一個控制項沒有怎麼辦?最簡單的方法就是使用包裝類,繼承自原始類並重寫相對應屬性的set和get方法,這是最簡單的一種實現方式。

2、原理分析

ObjectAnimator實際上繼承自ValueAnimator,主要細節如下:

(1)屬性值的設置:使用的是反射來調用,進行屬性值的獲取和設置,插值器和估值器的作用就在設置之前進行計算,用來確定屬性值。

(2)動畫效果類似於我們用post實現動畫一樣,animationHandler實際上是一個Runnable對象,存儲在當前線程的ThreadLocal中,start的時候開始執行scheduleAnimation函數, animationHandler一個線程只有一個,用於執行此線程的所有動畫。而一個動畫過程則是以ValueAnimator的形式存儲在animationHandler自己的一個隊列Animations中的。當對動畫完成了設置只有調用animationHandler的start函數,它裡面使用了native的方法來保證UI系統每幀都會執行自己的run函數一次(發送給Looper執行run),run中它取出自己的隊列Animations中的ValueAnimator並且執行其中的doAnimationFrame。這就是真正執行動畫效果的地方,並且run中有一個FrameTime是當前動畫運行的時間,由native函數計算。

(3)doAnimationFrame的作用:它將傳入的FrameTime和mStartTime進行比較,因為有可能還未開始執行。之後根據他們的差值,加上插值器和估值器的使用來計算當前應該得到的值,之後利用反射來進行動畫的變化調用。

17. Handler、Looper消息隊列模型,各部分的作用。

答案:

一、消息機制的角色分析

首先我們介紹一下消息機制的幾位主人公,正是有它們的通力合作,消息機制才能正常的運行:

1、Handler:處理器,負責的內容是消息的發送和具體處理流程,一般使用時由開發者重寫handleMessage函數,根據自定義的不同message做不同的UI更新操作;

2、Message:消息對象,代表一個消息實體,存放消息的基本內容;

3、MessageQueue:消息隊列,按順序存放消息實體,由單鏈表實現,被Looper(4)所使用(一個Looper具有一個消息隊列);

4、Looper:循環器,也是我們的消息機制的主角,一個線程至多具有一個並且與線程一一對應(如何理解等會來說),負責的內容是不斷從消息隊列取出放入的消息並且交由消息對應的Handler進行處理(Handler的具體handleMessage操作);

5、ThreadLocal:線程本地數據,是一個線程內部的數據存儲類,為每個線程存儲屬於自己的獨立的數據。

二、消息機制總覽

我們來用最簡短的語言說明一下消息循環的整個過程,有個整體性的認識,之後再進行逐一的進行源碼分析。

1、首先,我們知道ThreadLocal是線程內部的數據存儲類,一個線程對應一個自己獨一無二的數據,而我們的主角Looper就是這樣一個對象,每個線程都可以有自己獨一無二的Looper,而Looper自己具有一個屬於自己的MessageQueue,它不斷地從MessageQueue中取Message(注意,Looper的代碼是運行在自己對應的線程中的),如果MessageQueue中沒有消息,Looper只會不斷地循環嘗試取消息(阻塞)。

2、這時,我們在主線程創建了Handler對象,它需要它所在的線程(這裡是主線程)所擁有的Looper對象(也就是說,沒有Looper的線程創建不了Handler,後面我們也會看到具體代碼),一旦我們用Handler發送了消息(可以在別的線程中,這裡假設在某個子線程中),Handler就會把這個消息放入自己擁有的Looper對象的屬於這個Looper對象的MessageQueue中(這句有點拗口,就是放入Looper的MessageQueue中)。

3、我們已經知道Looper會不斷地嘗試從自己的MessageQueue中取出消息,我們剛剛放入的消息立刻被Looper取出來了,它得到了消息就執行了發出消息的Handler(也就是2過程我們所創建的)的消息處理函數handleMessage,我們編寫的UI更新操作就在Looper對象的代碼中執行了,而我們剛才也說了這個Looper運行在我們創建Handler的線程中的,也就是主線程(UI線程),那這樣一來就達到了我們的目標,成功的把代碼執行從子線程切換到了主線程中,這個過程我們也就有個總覽了,

18. 怎樣退出終止App。

建立一個基類Activity,在Activity創建時添加到全局表中,在Activity銷毀時移除全局表,調用全局退出時,對全局表Activity進行全部退出,達到完全退出的效果。

19. Assets目錄與res目錄的區別。

assets目錄與res下的raw、drawable目錄一樣,也可用來存放資源文件,但它們三者有區別,對比總結如下表:

(1)res/raw和asset中的文件不會被編譯成2進位文件,而是原樣複製到設備上,可以用文件流來讀取原始文件,所以可以用來存放通用的靜態文件,比如視頻等;

(2)與res/raw不同點在於,Assets支持任意深度的子目錄,這是它們的主要區別。這些文件不會生成任何資源ID,必須使用/assets開始(不包含它)的相對路徑名,而res中的所有文件都會生成索引來直接訪問。

20. Android怎麼加速啟動Activity。

21. Android內存優化方法:ListView優化,及時關閉資源,圖片緩存等等。

(1)Bitmap的高效載入

核心是利用BitmapFactory載入一個圖片時(從文件系統、資源、輸入流以及位元組數組)中載入一個Bitmap對象的時候,選擇合適的採樣率進行載入,即Options參數的採樣率參數對要載入的圖片進行縮放,變成合適ImageView的大小的圖片。縮放率是1/採樣率的平方。具體做法如下:

第一:首先設置Options的inJustDecodeBounds為true並載入圖片,這樣只會獲取圖片的參數(長寬)

第二:根據需要的長寬對圖片的長寬不停做/2操作,計算合適的採樣率

第三:根據新的採樣率,重新載入圖片

(2)ListView(RecyclerView)的優化

1、ListView的基礎優化方式

(1)優化載入布局——convertView

通過在getView方法中使用convertView從而來避免View的重複載入,原理是復用已經離開屏幕的View,避免了View的重新載入。

(2)優化載入控制項——ViewHolder

通過給復用的view設置Tag,實際上就是避免了重新find控制項的過程,將控制項的引用提前設置給ViewHolder,讓其持有,這樣重新設置數據復用是的速度更快。

2、ListView的載入優化方式

(1)載入圖片時使用壓縮方式——Bitmap的高效載入

(2)非同步載入過程

(3)緩存載入,利用緩存避免重複載入

優化詳情見優化方法整理。

22. Android中弱引用與軟引用的應用場景。

(1)弱引用

主要作用:防止內存泄漏。

使用場景:全局Map用於保存某種映射的時候一定一定使用弱引用來保存對象,因為全局變數一般是static的,它的聲明周期一定長於單個對象,如果用弱引用保存對象,當對象被回收時,如果使用強引用,對象就會發生內存泄漏問題。

(2)軟引用

主要作用:緩存

使用場景:對於Bitmap的載入,非常耗費時間,我們希望把載入過的Bitmap做緩存來節省載入時間,可是Bitmap非常吃內存,我們又不希望發生OOM的問題,所以應該使用軟引用來做緩存,這樣在系統內存不足時,此部分內存又會重新被回收,避免OOM的問題、

23. Bitmap的四種屬性,與每種屬性隊形的大小。

24. View與View Group分類。自定義View過程:onMeasure()、onLayout()、onDraw()。

View的總體繪製過程:

當Activity對象被創建完成,會將DecorView添加到Window中(顯示),同時創建ViewRoot的實現對象ViewRootImpl與之關聯。ViewRootImpl會調用performTraversals來進行View的繪製過程。經過measure,layout,draw三個流程才能完成一個View的繪製過程,分別是用於測量寬、高;確定在父容器中的位置;繪製在屏幕上三個過程。而measure方法會調用onMeasure函數,這其中又會調用子元素的measure函數,如此反覆就能完成整個View樹的遍歷過程。其他兩個流程也同樣如此。

measure決定了View的寬和高,測量之後就可以根據getMeasuredWidth和getMeasuredHeight來獲取View測量後的寬和高,幾乎等於最終的寬和高,但有例外;layout過程決定了View四個頂點的位置和實際的寬和高,完成之後可以根據getTop,getBottom,getLeft,getRight來獲得四個頂點的位置,並且可以使用getWidth和getHeight來獲取實際的寬和高;draw過程就決定了View的顯示,完成draw才能真正顯示出來。

1.測量Measure過程:

MeasureSpec是測量規格,它是系統將View的參數根據父容器的規則轉換而成的,之後根據它來測量出View的寬和高。它實際上是一個32位的int值,高二位表示SpecMode,就是測量的模式;低30位表示SpecSize,即在某種測量模式下的規格大小。

MeausureSpec有三種模式,常用的由兩種:EXACTLY和AT_MOST。EXACTLY表示父容器已經檢測出View所需要的精確大小(即父容器根據View的參數已經可以確定View的大小了),這時View的最終大小就是SpecSize的值,它對應於View參數中的match_parent和具體大小數值這兩種模式;AT_MOST表示父容器指定了一個可用大小的數值,記錄在SpecSize中,View的大小不能大於它,但具體的值還是看View的具體實現。它對應於View參數中的wrap_content。

DecorView(頂級View)的測量由窗口的大小和自身的LayoutParams決定,具體邏輯由getRootMeasureSpec決定,如果是具體值或者是match_parent,就是精確模式;如果是wrap_content就是最大模式;普通View的measure實際上是由父元素進行調用的(遍歷),父元素調用child的measure之前使用getChildMeasureSpec來轉換得到子元素的MeasureSpec(具體代碼:藝術探索P180-181),總結而來就是與自身的參數以及父元素的SpecMode有關:1、如果View的參數是具體值,那麼不管父元素的Mode是什麼,子元素的Mode都是精確模式並且大小就是參數的大小;2、如果View的參數是match_parent,如果父元素的mode是精確模式那麼View也是精確模式並且大小是父元素剩餘的大小;如果父元素的mode是最大模式,那麼View也是最大模式;3、如果View的參數是wrap_content,那麼View的模式一定是最大化模式,並且不能超過父容器的剩餘空間。

View的measure過程:

View自身的onMeasure方法就是把MeasureSpec的Size設為最終的測量結果,這樣的測量問題就是match_parent和wrap_content是一樣的結果(因為wrap_content的Size是最大可用Size),所以如果自定義View直接繼承自View,就需要對wrap_content進行處理,ImageView等都對wrap_content進行了特殊處理。

ViewGroup的measure過程:

ViewGroup不同於View,它是一個抽象類,沒有實現onMeasure方法(因為具體的Layout布局特性各不相同),但它measure時會遍歷children調用measureChild,執行getChildMeasureSpec進行子元素的MeasureSpec創建,創建過程之前已經了解了,就是利用自身的Spec與子元素參數進行創建。

2.確定位置Layout過程:

Layout的作用是ViewGroup來確定子元素的位置,當ViewGroup的位置被確定了之後,它就在自己的onLayout函數中遍歷所有的子元素並調用其layout方法,確定子元素的位置,對於子元素View,layout中又會調用其onLayout函數。View和ViewGroup中都沒有真正實現onLayout方法。但View和ViewGroup的layout方法是一致的,作用都是用於確定自己的位置,layout方法會調用setFrame方法來設定View的四個頂點的位置,即初始化mLeft,mRight,mTop,mBottom四個值,這樣就確定了View在父元素中的位置。

問題:getMeasuredHeight和getHeight有什麼區別(同Width)?

在measure之後就可以使用getMeasuredHeight來進行獲取測量的寬和高,而layout過程是晚於measure的,ViewGroup的setChildFrame會調用child的layout來確定child的真實位置,源代碼中也可以看出layout的bottom和top就是利用getMeasuredHeight和getMeasuredWidth來計算的,所以說如果child的layout不重寫,那麼就是一樣的!如果child的layout函數被重寫,就會有不一樣的結果。

3.繪製draw過程:

OnDraw中進行繪製自己的操作。使用canvas進行繪製等等,簡單來說就是利用代碼畫自己需要的圖形。

繪製過程中的旋轉以及save和restore

Android中的旋轉rotate旋轉的是坐標系,也就是說旋轉畫布實際上畫布本身的內容是不動的,不會直接把畫布上已經存在的內容進行移動,只是旋轉坐標系,這樣旋轉之後的操作全部是針對這個新的坐標系的。

save就是保存當前的的坐標系,之後再調用restore時,坐標系復原到save保存的狀態,這兩個函數restore的次數不能大於save,否則會引發異常。

25. Touch事件分發機制以及滑動衝突的解決。

答案:

一、View的事件分發機制

事件分發機制傳遞的就是MotionEvent,也就是點擊事件,這個傳遞過程就是分發的過程。

(1)點擊事件的傳遞規則

三大函數:

public boolean dispatchTouchEvent(MotionEvent ev)

這個函數用於進行事件的分發,如果這個時間能夠傳遞給當前的View,那麼這個方法一定會調用,返回的結果表示是否消耗當前事件,返回的結果受onInterceptTouchEvent和下級View的影響。

public boolean onInterceptTouchEvent(MotionEvent ev)

這個函數內部調用,用於判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼同一事件序列中,此方法不會被再次調用。

public boolean onTouchEvent(MotionEvent ev)

dispatchTouchEvent中調用,用於處理點擊事件,其返回結果表示是否消耗當前事件,如果不消耗,那麼同一事件序列中,當前View無法再接收到事件。

偽代碼:

public boolean dispatchTouchEvent(MotionEvent ev){

boolean consume = false;

if(onInterceptTouchEvent(ev)){

consume = onTouchEvent(ev);

} else {

consume = child. dispatchTouchEvent(ev);

}

return consume;

}

通過上述偽代碼,我們可以大致得出傳遞的規則:

(1)對於一個根ViewGroup來說,點擊事件產生後,首先會傳遞給它自己,如果它的onInterceptTouchEvent返回true,那麼就表示它要攔截當前事件,那麼它的onTouchEvent函數就會被調用;如果返回false,那麼就傳遞給子元素,直到事件被處理。

(2)當一個View需要進行事件處理時,如果它設置了OnTouchListener,那麼它的onTouch方法就會被調用,這時事件如何處理還要看onTouch的返回值,如果返回false,那麼當前View的onTouchEvent就會被調用;如果返回true,那麼onTouchEvent方法將不會調用!!!!!!。由此可見,OnTouchListener的優先順序高於onTouchEvent。在onTouchEvent方法中,如果當前設置有OnClickListener,那麼它的onClick會被調用,其優先順序最低,處於調用的末端。

(3)如果一個事件傳遞到View,如果此View的onTouchEvent返回false,就是不消耗事件,那麼此View的父容器的onTouchEvent就會被調用,也就是說如果事件最終沒有View處理,那麼處理的人就是Activity,也就是責任鏈模式。

一些結論:

(1)同一個事件序列指的是從手指接觸屏幕開始,到手指離開屏幕的過程,也就是DOWN—MOVE…MOVE—UP,這是一個事件序列。

(2)同一個事件序列只能被同一個View所消耗,因為一旦一個View攔截了某個事件,那麼同一序列內的所有事件都會直接交給他處理。但是要注意,如果事件在之前又被別人攔截,根本不交給它處理的情況也會發生——事件攔截。

(3)某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那麼同一事件序列中的其他事件都不會再交給他處理,即父元素的onTouchEvent會被調用,交給父元素來處理(責任鏈模式)。

如果某個View不處理除了ACTION_DOWN之外的其他事件,那麼這個點擊事件就會消失,並且當前View可以持續接收到後續事件(無論你選擇不選擇處理),最終消失的會被Activity處理。

(4)ViewGroup的onInterceptTouchEvent方法默認返回false,即不攔截任何事件,而View沒有onInterceptTouchEvent函數,即不能選擇是否攔截,必須攔截,但可以不處理。

(5)View的onTouchEvent默認都會消耗事件,返回true,除非它是不可點擊的。

(6)對於onTouch和onClick的總結

規律(總結):

(1)首先沒有設置OnClickListener的情況下,onTouch的返回值表示的就是View對點擊事件是否消耗,如果在DOWN事件傳遞過來時返回false,那麼剩下的MOVE直到UP的事件都不會被onTouch接收到;如果在DOWN事件返回true,那麼剩下的直到UP的事件都會接受到,無論你之後的返回值。

(2)在同時設置了OnTouchListener與OnClickListener之後,情況就有些複雜了:

情況1:如果onTouch在DOWN時返回了true,那麼onTouch就和(1)一樣收到剩下的所有事件,但onClick就不會被執行;

情況2:如果onTouch在DOWN時返回了false,與(1)不同的是,onTouch儘管在DOWN時返回了false,但之後的所有事件仍能接受到,並且onClick會在之後被調用。

public boolean dispatchTouchEvent(MotionEvent event){

... ...

if(onFilterTouchEventForSecurity(event)){

ListenerInfo li = mListenerInfo;

if(li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED

&& li.mOnTouchListener.onTouch(this, event)) { //(1)onTouch調用

return true;

}

if(onTouchEvent(event)){ //(2)onTouchEvent調用

return true;

}

}

... ...

return false;

}

分析:

(1)如果沒有設置OnClickListener,只設置了OnTouchListener,那麼在代碼(1)處就會調用onTouch,如果DOWN事件時返回了true,那麼剩下的事件都會交由此View進行處理;如果返回了false,那麼就會執行代碼(2)處的onTouchEvent函數,如果設置了OnClickListener,就會在其中進行調用,如果沒有設置,dispatchTouchEvent就會返回false,那麼剩下的事件都不會交由此View進行處理;

(2)如果同時設置了OnTouchListener與OnClickListener,那麼我們再按上面的兩種情況進行分析:

情況1:onTouch在DOWN時返回了true,那麼代碼(1)處就得到了真的結果,直接就返回了true,可以知道後面代碼(2)處的onTouchEvent函數不會被執行,那麼自然你的OnClickListener就不起作用了,onClick就不會被執行;

情況2:
onTouch在DOWN時返回了false,那麼當DOWN事件傳遞來的時候,代碼(1)處就不會得到真的結果,也就是說onTouch中你表示自己不會處理這個事件序列了,後面代碼(2)處的onTouchEvent函數就會得到執行,而如果你設置了OnClickListener,View就會處於CLICKABLE狀態,那麼onTouchEvent函數就會返回true,又表示你可以處理這個點擊事件序列了,dispatchTouchEvent就會返回true,那麼這時後面的事件由於DOWN時返回true,就會統統交由此View進行處理,自然你的onTouch中也能夠監聽到後面的所有事件!這樣上面的情況就能夠得到解釋了。

二、滑動衝突的解決方法

(1)滑動衝突的類型

滑動衝突分為三種類型,第一類是外部和內部滑動方向不一致,第二類是外部和內部滑動方向一致,第三類是前兩種嵌套的模式。

處理這三種類型的規則分為兩類,對於第一種類型,我們可以根據滑動方向來處理,符合處理方向的分配給對應的控制項;對於2、3種類型,必須根據業務上的區別來處理,某種狀態的處理時間分發給對應的控制項來處理。

(2)滑動衝突的解決方式

解決方式一:外部攔截法

外部攔截法指點擊事件首先都會經過父容器的攔截處理,父容器如果需要此事件就進行攔截,如果不需要此事件就不進行攔截,這樣就可以解決滑動衝突問題。外部攔截法主要就是重寫父容器的onInterceptTouchEvent方法,但是要注意,父容器攔截不能在ACTION_DOWN中返回true,否則之後的所有事件序列都會交給它處理,無論返回什麼,因為不會再調用它的onInterceptTouchEvent函數了。所以父控制項應該在ACTION_MOVE中選擇是否攔截。但是這種攔截的問題是,如果攔截了,那麼子控制項的onClick事件將無法再出發了。

偽代碼如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {

boolean intercepted = false;

switch (ev.getAction()){

case MotionEvent.ACTION_DOWN:

intercepted = false;

break;

case MotionEvent.ACTION_MOVE:

if(父控制項需要處理){

intercepted = true;

} else{

intercepted = false;

}

break;

case MotionEvent.ACTION_UP:

intercepted = false;

break;

}

return intercepted;

}

解決方法二:內部攔截法

內部攔截法指的是父容器不攔截任何事件,所有事件全部傳遞給子元素,如果子元素需要就進行消耗,否則交由父容器進行處理。這種方式需要配合ViewGroup的FLAG_DISALLOW_INTERCEPT標誌位來使用。設置此標誌為可以通過requestDisallowIntercept TouchEvent函數來設置,如果設置了此標誌位,那麼ViewGroup就無法攔截除了ACTION_DOWN之外的任何事件。這樣首先我們保證ViewGroup的onInterceptTouchEvent方法除了DOWN其他都返回true,DOWN返回false,這樣保證了不會攔截DOWN事件,交給它的子View進行處理;重寫View的dispatchTouchEvent函數,在DOWN中設置parent.requestDisallowInterceptTouchEvent(true),這樣父控制項在默認的情況下DOWN之後的所有事件它都攔截不到,交由子View來處理,View在MOVE中判斷父控制項需要時,調用parent.requestDisallow InterceptTouchEvent(false),這樣父控制項的攔截又起作用了,相應的事件交給了父控制項進行處理。偽代碼如下:

父控制項中:

@Override

public boolean onInterceptTouchEvent(MotionEvent ev) {

int action = ev.getAction();

if(action == MotionEvent.ACTION_DOWN){

return false;

} else {

return true;

}

}

子View中:

@Override

public boolean dispatchTouchEvent(MotionEvent ev) {

switch (ev.getAction()){

case MotionEvent.ACTION_DOWN:

getParent().requestDisallowInterceptTouchEvent(true);

break;

case MotionEvent.ACTION_MOVE:

if(父控制項需要此點擊事件){

getParent().requestDisallowInterceptTouchEvent(false);

}

break;

case MotionEvent.ACTION_UP:

break;

}

}

滑動衝突處理案例:下拉刷新實現原理。

26. Android長連接,怎麼處理心跳機制。

27. Zygote的啟動過程。

28. Android IPC:Binder原理。

29. 你用過什麼框架,是否看過源碼,是否知道底層原理。

30. Android6.0、7.0新特性。


推薦閱讀:

TAG:Android開發 | Android學習 | 面試問題 |