Android 面試題集 包含答案

風靈使發表於2018-11-16

手畫一下Android系統架構圖,描述一下各個層次的作用?

Android系統架構圖

在這裡插入圖片描述
從上到下依次分為四層:

  • Android應用框架層
  • Java系統框架層
  • C++系統框架層
  • Linux核心層

Activity如與Service通訊?

可以通過bindService的方式,先在Activity裡實現一個ServiceConnection介面,並將該介面傳遞給bindService()方法,在ServiceConnection介面的onServiceConnected()方法
裡執行相關操作。

Service的生命週期與啟動方法由什麼區別?

  • startService():開啟Service,呼叫者退出後Service仍然存在。
  • bindService():開啟Service,呼叫者退出後Service也隨即退出。

Service生命週期:

  • 只是用startService()啟動服務:onCreate() -> onStartCommand() -> onDestory
  • 只是用bindService()繫結服務:onCreate() -> onBind() -> onUnBind() -> onDestory
  • 同時使用startService()啟動服務與bindService()繫結服務:onCreate() -> onStartCommnad() -> onBind() -> onUnBind() -> onDestory

Service先start再bind如何關閉service,為什麼bindService可以跟Activity生命週期聯動?

廣播分為哪幾種,應用場景是什麼?

  • 普通廣播:呼叫sendBroadcast()傳送,最常用的廣播。
  • 有序廣播:呼叫sendOrderedBroadcast(),發出去的廣播會被廣播接受者按照順序接收,廣播接收者按照Priority屬性值從大-小排序,Priority屬性相同者,動態註冊的廣播優先,廣播接收者還可以
    選擇對廣播進行截斷和修改。

廣播的兩種註冊方式有什麼區別?

  • 靜態註冊:常駐系統,不受元件生命週期影響,即便應用退出,廣播還是可以被接收,耗電、佔記憶體。
  • 動態註冊:非常駐,跟隨元件的生命變化,元件結束,廣播結束。在元件結束前,需要先移除廣播,否則容易造成記憶體洩漏。

廣播傳送和接收的原理了解嗎?

  1. 繼承BroadcastReceiver,重寫onReceive()方法。
  2. 通過Binder機制向ActivityManagerService註冊廣播。
  3. 通過Binder機制向ActivityMangerService傳送廣播。
  4. ActivityManagerService查詢符合相應條件的廣播(IntentFilter/Permission)的BroadcastReceiver,將廣播傳送到BroadcastReceiver所在的訊息佇列中。
  5. BroadcastReceiver所在訊息佇列拿到此廣播後,回撥它的onReceive()方法。

廣播傳輸的資料是否有限制,是多少,為什麼要限制?

ContentProvider、ContentResolver與ContentObserver之間的關係是什麼?

  • ContentProvider:管理資料,提供資料的增刪改查操作,資料來源可以是資料庫、檔案、XML、網路等,ContentProvider為這些資料的訪問提供了統一的介面,可以用來做程式間資料共享。
  • ContentResolver:ContentResolver可以不同URI操作不同的ContentProvider中的資料,外部程式可以通過ContentResolver與ContentProvider進行互動。
  • ContentObserver:觀察ContentProvider中的資料變化,並將變化通知給外界。

遇到過哪些關於Fragment的問題,如何處理的?

  • getActivity()空指標:這種情況一般發生在在非同步任務裡呼叫getActivity(),而Fragment已經onDetach(),此時就會有空指標,解決方案是在Fragment裡使用
    一個全域性變數mActivity,在onAttach()方法裡賦值,這樣可能會引起記憶體洩漏,但是非同步任務沒有停止的情況下本身就已經可能記憶體洩漏,相比直接crash,這種方式
    顯得更妥當一些。

  • Fragment檢視重疊:在類onCreate()的方法載入Fragment,並且沒有判斷saveInstanceState==null或if(findFragmentByTag(mFragmentTag) == null),導致重複載入了同一個Fragment導致重疊。(PS:replace情況下,如果沒有加入回退棧,則不判斷也不會造成重疊,但建議還是統一判斷下)

@Override 
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 在頁面重啟時,Fragment會被儲存恢復,而此時再載入Fragment會重複載入,導致重疊 ;
    if(saveInstanceState == null){
    // 或者 if(findFragmentByTag(mFragmentTag) == null)
       // 正常情況下去 載入根Fragment 
    } 
}

Android裡的Intent傳遞的資料有大小限制嗎,如何解決?

Intent傳遞資料大小的限制大概在1M左右,超過這個限制就會靜默崩潰。處理方式如下:

  • 程式內:EventBus,檔案快取、磁碟快取。
  • 程式間:通過ContentProvider進行款程式資料共享和傳遞。

描述一下Android的事件分發機制?

Android事件分發機制的本質:事件從哪個物件發出,經過哪些物件,最終由哪個物件處理了該事件。此處物件指的是Activity、Window與View。

Android事件的分發順序:Activity(Window) -> ViewGroup -> View

