Android面試題集(含答案)

手畫一下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()方法。

廣播傳輸的數據是否有限制,是多少,爲什麼要限制?

  1. 廣播是通過Intent攜帶需要傳遞的數據的
  2. Intent是通過Binder機制實現的
  3. Binder對數據大小有限制,不同room不一樣,一般爲1M

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左右,超過這個限制就會靜默崩潰。處理方式如下:

  • 進程內:文件緩存、磁盤緩存。
  • 進程間:通過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整體的的打包流程如下圖所示:

在這裏插入圖片描述

具體說來:

  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的安裝流程如下所示:
在這裏插入圖片描述apk_install_structure

  1. 複製APK到/data/app目錄下,解壓並掃描安裝包。
  2. 資源管理器解析APK裏的資源文件。
  3. 解析AndroidManifest文件,並在/data/data/目錄下創建對應的應用數據目錄。
  4. 然後對dex文件進行優化,並保存在dalvik-cache目錄下。
  5. 將AndroidManifest文件解析出的四大組件信息註冊到PackageManagerService中。
  6. 安裝完成後,發送廣播。

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

點擊應用圖標後會去啓動應用的LauncherActivity,如果LancerActivity所在的進程沒有創建,還會創建新進程,整體的流程就是一個Activity的啓動流程。

Activity的啓動流程圖(放大可查看)如下所示:

在這裏插入圖片描述

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

  • 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消息循環流程圖如下所示:

在這裏插入圖片描述https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/native/process/android_handler_structure.png

主要涉及的角色如下所示:

  • 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生命週期如下所示:

在這裏插入圖片描述https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/app/component/complete_android_fragment_lifecycle.png

讀者可以從上圖看出,Activity有很多種狀態,狀態之間的變化也比較複雜,在衆多狀態中,只有三種是常駐狀態:

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

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

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

在這裏插入圖片描述https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/app/component/activity_lifecycle_structure.png

所以你可以看到,整個流程是這樣的:

  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
  • LocalBroadcastReceiver

Android應用裏有幾種Context對象,

Context類圖如下所示:

在這裏插入圖片描述https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/app/component/context_uml.png

可以發現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數據源的實現方案。

你可能沒注意到的小知識

  1. YYYY表示的是這個周所屬的年份,yyyy才表示的是我們日常使用的年份。
  2. 子線程未必不能更新UI,更新UI的檢查方法是ViewRootImpl#checkThread(),然而ViewRootImpl的創建在onResume()之後,在onResume()之前更新UI不會報錯,但是不要這麼幹。
  3. 代碼生成的View沒有id,如果需要,可以調用View#generateViewId()方法來生成一個id。
  4. View#getContext()不一定會返回Activity,通過 new View、View.inflate、LayoutInflater.inflate 這幾種方式添加View,我們傳參時傳的是什麼context, View中的就是什麼Context。
  5. RemoteViews和View沒啥關係,RemoteViews提供一組基礎的操作用戶跨進程更新,主要用於通知欄和桌面小組件開發,該類實現了Oarcelable和Filte接口。
  6. getVisibility()只是判斷自身的顯示狀態,這個時候如果父級View不可見,則不能判斷對用戶來說是不是真的不可見,可以使用isShow()方法,它會循環判斷自身和父View,只要有一個不可見,它就會返回fasle。

參考地址:https://github.com/sucese/android-interview-guide

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