Android事件的分發主要由三個方法來完成,如下所示:

// 父View呼叫dispatchTouchEvent()開始分發事件
public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume = false;
    // 父View決定是否攔截事件
    if(onInterceptTouchEvent(event)){
        // 父View呼叫onTouchEvent(event)消費事件,如果該方法返回true,表示
        // 該View消費了該事件,後續該事件序列的事件(Down、Move、Up)將不會在傳遞
        // 該其他View。
        consume = onTouchEvent(event);
    }else{
        // 呼叫子View的dispatchTouchEvent(event)方法繼續分發事件
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}

描述一下View的繪製原理?

View的繪製流程主要分為三步:

  1. onMeasure:測量檢視的大小,從頂層父View到子View遞迴呼叫measure()方法,measure()呼叫onMeasure()方法,onMeasure()方法完成測量工作。
  2. onLayout:確定檢視的位置,從頂層父View到子View遞迴呼叫layout()方法,父View將上一步measure()方法得到的子View的佈局大小和佈局引數,將子View放在合適的位置上。
  3. onDraw:繪製最終的檢視,首先ViewRoot建立一個Canvas物件,然後呼叫onDraw()方法進行繪製。onDraw()方法的繪製流程為:① 繪製檢視背景。② 繪製畫布的圖層。 ③ 繪製View內容。
    ④ 繪製子檢視,如果有的話。⑤ 還原圖層。⑥ 繪製滾動條。

requestLayout()、invalidate()與postInvalidate()有什麼區別?

  • requestLayout():該方法會遞迴呼叫父視窗的requestLayout()方法,直到觸發ViewRootImpl的performTraversals()方法,此時mLayoutRequestede為true,會觸發onMesaure()與onLayout()方法,不一定
    會觸發onDraw()方法。
  • invalidate():該方法遞迴呼叫父View的invalidateChildInParent()方法,直到呼叫ViewRootImpl的invalidateChildInParent()方法,最終觸發ViewRootImpl的performTraversals()方法,此時mLayoutRequestede為false,不會
    觸發onMesaure()與onLayout()方法,當時會觸發onDraw()方法。
  • postInvalidate():該方法功能和invalidate()一樣,只是它可以在非UI執行緒中呼叫。

一般說來需要重新佈局就呼叫requestLayout()方法,需要重新繪製就呼叫invalidate()方法。

Scroller用過嗎,瞭解它的原理嗎?

瞭解APK的打包流程嗎,描述一下?

Android的包檔案APK分為兩個部分:程式碼和資源,所以打包方面也分為資源打包和程式碼打包兩個方面,這篇文章就來分析資源和程式碼的編譯打包原理。

APK整體的的打包流程如下圖所示:

Android 面試題集 包含答案

具體說來:

  1. 通過AAPT工具進行資原始檔(包括AndroidManifest.xml、佈局檔案、各種xml資源等)的打包,生成R.java檔案。
  2. 通過AIDL工具處理AIDL檔案,生成相應的Java檔案。
  3. 通過Javac工具編譯專案原始碼,生成Class檔案。
  4. 通過DX工具將所有的Class檔案轉換成DEX檔案,該過程主要完成Java位元組碼轉換成Dalvik位元組碼,壓縮常量池以及清除冗餘資訊等工作。
  5. 通過ApkBuilder工具將資原始檔、DEX檔案打包生成APK檔案。
  6. 利用KeyStore對生成的APK檔案進行簽名。
  7. 如果是正式版的APK,還會利用ZipAlign工具進行對齊處理,對齊的過程就是將APK檔案中所有的資原始檔舉例檔案的起始距離都偏移4位元組的整數倍,這樣通過記憶體對映訪問APK檔案
    的速度會更快。

瞭解APK的安裝流程嗎,描述一下?

APK的安裝流程如下所示:

Android 面試題集 包含答案
  1. 複製APK到/data/app目錄下,解壓並掃描安裝包。
  2. 資源管理器解析APK裡的資原始檔。
  3. 解析AndroidManifest檔案,並在/data/data/目錄下建立對應的應用資料目錄。
  4. 然後對dex檔案進行優化,並儲存在dalvik-cache目錄下。
  5. 將AndroidManifest檔案解析出的四大元件資訊註冊到PackageManagerService中。
  6. 安裝完成後,傳送廣播。

當點選一個應用圖示以後,都發生了什麼,描述一下這個過程?

點選應用圖示後會去啟動應用的LauncherActivity,如果LancerActivity所在的程式沒有建立,還會建立新程式,整體的流程就是一個Activity的啟動流程。

Activity的啟動流程圖(放大可檢視)如下所示:

Android 面試題集 包含答案

整個流程涉及的主要角色有:

  • Instrumentation: 監控應用與系統相關的互動行為。
  • AMS:元件管理排程中心,什麼都不幹,但是什麼都管。
  • ActivityStarter:Activity啟動的控制器,處理Intent與Flag對Activity啟動的影響,具體說來有:1 尋找符合啟動條件的Activity,如果有多個,讓使用者選擇;2 校驗啟動引數的合法性;3 返回int引數,代表Activity是否啟動成功。
  • ActivityStackSupervisior:這個類的作用你從它的名字就可以看出來,它用來管理任務棧。
  • ActivityStack:用來管理任務棧裡的Activity。
  • ActivityThread:最終幹活的人,是ActivityThread的內部類,Activity、Service、BroadcastReceiver的啟動、切換、排程等各種操作都在這個類裡完成。

注:這裡單獨提一下ActivityStackSupervisior,這是高版本才有的類,它用來管理多個ActivityStack,早期的版本只有一個ActivityStack對應著手機螢幕,後來高版本支援多屏以後,就
有了多個ActivityStack,於是就引入了ActivityStackSupervisior用來管理多個ActivityStack。

整個流程主要涉及四個程式:

  • 呼叫者程式,如果是在桌面啟動應用就是Launcher應用程式。
  • ActivityManagerService等所在的System Server程式,該程式主要執行著系統服務元件。
  • Zygote程式,該程式主要用來fork新程式。
  • 新啟動的應用程式,該程式就是用來承載應用執行的程式了,它也是應用的主執行緒(新建立的程式就是主執行緒),處理元件生命週期、介面繪製等相關事情。

有了以上的理解,整個流程可以概括如下:

  1. 點選桌面應用圖示,Launcher程式將啟動Activity(MainActivity)的請求以Binder的方式傳送給了AMS。
  2. AMS接收到啟動請求後,交付ActivityStarter處理Intent和Flag等資訊,然後再交給ActivityStackSupervisior/ActivityStack
    處理Activity進棧相關流程。同時以Socket方式請求Zygote程式fork新程式。
  3. Zygote接收到新程式建立請求後fork出新程式。
  4. 在新程式裡建立ActivityThread物件,新建立的程式就是應用的主執行緒,在主執行緒裡開啟Looper訊息迴圈,開始處理建立Activity。
  5. ActivityThread利用ClassLoader去載入Activity、建立Activity例項,並回撥Activity的onCreate()方法。這樣便完成了Activity的啟動。

BroadcastReceiver與LocalBroadcastReceiver有什麼區別?

  • BroadcastReceiver 是跨應用廣播,利用Binder機制實現。
  • LocalBroadcastReceiver 是應用內廣播,利用Handler實現,利用了IntentFilter的match功能,提供訊息的釋出與接收功能,實現應用內通訊,效率比較高。

Android Handler機制是做什麼的,原理了解嗎?

Android訊息迴圈流程圖如下所示:

在這裡插入圖片描述
主要涉及的角色如下所示:

  • Message:訊息,分為硬體產生的訊息(例如:按鈕、觸控)和軟體產生的訊息。
  • MessageQueue:訊息佇列,主要用來向訊息池新增訊息和取走訊息。
  • Looper:訊息迴圈器,主要用來把訊息分發給相應的處理者。
  • Handler:訊息處理器,主要向訊息佇列傳送各種訊息以及處理各種訊息。

整個訊息的迴圈流程還是比較清晰的,具體說來:

  1. Handler通過sendMessage()傳送訊息Message到訊息佇列MessageQueue。
  2. Looper通過loop()不斷提取觸發條件的Message,並將Message交給對應的target handler來處理。
  3. target handler呼叫自身的handleMessage()方法來處理Message。

事實上,在整個訊息迴圈的流程中,並不只有Java層參與,很多重要的工作都是在C++層來完成的。我們來看下這些類的呼叫關係。

在這裡插入圖片描述
注:虛線表示關聯關係,實線表示呼叫關係。

在這些類中MessageQueue是Java層與C++層維繫的橋樑,MessageQueue與Looper相關功能都通過MessageQueue的Native方法來完成,而其他虛線連線的類只有關聯關係,並沒有
直接呼叫的關係,它們發生關聯的橋樑是MessageQueue。

Android Binder機制是做什麼的,為什麼選用Binder,原理了解嗎?

Android Binder是用來做程式通訊的,Android的各個應用以及系統服務都執行在獨立的程式中,它們的通訊都依賴於Binder。

為什麼選用Binder,在討論這個問題之前,我們知道Android也是基於Linux核心,Linux現有的程式通訊手段有以下幾種:

  1. 管道:在建立時分配一個page大小的記憶體,快取區大小比較有限;
  2. 訊息佇列:資訊複製兩次,額外的CPU消耗;不合適頻繁或資訊量大的通訊;
  3. 共享記憶體:無須複製,共享緩衝區直接付附加到程式虛擬地址空間,速度快;但程式間的同步問題作業系統無法實現,必須各程式利用同步工具解決;
  4. 套接字:作為更通用的介面,傳輸效率低,主要用於不通機器或跨網路的通訊;
  5. 訊號量:常作為一種鎖機制,防止某程式正在訪問共享資源時,其他程式也訪問該資源。因此,主要作為程式間以及同一程式內不同執行緒之間的同步手段。6. 訊號: 不適用於資訊交換,更適用於程式中斷控制,比如非法記憶體訪問,殺死某個程式等;

既然有現有的IPC方式,為什麼重新設計一套Binder機制呢。主要是出於以上三個方面的考量:

  • 高效能:從資料拷貝次數來看Binder只需要進行一次記憶體拷貝,而管道、訊息佇列、Socket都需要兩次,共享記憶體不需要拷貝,Binder的效能僅次於共享記憶體。
  • 穩定性:上面說到共享記憶體的效能優於Binder,那為什麼不適用共享記憶體呢,因為共享記憶體需要處理併發同步問題,控制負責,容易出現死鎖和資源競爭,穩定性較差。而Binder基於C/S架構,客戶端與服務端彼此獨立,穩定性較好。
  • 安全性:我們知道Android為每個應用分配了UID,用來作為鑑別程式的重要標誌,Android內部也依賴這個UID進行許可權管理,包括6.0以前的固定許可權和6.0以後的動態許可權,傳榮IPC只能由使用者在資料包裡填入UID/PID,這個標記完全
    是在使用者空間控制的,沒有放在核心空間,因此有被惡意篡改的可能,因此Binder的安全性更高。

描述一下Activity的生命週期,這些生命週期是如何管理的?

Activity與Fragment生命週期如下所示:

在這裡插入圖片描述
讀者可以從上圖看出,Activity有很多種狀態,狀態之間的變化也比較複雜,在眾多狀態中,只有三種是常駐狀態:

  • Resumed(執行狀態):Activity處於前臺,使用者可以與其互動。
  • Paused(暫停狀態):Activity被其他Activity部分遮擋,無法接受使用者的輸入。
  • Stopped(停止狀態):Activity被完全隱藏,對使用者不可見,進入後臺。

其他的狀態都是中間狀態。

我們再來看看生命週期變化時的整個排程流程,生命週期排程流程圖如下所示:

在這裡插入圖片描述
所以你可以看到,整個流程是這樣的:

  1. 比方說我們點選跳轉一個新Activity,這個時候Activity會入棧,同時它的生命週期也會從onCreate()到onResume()開始變換,這個過程是在ActivityStack裡完成的,ActivityStack
    是執行在Server程式裡的,這個時候Server程式就通過ApplicationThread的代理物件ApplicationThreadProxy向執行在app程式ApplicationThread發起操作請求。
  2. ApplicationThread接收到操作請求後,因為它是執行在app程式裡的其他執行緒裡,所以ApplicationThread需要通過Handler向主執行緒ActivityThread傳送操作訊息。
  3. 主執行緒接收到ApplicationThread發出的訊息後,呼叫主執行緒ActivityThread執行響應的操作,並回撥Activity相應的週期方法。

注:這裡提到了主執行緒ActivityThread,更準確來說ActivityThread不是執行緒,因為它沒有繼承Thread類或者實現Runnable介面,它是執行在應用主執行緒裡的物件,那麼應用的主執行緒
到底是什麼呢?從本質上來講啟動啟動時建立的程式就是主執行緒,執行緒和程式處理是否共享資源外,沒有其他的區別,對於Linux來說,它們都只是一個struct結構體。

Activity的通訊方式有哪些?

  • startActivityForResult
  • EventBus
  • LocalBroadcastReceiver

Android應用裡有幾種Context物件,

Context類圖如下所示:

在這裡插入圖片描述
可以發現Context是個抽象類,它的具體實現類是ContextImpl,ContextWrapper是個包裝類,內部的成員變數mBase指向的也是個ContextImpl物件,ContextImpl完成了
實際的功能,Activity、Service與Application都直接或者間接的繼承ContextWrapper。

描述一下程式和Application的生命週期?

一個安裝的應用對應一個LoadedApk物件,對應一個Application物件,對於四大元件,Application的建立和獲取方式也是不盡相同的,具體說來:

  • Activity:通過LoadedApk的makeApplication()方法建立。
  • Service:通過LoadedApk的makeApplication()方法建立。
  • 靜態廣播:通過其回撥方法onReceive()方法的第一個引數指向Application。
  • ContentProvider:無法獲取Application,因此此時Application不一定已經初始化。

Android哪些情況會導致記憶體洩漏,如何分析記憶體洩漏?

常見的產生記憶體洩漏的情況如下所示:

  • 持有靜態的Context(Activity)引用。
  • 持有靜態的View引用,
  • 內部類&匿名內部類例項無法釋放(有延遲時間等等),而內部類又持有外部類的強引用,導致外部類無法釋放,這種匿名內部類常見於監聽器、Handler、Thread、TimerTask
  • 資源使用完成後沒有關閉,例如:BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap。
  • 不正確的單例模式,比如單例持有Activity。
  • 集合類記憶體洩漏,如果一個集合類是靜態的(快取HashMap),只有新增方法,沒有對應的刪除方法,會導致引用無法被釋放,引發記憶體洩漏。
  • 錯誤的覆寫了finalize()方法,finalize()方法執行執行不確定,可能會導致引用無法被釋放。

查詢記憶體洩漏可以使用Android Profiler工具或者利用LeakCanary工具。

Android有哪幾種程式,是如何管理的?

Android的程式主要分為以下幾種:

前臺程式

使用者當前操作所必需的程式。如果一個程式滿足以下任一條件,即視為前臺程式:

  • 託管使用者正在互動的 Activity(已呼叫 Activity 的 onResume() 方法)
  • 託管某個 Service,後者繫結到使用者正在互動的 Activity
  • 託管正在“前臺”執行的 Service(服務已呼叫 startForeground())
  • 託管正執行一個生命週期回撥的 Service(onCreate()、onStart() 或 onDestroy())
  • 託管正執行其 onReceive() 方法的 BroadcastReceiver

通常,在任意給定時間前臺程式都為數不多。只有在記憶體不足以支援它們同時繼續執行這一萬不得已的情況下,系統才會終止它們。 此時,裝置往往已達到記憶體分頁狀態,因此需要終止一些前臺程式來確保使用者介面正常響應。

可見程式

沒有任何前臺元件、但仍會影響使用者在螢幕上所見內容的程式。 如果一個程式滿足以下任一條件,即視為可見程式:

  • 託管不在前臺、但仍對使用者可見的 Activity(已呼叫其 onPause() 方法)。例如,如果前臺 Activity 啟動了一個對話方塊,允許在其後顯示上一 Activity,則有可能會發生這種情況。
  • 託管繫結到可見(或前臺)Activity 的 Service。

可見程式被視為是極其重要的程式,除非為了維持所有前臺程式同時執行而必須終止,否則系統不會終止這些程式。

服務程式

正在執行已使用 startService() 方法啟動的服務且不屬於上述兩個更高類別程式的程式。儘管服務程式與使用者所見內容沒有直接關聯,但是它們通常在執行一些使用者關
心的操作(例如,在後臺播放音樂或從網路下載資料)。因此,除非記憶體不足以維持所有前臺程式和可見程式同時執行,否則系統會讓服務程式保持執行狀態。

後臺程式

包含目前對使用者不可見的 Activity 的程式(已呼叫 Activity 的 onStop() 方法)。這些程式對使用者體驗沒有直接影響,系統可能隨時終止它們,以回收記憶體供前臺程式、可見程式或服務程式使用。 通常會有很多後臺程式在執行,因此它們會儲存在 LRU (最近最少使用)列表中,以確保包含使用者最近檢視的 Activity 的程式最後一個被終止。如果某個 Activity 正確實現了生命週期方法,並儲存了其當前狀態,則終止其程式不會對使用者體驗產生明顯影響,因為當使用者導航回該 Activity 時,Activity 會恢復其所有可見狀態。

空程式

不含任何活動應用元件的程式。保留這種程式的的唯一目的是用作快取,以縮短下次在其中執行元件所需的啟動時間。 為使總體系統資源在程式快取和底層核心快取之間保持平衡,系統往往會終止這些程式。

ActivityManagerService負責根據各種策略演算法計算程式的adj值,然後交由系統核心進行程式的管理。

SharePreference效能優化,可以做程式同步嗎?

在Android中, SharePreferences是一個輕量級的儲存類,特別適合用於儲存軟體配置引數。使用SharedPreferences儲存資料,其背後是用xml檔案存放資料,檔案
存放在/data/data/ < package name > /shared_prefs目錄下.

之所以說SharedPreference是一種輕量級的儲存方式,是因為它在建立的時候會把整個檔案全部載入進記憶體,如果SharedPreference檔案比較大,會帶來以下問題:

  1. 第一次從sp中獲取值的時候,有可能阻塞主執行緒,使介面卡頓、掉幀。
  2. 解析sp的時候會產生大量的臨時物件,導致頻繁GC,引起介面卡頓。
  3. 這些key和value會永遠存在於記憶體之中,佔用大量記憶體。

優化建議

  1. 不要存放大的key和value,會引起介面卡、頻繁GC、佔用記憶體等等。
  2. 毫不相關的配置項就不要放在在一起,檔案越大讀取越慢。
  3. 讀取頻繁的key和不易變動的key儘量不要放在一起,影響速度,如果整個檔案很小,那麼忽略吧,為了這點效能新增維護成本得不償失。
  4. 不要亂edit和apply,儘量批量修改一次提交,多次apply會阻塞主執行緒。
  5. 儘量不要存放JSON和HTML,這種場景請直接使用JSON。
  6. SharedPreference無法進行跨程式通訊,MODE_MULTI_PROCESS只是保證了在API 11以前的系統上,如果sp已經被讀取進記憶體,再次獲取這個SharedPreference的時候,如果有這個flag,會重新讀一遍檔案,僅此而已。

如何做SQLite升級?

資料庫升級增加表和刪除表都不涉及資料遷移,但是修改表涉及到對原有資料進行遷移。升級的方法如下所示:

  1. 將現有表命名為臨時表。
  2. 建立新表。
  3. 將臨時表的資料匯入新表。
  4. 刪除臨時表。

重寫

如果是跨版本資料庫升級,可以由兩種方式,如下所示:

  1. 逐級升級,確定相鄰版本與現在版本的差別,V1升級到V2,V2升級到V3,依次類推。
  2. 跨級升級,確定每個版本與現在資料庫的差別,為每個case編寫專門升級大程式碼。

程式保護如何做,如何喚醒其他程式?

程式保活主要有兩個思路:

  1. 提升程式的優先順序,降低程式被殺死的概率。
  2. 拉活已經被殺死的程式。

如何提升優先順序,如下所示:

監控手機鎖屏事件,在螢幕鎖屏時啟動一個畫素的Activity,在使用者解鎖時將Activity銷燬掉,前臺Activity可以將程式變成前臺程式,優先順序升級到最高。

如果拉活

利用廣播拉活Activity。

理解序列化嗎,Android為什麼引入Parcelable?

所謂序列化就是將物件變成二進位制流,便於儲存和傳輸。

  • Serializable是java實現的一套序列化方式,可能會觸發頻繁的IO操作,效率比較低,適合將物件儲存到磁碟上的情況。
  • Parcelable是Android提供一套序列化機制,它將序列化後的位元組流寫入到一個共性記憶體中,其他物件可以從這塊共享記憶體中讀出位元組流,並反序列化成物件。因此效率比較高,適合在物件間或者程式間傳遞資訊。

如何計算一個Bitmap佔用記憶體的大小,怎麼保證載入Bitmap不產生記憶體溢位?

Bitamp 佔用記憶體大小 = 寬度畫素 x (inTargetDensity / inDensity) x 高度畫素 x (inTargetDensity / inDensity)x 一個畫素所佔的記憶體

注:這裡inDensity表示目標圖片的dpi(放在哪個資原始檔夾下),inTargetDensity表示目標螢幕的dpi,所以你可以發現inDensity和inTargetDensity會對Bitmap的寬高
進行拉伸,進而改變Bitmap佔用記憶體的大小。

在Bitmap裡有兩個獲取記憶體佔用大小的方法。

  • getByteCount():API12 加入,代表儲存 Bitmap 的畫素需要的最少記憶體。
  • getAllocationByteCount():API19 加入,代表在記憶體中為 Bitmap 分配的記憶體大小,代替了 getByteCount() 方法。

在不復用 Bitmap 時,getByteCount() 和 getAllocationByteCount 返回的結果是一樣的。在通過複用 Bitmap 來解碼圖片時,那麼 getByteCount() 表示新解碼圖片佔用記憶體的大
小,getAllocationByteCount() 表示被複用 Bitmap真實佔用的記憶體大小(即 mBuffer 的長度)。

為了保證在載入Bitmap的時候不產生記憶體溢位,可以受用BitmapFactory進行圖片壓縮,主要有以下幾個引數:

  • BitmapFactory.Options.inPreferredConfig:將ARGB_8888改為RGB_565,改變編碼方式,節約記憶體。
  • BitmapFactory.Options.inSampleSize:縮放比例,可以參考Luban那個庫,根據圖片寬高計算出合適的縮放比例。
  • BitmapFactory.Options.inPurgeable:讓系統可以記憶體不足時回收記憶體。

Android如何在不壓縮的情況下載入高清大圖?

使用BitmapRegionDecoder進行佈局載入。

Android裡的記憶體快取和磁碟快取是怎麼實現的。

記憶體快取基於LruCache實現,磁碟快取基於DiskLruCache實現。這兩個類都基於Lru演算法和LinkedHashMap來實現。

LRU演算法可以用一句話來描述,如下所示:

LRU是Least Recently Used的縮寫,最近最久未使用演算法,從它的名字就可以看出,它的核心原則是如果一個資料在最近一段時間沒有使用到,那麼它在將來被
訪問到的可能性也很小,則這類資料項會被優先淘汰掉。

LruCache的原理是利用LinkedHashMap持有物件的強引用,按照Lru演算法進行物件淘汰。具體說來假設我們從表尾訪問資料,在表頭刪除資料,當訪問的資料項在連結串列中存在時,則將該資料項移動到表尾,否則在表尾新建一個資料項。當連結串列容量超過一定閾值,則移除表頭的資料。

為什麼會選擇LinkedHashMap呢?

這跟LinkedHashMap的特性有關,LinkedHashMap的建構函式裡有個布林引數accessOrder,當它為true時,LinkedHashMap會以訪問順序為序排列元素,否則以插入順序為序排序元素。

DiskLruCache與LruCache原理相似,只是多了一個journal檔案來做磁碟檔案的管理和迎神,如下所示:

libcore.io.DiskLruCache
1
1
1

DIRTY 1517126350519
CLEAN 1517126350519 5325928
REMOVE 1517126350519

注:這裡的快取目錄是應用的快取目錄/data/data/pckagename/cache,未root的手機可以通過以下命令進入到該目錄中或者將該目錄整體拷貝出來:


//進入/data/data/pckagename/cache目錄
adb shell
run-as com.your.packagename 
cp /data/data/com.your.packagename/

//將/data/data/pckagename目錄拷貝出來
adb backup -noapk com.your.packagename

我們來分析下這個檔案的內容:

  • 第一行:libcore.io.DiskLruCache,固定字串。
  • 第二行:1,DiskLruCache原始碼版本號。
  • 第三行:1,App的版本號,通過open()方法傳入進去的。
  • 第四行:1,每個key對應幾個檔案,一般為1.
  • 第五行:空行
  • 第六行及後續行:快取操作記錄。

第六行及後續行表示快取操作記錄,關於操作記錄,我們需要了解以下三點:

  1. DIRTY 表示一個entry正在被寫入。寫入分兩種情況,如果成功會緊接著寫入一行CLEAN的記錄;如果失敗,會增加一行REMOVE記錄。注意單獨只有DIRTY狀態的記錄是非法的。
  2. 當手動呼叫remove(key)方法的時候也會寫入一條REMOVE記錄。
  3. READ就是說明有一次讀取的記錄。
  4. CLEAN的後面還記錄了檔案的長度,注意可能會一個key對應多個檔案,那麼就會有多個數字。

PathClassLoader與DexClassLoader有什麼區別?

  • PathClassLoader:只能載入已經安裝到Android系統的APK檔案,即/data/app目錄,Android預設的類載入器。
  • DexClassLoader:可以載入任意目錄下的dex、jar、apk、zip檔案。

WebView優化了解嗎,如何提高WebView的載入速度?

為什麼WebView載入會慢呢?

這是因為在客戶端中,載入H5頁面之前,需要先初始化WebView,在WebView完全初始化完成之前,後續的介面載入過程都是被阻塞的。

優化手段圍繞著以下兩個點進行:

  1. 預載入WebView。
  2. 載入WebView的同時,請求H5頁面資料。

因此常見的方法是:

  1. 全域性WebView。
  2. 客戶端代理頁面請求。WebView初始化完成後向客戶端請求資料。
  3. asset存放離線包。

除此之外還有一些其他的優化手段:

  • 指令碼執行慢,可以讓指令碼最後執行,不阻塞頁面解析。
  • DNS與連結慢,可以讓客戶端複用使用的域名與連結。
  • React框架程式碼執行慢,可以將這部分程式碼拆分出來,提前進行解析。

Java和JS的相互呼叫怎麼實現,有做過什麼優化嗎?

jockeyjs:https://github.com/tcoulter/jockeyjs

對協議進行統一的封裝和處理。

JNI瞭解嗎,Java與C++如何相互呼叫?

Java呼叫C++

  1. 在Java中宣告Native方法(即需要呼叫的本地方法)
  2. 編譯上述 Java原始檔javac(得到 .class檔案)
    3。 通過 javah 命令匯出JNI的標頭檔案(.h檔案)
  3. 使用 Java需要互動的原生程式碼 實現在 Java中宣告的Native方法
  4. 編譯.so庫檔案
  5. 通過Java命令執行 Java程式,最終實現Java呼叫原生程式碼

C++呼叫Java

  1. 從classpath路徑下搜尋ClassMethod這個類,並返回該類的Class物件。
  2. 獲取類的預設構造方法ID。
  3. 查詢例項方法的ID。
  4. 建立該類的例項。
  5. 呼叫物件的例項方法。
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod  
(JNIEnv *env, jclass cls)  
{  
    jclass clazz = NULL;  
    jobject jobj = NULL;  
    jmethodID mid_construct = NULL;  
    jmethodID mid_instance = NULL;  
    jstring str_arg = NULL;  
    // 1、從classpath路徑下搜尋ClassMethod這個類,並返回該類的Class物件  
    clazz = (*env)->FindClass(env, "com/study/jnilearn/ClassMethod");  
    if (clazz == NULL) {  
        printf("找不到'com.study.jnilearn.ClassMethod'這個類");  
        return;  
    }  

    // 2、獲取類的預設構造方法ID  
    mid_construct = (*env)->GetMethodID(env,clazz, "<init>","()V");  
    if (mid_construct == NULL) {  
        printf("找不到預設的構造方法");  
        return;  
    }  

    // 3、查詢例項方法的ID  
    mid_instance = (*env)->GetMethodID(env, clazz, "callInstanceMethod", "(Ljava/lang/String;I)V");  
    if (mid_instance == NULL) {  

        return;  
    }  

    // 4、建立該類的例項  
    jobj = (*env)->NewObject(env,clazz,mid_construct);  
    if (jobj == NULL) {  
        printf("在com.study.jnilearn.ClassMethod類中找不到callInstanceMethod方法");  
        return;  
    }  

    // 5、呼叫物件的例項方法  
    str_arg = (*env)->NewStringUTF(env,"我是例項方法");  
    (*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200);  

    // 刪除區域性引用  
    (*env)->DeleteLocalRef(env,clazz);  
    (*env)->DeleteLocalRef(env,jobj);  
    (*env)->DeleteLocalRef(env,str_arg);  
}  

瞭解外掛化和熱修復嗎,它們有什麼區別,理解它們的原理嗎?

  • 外掛化:外掛化是體現在功能拆分方面的,它將某個功能獨立提取出來,獨立開發,獨立測試,再插入到主應用中。依次來較少主應用的規模。
  • 熱修復:熱修復是體現在bug修復方面的,它實現的是不需要重新發版和重新安裝,就可以去修復已知的bug。

利用PathClassLoader和DexClassLoader去載入與bug類同名的類,替換掉bug類,進而達到修復bug的目的,原理是在app打包的時候阻止類打上CLASS_ISPREVERIFIED標誌,然後在
熱修復的時候動態改變BaseDexClassLoader物件間接引用的dexElements,替換掉舊的類。

目前熱修復框架主要分為兩大類:

  • Sophix:修改方法指標。
  • Tinker:修改dex陣列元素。

如何做效能優化?

  1. 節制的使用Service,當啟動一個Service時,系統總是傾向於保留這個Service依賴的程式,這樣會造成系統資源的浪費,可以使用IntentService,執行完成任務後會自動停止。
  2. 當介面不可見時釋放記憶體,可以重寫Activity的onTrimMemory()方法,然後監聽TRIM_MEMORY_UI_HIDDEN這個級別,這個級別說明使用者離開了頁面,可以考慮釋放記憶體和資源。
  3. 避免在Bitmap浪費過多的記憶體,使用壓縮過的圖片,也可以使用Fresco等庫來優化對Bitmap顯示的管理。
  4. 使用優化過的資料集合SparseArray代替HashMap,HashMap為每個鍵值都提供一個物件入口,使用SparseArray可以免去基本物件型別轉換為引用資料類想的時間。

如果防止過度繪製,如何做佈局優化?

  1. 使用include複用佈局檔案。
  2. 使用merge標籤避免巢狀佈局。
  3. 使用stub標籤僅在需要的時候在展示出來。

如何提交程式碼質量?

  1. 避免建立不必要的物件,儘可能避免頻繁的建立臨時物件,例如在for迴圈內,減少GC的次數。
  2. 儘量使用基本資料型別代替引用資料型別。
  3. 靜態方法呼叫效率高於動態方法,也可以避免建立額外物件。
  4. 對於基本資料型別和String型別的常量要使用static final修飾,這樣常量會在dex檔案的初始化器中進行初始化,使用的時候可以直接使用。
  5. 多使用系統API,例如陣列拷貝System.arrayCopy()方法,要比我們用for迴圈效率快9倍以上,因為系統API很多都是通過底層的彙編模式執行的,效率比較高。

有沒有遇到64k問題,為什麼會出現這個問題,如何解決?

  • 在DEX檔案中,method、field、class等的個數使用short型別來做索引,即兩個位元組(65535),method、field、class等均有此限制。
  • APK在安裝過程中會呼叫dexopt將DEX檔案優化成ODEX檔案,dexopt使用LinearAlloc來儲存應用資訊,關於LinearAlloc緩衝區大小,不同的版本經歷了4M/8M/16M的限制,超出
    緩衝區時就會丟擲INSTALL_FAILED_DEXOPT錯誤。

解決方案是Google的MultiDex方案,具體參見:配置方法數超過 64K 的應用

MVC、MVP與MVVM之間的對比分析?

在這裡插入圖片描述

  • MVC:PC時代就有的架構方案,在Android上也是最早的方案,Activity/Fragment這些上帝角色既承擔了V的角色,也承擔了C的角色,小專案開發起來十分順手,大專案就會遇到
    耦合過重,Activity/Fragment類過大等問題。
  • MVP:為了解決MVC耦合過重的問題,MVP的核心思想就是提供一個Presenter將檢視邏輯I和業務邏輯相分離,達到解耦的目的。
  • MVVM:使用ViewModel代替Presenter,實現資料與View的雙向繫結,這套框架最早使用的data-binding將資料繫結到xml裡,這麼做在大規模應用的時候是不行的,不過資料繫結是
    一個很有用的概念,後續Google又推出了ViewModel元件與LiveData元件。ViewModel元件規範了ViewModel所處的地位、生命週期、生產方式以及一個Activity下多個Fragment共享View
    Model資料的問題。LiveData元件則提供了在Java層面View訂閱ViewModel資料來源的實現方案。

本文僅是作者開源專案的一部分,整體包含:

  • Java面試題集(含答案)
  • Android面試題集(含答案)
  • Android開源庫面試題集(含答案)
  • Android網路程式設計面試題集(含答案)
  • 資料結構與演算法面試題集(含答案)
  • HR面試題集(含答案)

相關文章