天天看點

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

轉載:https://juejin.im/post/5e5c5dea6fb9a07c8e6a36d1

前言

成為一名優秀的Android開發,需要一份完備的知識體系,在這裡,讓我們一起成長為自己所想的那樣~。

🔥 A awesome android expert interview questions and answers(continuous updating ...)

從幾十份頂級面試倉庫和300多篇高品質面經中總結出一份全面成體系化的Android進階面試題集。

歡迎來到2020年中進階Android大廠面試秘籍,為你保駕護航金三銀四,直通大廠的Android進階篇(上)。

Android進階面試題 (⭐⭐⭐)

一、性能優化

1、App穩定性優化

1、你們做了哪些穩定性方面的優化?

随着項目的逐漸成熟,使用者基數逐漸增多,DAU持續升高,我們遇到了很多穩定性方面的問題,對于我們技術同學遇到了很多的挑戰,使用者經常使用我們的App卡頓或者是功能不可用,是以我們就針對穩定性開啟了專項的優化,我們主要優化了三項:

  • Crash專項優化(=>2)
  • 性能穩定性優化(=>2)
  • 業務穩定性優化(=>3)

通過這三方面的優化我們搭建了移動端的高可用平台。同時,也做了很多的措施來讓App真正地實作了高可用。

2、性能穩定性是怎麼做的?

  • 全面的性能優化:啟動速度、記憶體優化、繪制優化
  • 線下發現問題、優化為主
  • 線上監控為主
  • Crash專項優化

我們針對啟動速度,記憶體、布局加載、卡頓、瘦身、流量、電量等多個方面做了多元的優化。

我們的優化主要分為了兩個層次,即線上和線下,針對于線下呢,我們側重于發現問題,直接解決,将問題盡可能在上線之前解決為目的。而真正到了線上呢,我們最主要的目的就是為了監控,對于各個性能緯度的監控呢,可以讓我們盡可能早地擷取到異常情況的報警。

同時呢,對于線上最嚴重的性能問題性問題:Crash,我們做了專項的優化,不僅優化了Crash的具體名額,而且也盡可能地擷取了Crash發生時的詳細資訊,結合後端的聚合、報警等功能,便于我們快速地定位問題。

3、業務穩定性如何保障?

  • 資料采集 + 報警
  • 需要對項目的主流程與核心路徑進行埋點監控,
  • 同時還需知道每一步發生了多少異常,這樣,我們就知道了所有業務流程的轉換率以及相應界面的轉換率
  • 結合大盤,如果轉換率低于某個值,進行報警
  • 異常監控 + 單點追查
  • 兜底政策

移動端業務高可用它側重于使用者功能完整可用,主要是為了解決一些線上一些異常情況導緻使用者他雖然沒有崩潰,也沒有性能問題,但是呢,隻是單純的功能不可用的情況,我們需要對項目的主流程、核心路徑進行埋點監控,來計算每一步它真實的轉換率是多少,同時呢,還需要知道在每一步到底發生了多少異常。這樣我們就知道了所有業務流程的轉換率以及相應界面的轉換率,有了大盤的資料呢,我們就知道了,如果轉換率或者是某些監控的成功率低于某個值,那很有可能就是出現了線上異常,結合了相應的報警功能,我們就不需要等使用者來回報了,這個就是業務穩定性保障的基礎。

同時呢,對于一些特殊情況,比如說,開發過程當中或代碼中出現了一些catch代碼塊,捕獲住了異常,讓程式不崩潰,這其實是不合理的,程式雖然沒有崩潰,當時程式的功能已經變得不可用,是以呢,這些被catch的異常我們也需要上報上來,這樣我們才能知道使用者到底出現了什麼問題而導緻的異常。此外,線上還有一些單點問題,比如說使用者點選登入一直進不去,這種就屬于單點問題,其實我們是無法找出其和其它問題的共性之處的,是以呢,我們就必須要找到它對應的詳細資訊。

最後,如果發生了異常情況,我們還采取了一系列措施進行快速止損。(=>4)

4、如果發生了異常情況,怎麼快速止損?

  • 功能開關
  • 統跳中心
  • 動态修複:熱修複、資源包更新
  • 自主修複:安全模式

首先,需要讓App具備一些進階的能力,我們對于任何要上線的新功能,要加上一個功能的開關,通過配置中心下發的開關呢,來決定是否要顯示新功能的入口。如果有異常情況,可以緊急關閉新功能的入口,那就可以讓這個App處于可控的狀态了。

然後,我們需要給App設立路由跳轉,所有的界面跳轉都需要通過路由來分發,如果我們比對到需要跳轉到有bug的這樣一個新功能時,那我們就不跳轉了,或者是跳轉到統一的異常正進行中的界面。如果這兩種方式都不可以,那就可以考慮通過熱修複的方式來動态修複,目前熱修複的方案其實已經比較成熟了,我們完全可以低成本地在我們的項目中添加熱修複的能力,當然,如果有些功能是由RN或WeeX來實作就更好了,那就可以通過更新資源包的方式來實作動态更新。而這些如果都不可以的話呢,那就可以考慮自己去給應用加上一個自主修複的能力,如果App啟動多次的話,那就可以考慮清空所有的緩存資料,将App重置到安裝的狀态,到了最嚴重的等級呢,可以阻塞主線程,此時一定要等App熱修複成功之後才允許使用者進入。

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

需要更全面更深入的了解請檢視深入探索Android穩定性優化

2、App啟動速度優化

1、啟動優化是怎麼做的?

  • 分析現狀、确認問題
  • 針對性優化(先概括,引導其深入)
  • 長期保持優化效果

在某一個版本之後呢,我們會發現這個啟動速度變得特别慢,同時使用者給我們的回報也越來越多,是以,我們開始考慮對應用的啟動速度來進行優化。然後,我們就對啟動的代碼進行了代碼層面的梳理,我們發現應用的啟動流程已經非常複雜,接着,我們通過一系列的工具來确認是否在主線程中執行了太多的耗時操作。

我們經過了細查代碼之後,發現應用主線程中的任務太多,我們就想了一個方案去針對性地解決,也就是進行異步初始化。(引導=>第2題) 然後,我們還發現了另外一個問題,也可以進行針對性的優化,就是在我們的初始化代碼當中有些的優先級并不是那麼高,它可以不放在Application的onCreate中執行,而完全可以放在之後延遲執行的,因為我們對這些代碼進行了延遲初始化,最後,我們還結合了idealHandler做了一個更優的延遲初始化的方案,利用它可以在主線程的空閑時間進行初始化,以減少啟動耗時導緻的卡頓現象。做完這些之後,我們的啟動速度就變得很快了。

最後,我簡單說下我們是怎麼長期來保持啟動優化的效果的。首先,我們做了我們的啟動器,并且結合了我們的CI,線上上加上了很多方面的監控。(引導=> 第4題)

2、是怎麼異步的,異步遇到問題沒有?

  • 展現演進過程
  • 詳細介紹啟動器

我們最初是采用的普通的一個異步的方案,即new Thread + 設定線程優先級為背景線程的方式在Application的onCreate方法中進行異步初始化,後來,我們使用了線程池、IntentService的方式,但是,在我們應用的演進過程當中,發現代碼會變得不夠優雅,并且有些場景非常不好處理,比如說多個初始化任務直接的依賴關系,比如說某一個初始化任務需要在某一個特定的生命周期中初始化完成,這些都是使用線程池、IntentService無法實作的。是以說,我們就開始思考一個新的解決方案,它能夠完美地解決我們剛剛所遇到的這些問題。

這個方案就是我們目前所使用的啟動器,在啟動器的概念中,我們将每一個初始化代碼抽象成了一個Task,然後,對它們進行了一個排序,根據它們之間的依賴關系排了一個有向無環圖,接着,使用一個異步隊列進行執行,并且這個異步隊列它和CPU的核心數是強烈相關的,它能夠最大程度地保證我們的主線程和别的線程都能夠執行我們的任務,也就是大家幾乎都可以同時完成。

3、啟動優化有哪些容易忽略的注意點?

  • cpu time與wall time
  • 注意延遲初始化的優化
  • 介紹下黑科技

首先,在CPU Profiler和Systrace中有兩個很重要的名額,即cpu time與wall time,我們必須清楚cpu time與wall time之間的差別,wall time指的是代碼執行的時間,而cpu time指的是代碼消耗CPU的時間,鎖沖突會造成兩者時間差距過大。我們需要以cpu time來作為我們優化的一個方向。

其次,我們不僅隻追求啟動速度上的一個提升,也需要注意延遲初始化的一個優化,對于延遲初始化,通常的做法是在界面顯示之後才去進行加載,但是如果此時界面需要進行滑動等與使用者互動的一系列操作,就會有很嚴重的卡頓現象,是以我們使用了idealHandler來實作cpu空閑時間來執行耗時任務,這極大地提升了使用者的體驗,避免了因啟動耗時任務而導緻的頁面卡頓現象。

最後,對于啟動優化,還有一些黑科技,首先,就是我們采用了類預先加載的方式,我們在MultiDex.install方法之後起了一個線程,然後用Class.forName的方式來預先觸發類的加載,然後當我們這個類真正被使用的時候,就不用再進行類加載的過程了。同時,我們再看Systrace圖的時候,有一部分手機其實并沒有給我們應用去跑滿cpu,比如說它有8核,但是卻隻給了我們4核等這些情況,然後,有些應用對此做了一些黑科技,它會将cpu的核心數以及cpu的頻率在啟動的時候去進行一個暴力的提升。

4、版本疊代導緻的啟動變慢有好的解決方式嗎?

  • 啟動器
  • 結合CI
  • 監控完善

這種問題其實我們之前也遇到過,這的确非常難以解決。但是,我們後面對此進行了反複的思考與嘗試,終于找到了一個比較好的解決方式。

首先,我們使用了啟動器去管理每一個初始化任務,并且啟動器中每一個任務的執行都是被其自動進行配置設定的,也就是說這些自動配置設定的task我們會盡量保證它會平均配置設定在我們每一個線程當中的,這和我們普通的異步是不一樣的,它可以很好地緩解我們應用的啟動變慢。

其次,我們還結合了CI,比如說,我們現在限制了一些類,如Application,如果有人修改了它,我們不會讓這部分代碼合并到主幹分支或者是修改之後會有一些内部的工具如郵件的形式發送到我,然後,我就會和他确認他加的這些代碼到底是耗時多少,能否異步初始化,不能異步的話就考慮延遲初始化,如果初始化時間太長,則可以考慮是否能進行懶加載,等用到的時候再去使用等等。

然後,我們會将問題盡可能地暴露在上線之前。同時,我們真正已經到了線上的一個環境下時,我們進行了監控的一個完善,我們不僅是監控了App的整個的啟動時間,同時呢,我們也将每一個生命周期都進行了一個監控。比如說Application的onCreate與onAttachBaseContext方法的耗時,以及這兩個生命周期之間間隔的時間,我們都進行了一個監控,如果說下一次我們發現了這個啟動速度變慢了,我們就可以去查找到底是哪一個環節變慢了,我們會和以前的版本進行對比,對比完成之後呢,我們就可以來找這一段新加的代碼。

5、開放問題:如果提高啟動速度,設計一個延遲加載架構或者sdk的方法和注意的問題

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)
2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

需要更全面更深入的了解請檢視深入探索Android啟動速度優化

3、App記憶體優化

1、你們記憶體優化項目的過程是怎麼做的?

1、分析現狀、确認問題

我們發現我們的APP在記憶體方面可能存在很大的問題,第一方面的原因是我們的線上的OOM率比較高。第二點呢,我們經常會看到在我們的Android Studio的Profiler工具中記憶體的抖動比較頻繁。這是我一個初步的現狀,然後在我們知道了這個初步的現狀之後,進行了問題的确認,我們經過一系列的調研以及深入研究,我們最終發現我們的項目中存在以下幾點大問題,比如說:記憶體抖動、記憶體溢出、記憶體洩漏,還有我們的Bitmap使用非常粗犷。

2、針對性優化

比如記憶體抖動的解決 -> Memory Profiler工具的使用(呈現了鋸齒張圖形) -> 分析到具體代碼存在的問題(頻繁被調用的方法中出現了日志字元串的拼接),也可以說說記憶體洩漏或記憶體溢出的解決。

3、效率提升

為了不增加業務同學的工作量,我們使用了一些工具類或ARTHook這樣的大圖檢測方案,沒有任何的侵入性,同時,我們将這些技術教給了大家,然後讓大家一起進行工作效率上的提升。

我們對記憶體優化工具Memory Profiler、MAT的使用比較熟悉,是以針對一系列不同問題的情況,我們寫了一系列解決方案的文檔,分享給大家。這樣,我們整個團隊成員的記憶體優化意識就變強了。

2、你做了記憶體優化最大的感受是什麼?

1、磨刀不誤砍柴工

我們一開始并沒有直接去分析項目中代碼哪些地方存在記憶體問題,而是先去學習了Google官方的一些文檔,比如說學習了Memory Profiler工具的使用、學習了MAT工具的使用,在我們将這些工具學習熟練之後,當在我們的項目中遇到記憶體問題時,我們就能夠很快地進行排查定位問題進行解決。

2、技術優化必須結合業務代碼

一開始,我們做了整體APP運作階段的一個記憶體上報,然後,我們在一些重點的記憶體消耗子產品進行了一些監控,但是後面發現這些監控并沒有緊密地結合我們的業務代碼,比如說在梳理完項目之後,發現我們項目中存在使用多個圖檔庫的情況,多個圖檔庫的記憶體緩存肯定是不公用的,是以導緻我們整個項目的記憶體使用量非常高。是以進行技術優化時必須結合我們的業務代碼。

3、系統化完善解決方案

我們在做記憶體優化的過程中,不僅做了Android端的優化工作,還将我們Android端一些資料的采集上報到了我們的伺服器,然後傳到我們的背景,這樣,友善我們的無論是Bug跟蹤人員或者是Crash跟蹤人員進行一系列問題的解決。

3、如何檢測所有不合理的地方?

比如說大圖檔的檢測,我們最初的一個方案是通過繼承ImageView,重寫它的onDraw方法來實作。但是,我們在推廣它的過程中,發現很多開發人員并不接受,因為很多ImageView之前已經寫過了,你現在讓他去替換,工作成本是比較高的。是以說,後來我們就想,有沒有一種方案可以免替換,最終我們就找到了ARTHook這樣一個Hook的方案。

如何避免記憶體抖動?(代碼注意事項)

記憶體抖動是由于短時間内有大量對象進出新生區導緻的,它伴随着頻繁的GC,gc會大量占用ui線程和cpu資源,會導緻app整體卡頓。

避免發生記憶體抖動的幾點建議:

  • 盡量避免在循環體内建立對象,應該把對象建立移到循環體外。
  • 注意自定義View的onDraw()方法會被頻繁調用,是以在這裡面不應該頻繁的建立對象。
  • 當需要大量使用Bitmap的時候,試着把它們緩存在數組或容器中實作複用。
  • 對于能夠複用的對象,同理可以使用對象池将它們緩存起來。
2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

需要更全面更深入的了解請檢視Android性能優化之記憶體優化、深入探索Android記憶體優化

4、App繪制優化

1、你在做布局優化的過程中用到了哪些工具?

我在做布局優化的過程中,用到了很多的工具,但是每一個工具都有它不同的使用場景,不同的場景應該使用不同的工具。下面我從線上和線下兩個角度來進行分析。

比如說,我要統計線上的FPS,我使用的就是Choreographer這個類,它具有以下特性:

  • 1、能夠擷取整體的幀率。
  • 2、能夠帶到線上使用。
  • 3、它擷取的幀率幾乎是實時的,能夠滿足我們的需求。

同時,線上下,如果要去優化布局加載帶來的時間消耗,那就需要檢測每一個布局的耗時,對此我使用的是AOP的方式,它沒有侵入性,同時也不需要别的開發同學進行接入,就可以友善地擷取每一個布局加載的耗時。如果還要更細粒度地去檢測每一個控件的加載耗時,那麼就需要使用LayoutInflaterCompat.setFactory2這個方法去進行Hook。

此外,我還使用了LayoutInspector和Systrace這兩個工具,Systrace可以很友善地看到每幀的具體耗時以及這一幀在布局當中它真正做了什麼。而LayoutInspector可以很友善地看到每一個界面的布局層級,幫助我們對層級進行優化。

2、布局為什麼會導緻卡頓,你又是如何優化的?

分析完布局的加載流程之後,我們發現有如下四點可能會導緻布局卡頓:

  • 1、首先,系統會将我們的Xml檔案通過IO的方式映射的方式加載到我們的記憶體當中,而IO的過程可能會導緻卡頓。
  • 2、其次,布局加載的過程是一個反射的過程,而反射的過程也會可能會導緻卡頓。
  • 3、同時,這個布局的層級如果比較深,那麼進行布局周遊的過程就會比較耗時。
  • 4、最後,不合理的嵌套RelativeLayout布局也會導緻重繪的次數過多。

對此,我們的優化方式有如下幾種:

  • 1、針對布局加載Xml檔案的優化,我們使用了異步Inflate的方式,即AsyncLayoutInflater。它的核心原理是在子線程中對我們的Layout進行加載,而加載完成之後會将View通過Handler發送到主線程來使用。是以不會阻塞我們的主線程,加載的時間全部是在異步線程中進行消耗的。而這僅僅是一個從側面緩解的思路。
  • 2、後面,我們發現了一個從根源解決上述痛點的方式,即使用X2C架構。它的一個核心原理就是在開發過程我們還是使用的XML進行編寫布局,但是在編譯的時候它會使用APT的方式将XML布局轉換為Java的方式進行布局,通過這樣的方式去寫布局,它有以下優點:1、它省去了使用IO的方式去加載XML布局的耗時過程。2、它是采用Java代碼直接new的方式去建立控件對象,是以它也沒有反射帶來的性能損耗。這樣就從根本上解決了布局加載過程中帶來的問題。
  • 3、然後,我們可以使用ConstraintLayout去減少我們界面布局的嵌套層級,如果原始布局層級越深,它能減少的層級就越多。而使用它也能避免嵌套RelativeLayout布局導緻的重繪次數過多。
  • 4、最後,我們可以使用AspectJ架構(即AOP)和LayoutInflaterCompat.setFactory2的方式分别去建立線下全局的布局加載速度和控件加載速度的監控體系。

3、做完布局優化有哪些成果産出?

  • 1、首先,我們建立了一個體系化的監控手段,這裡的體系還指的是線上加線下的一個綜合方案,針對線下,我們使用AOP或者ARTHook,可以很友善地擷取到每一個布局的加載耗時以及每一個控件的加載耗時。針對線上,我們通過Choreographer.getInstance().postFrameCallback的方式收集到了FPS,這樣我們可以知道使用者在哪些界面出現了丢幀的情況。
  • 2、然後,對于布局監控方面,我們設立了FPS、布局加載時間、布局層級等一系列名額。
  • 3、最後,在每一個版本上線之前,我們都會對我們的核心路徑進行一次Review,確定我們的FPS、布局加載時間、布局層級等達到一個合理的狀态。

4、你是怎麼做卡頓優化的?

從項目的初期到壯大期,最後再到成熟期,每一個階段都針對卡頓優化做了不同的處理。各個階段所做的事情如下所示:

  • 1、系統工具定位、解決
  • 2、自動化卡頓方案及優化
  • 3、線上監控及線下監測工具的建設

我做卡頓優化也是經曆了一些階段,最初我們的項目當中的一些子產品出現了卡頓之後,我是通過系統工具進行了定位,我使用了Systrace,然後看了卡頓周期内的CPU狀況,同時結合代碼,對這個子產品進行了重構,将部分代碼進行了異步和延遲,在項目初期就是這樣解決了問題。但是呢,随着我們項目的擴大,線下卡頓的問題也越來越多,同時,線上上,也有卡頓的回報,但是線上的回報卡頓,我們線上下難以複現,于是我們開始尋找自動化的卡頓監測方案,其思路是來自于Android的消息處理機制,主線程執行任何代碼都會回到Looper.loop方法當中,而這個方法中有一個mLogging對象,它會在每個message的執行前後都會被調用,我們就是利用這個前後處理的時機來做到的自動化監測方案的。同時,在這個階段,我們也完善了線上ANR的上報,我們采取的方式就是監控ANR的資訊,同時結合了ANR-WatchDog,作為高版本沒有檔案權限的一個補充方案。在做完這個卡頓檢測方案之後呢,我們還做了線上監控及線下檢測工具的建設,最終實作了一整套完善,多元度的解決方案。

5、你是怎麼樣自動化的擷取卡頓資訊?

我們的思路是來自于Android的消息處理機制,主線程執行任何代碼它都會走到Looper.loop方法當中,而這個函數當中有一個mLogging對象,它會在每個message處理前後都會被調用,而主線程發生了卡頓,那就一定會在dispatchMessage方法中執行了耗時的代碼,那我們在這個message執行之前呢,我們可以在子線程當中去postDelayed一個任務,這個Delayed的時間就是我們設定的門檻值,如果主線程的messaege在這個門檻值之内完成了,那就取消掉這個子線程當中的任務,如果主線程的message在門檻值之内沒有被完成,那子線程當中的任務就會被執行,它會擷取到目前主線程執行的一個堆棧,那我們就可以知道哪裡發生了卡頓。

經過實踐,我們發現這種方案擷取的堆棧資訊它不一定是準确的,因為擷取到的堆棧資訊它很可能是主線程最終執行的一個位置,而真正耗時的地方其實已經執行完成了,于是呢,我們就對這個方案做了一些優化,我們采取了高頻采集的方案,也就是在一個周期内我們會多次采集主線程的堆棧資訊,如果發生了卡頓,那我們就将這些卡頓資訊壓縮之後上報給APM背景,然後找出重複的堆棧資訊,這些重複發生的堆棧大機率就是卡頓發生的一個位置,這樣就提高了擷取卡頓資訊的一個準确性。

6、卡頓的一整套解決方案是怎麼做的?

首先,針對卡頓,我們采用了線上、線下工具相結合的方式,線下工具我們冊中醫藥盡可能早地去暴露問題,而針對于線上工具呢,我們側重于監控的全面性、自動化以及異常感覺的靈敏度。

同時呢,卡頓問題還有很多的難題。比如說有的代碼呢,它不到你卡頓的一個門檻值,但是執行過多,或者它錯誤地執行了很多次,它也會導緻使用者感官上的一個卡頓,是以我們線上下通過AOP的方式對常見的耗時代碼進行了Hook,然後對一段時間内擷取到的資料進行分析,我們就可以知道這些耗時的代碼發生的時機和次數以及耗時情況。然後,看它是不是滿足我們的一個預期,不滿足預期的話,我們就可以直接到線下進行修改。同時,卡頓監控它還有很多容易被忽略的一個盲區,比如說生命周期的一個間隔,那對于這種特定的問題呢,我們就采用了編譯時注解的方式修改了項目當中所有Handler的父類,對于其中的兩個方法進行了監控,我們就可以知道主線程message的執行時間以及它們的調用堆棧。

對于線上卡頓,我們除了計算App的卡頓率、ANR率等正常名額之外呢,我們還計算了頁面的秒開率、生命周期的執行時間等等。而且,在卡頓發生的時刻,我們也盡可能多地儲存下來了目前的一個場景資訊,這為我們之後解決或者複現這個卡頓留下了依據。

7、TextView setText耗時的原因,對TextView繪制層源碼的了解?

8、開放問題:優化一個清單頁面的打開速度和流暢性。

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

需要更全面更深入的了解請檢視Android性能優化之繪制優化、深入探索Android布局優化(上)、深入探索Android布局優化(下)、深入探索Android卡頓優化(上)、深入探索Android卡頓優化(下)

5、App瘦身

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

6、網絡優化

1、移動端擷取網絡資料優化的幾個點

  • 1、連接配接複用:節省連接配接建立時間,如開啟 keep-alive。于Android來說預設情況下HttpURLConnection和HttpClient都開啟了keep-alive。隻是2.2之前HttpURLConnection存在影響連接配接池的Bug。
  • 2、請求合并:即将多個請求合并為一個進行請求,比較常見的就是網頁中的CSS Image Sprites。如果某個頁面内請求過多,也可以考慮做一定的請求合并。
  • 3、減少請求資料的大小:對于post請求,body可以做gzip壓縮的,header也可以做資料壓縮(不過隻支援http 2.0)。 傳回資料的body也可以做gzip壓縮,body資料體積可以縮小到原來的30%左右(也可以考慮壓縮傳回的json資料的key資料的體積,尤其是針對傳回資料格式變化不大的情況,支付寶聊天傳回的資料用到了)。
  • 4、根據使用者的目前的網絡品質來判斷下載下傳什麼品質的圖檔(電商用的比較多)。
  • 5、使用HttpDNS優化DNS:DNS存在解析慢和DNS劫持等問題,DNS 不僅支援 UDP,它還支援 TCP,但是大部分标準的 DNS 都是基于 UDP 與 DNS 伺服器的 53 端口進行互動。HTTPDNS 則不同,顧名思義它是利用 HTTP 協定與 DNS 伺服器的 80 端口進行互動。不走傳統的 DNS 解析,進而繞過營運商的 LocalDNS 伺服器,有效的防止了域名劫持,提高域名解析的效率。
2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

參考文章

2、用戶端網絡安全實作

3、設計一個網絡優化方案,針對移動端弱網環境。

7、App電量優化

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

8、安卓的安全優化

1、提高app安全性的方法?

2、安卓的app加強如何做?

3、安卓的混淆原理是什麼?

4、談談你對安卓簽名的了解。

9、為什麼WebView加載會慢呢?

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

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

  • 預加載WebView。
  • 加載WebView的同時,請求H5頁面資料。

是以常見的方法是:

  • 全局WebView。
  • 用戶端代理頁面請求。WebView初始化完成後向用戶端請求資料。
  • asset存放離線包。

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

  • 腳本執行慢,可以讓腳本最後運作,不阻塞頁面解析。
  • DNS連結慢,可以讓用戶端複用使用的域名與連結。
  • React架構代碼執行慢,可以将這部分代碼拆分出來,提前進行解析。

10、如何優化自定義View

為了加速你的view,對于頻繁調用的方法,需要盡量減少不必要的代碼。先從onDraw開始,需要特别注意不應該在這裡做記憶體配置設定的事情,因為它會導緻GC,進而導緻卡頓。在初始化或者動畫間隙期間做配置設定記憶體的動作。不要在動畫正在執行的時候做記憶體配置設定的事情。

你還需要盡可能的減少onDraw被調用的次數,大多數時候導緻onDraw都是因為調用了invalidate().是以請盡量減少調用invaildate()的次數。如果可能的話,盡量調用含有4個參數的invalidate()方法而不是沒有參數的invalidate()。沒有參數的invalidate會強制重繪整個view。

另外一個非常耗時的操作是請求layout。任何時候執行requestLayout(),會使得Android UI系統去周遊整個View的層級來計算出每一個view的大小。如果找到有沖突的值,它會需要重新計算好幾次。另外需要盡量保持View的層級是扁平化的,這樣對提高效率很有幫助。

如果你有一個複雜的UI,你應該考慮寫一個自定義的ViewGroup來執行他的layout操作。與内置的view不同,自定義的view可以使得程式僅僅測量這一部分,這避免了周遊整個view的層級結構來計算大小。

11、FC(Force Close)什麼時候會出現?

Error、OOM,StackOverFlowError、Runtime,比如說空指針異常

解決的辦法:

  • 注意記憶體的使用和管理
  • 使用Thread.UncaughtExceptionHandler接口

12、Java多線程引發的性能問題,怎麼解決?

13、TraceView的實作原理,分析資料誤差來源。

14、是否使用過SysTrace,原理的了解?

15、mmap + native 日志優化?

傳統日志列印有兩個性能問題,一個是反複操作檔案描述符表,一個是反複進入核心态。是以需要使用mmap的方式去直接讀寫記憶體。

二、Android Framework相關

1、Android系統架構

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

Android 是一種基于 Linux 的開放源代碼軟體棧,為廣泛的裝置和機型而建立。下圖所示為 Android 平台的五大元件:

1.應用程式

Android 随附一套用于電子郵件、短信、月曆、網際網路浏覽和聯系人等的核心應用。平台随附的應用與使用者可以選擇安裝的應用一樣,沒有特殊狀态。是以第三方應用可成為使用者的預設網絡浏覽器、短信 Messenger 甚至預設鍵盤(有一些例外,例如系統的“設定”應用)。

系統應用可用作使用者的應用,以及提供開發者可從其自己的應用通路的主要功能。例如,如果您的應用要發短信,您無需自己建構該功能,可以改為調用已安裝的短信應用向您指定的接收者發送消息。

2、Java API 架構

您可通過以 Java 語言編寫的 API 使用 Android OS 的整個功能集。這些 API 形成建立 Android 應用所需的建構塊,它們可簡化核心子產品化系統元件和服務的重複使用,包括以下元件和服務:

  • 豐富、可擴充的視圖系統,可用以建構應用的 UI,包括清單、網格、文本框、按鈕甚至可嵌入的網絡浏覽器
  • 資料總管,用于通路非代碼資源,例如本地化的字元串、圖形和布局檔案
  • 通知管理器,可讓所有應用在狀态欄中顯示自定義提醒
  • Activity 管理器,用于管理應用的生命周期,提供常見的導航傳回棧
  • 内容提供程式,可讓應用通路其他應用(例如“聯系人”應用)中的資料或者共享其自己的資料

開發者可以完全通路 Android 系統應用使用的架構 API。

3、系統運作庫

1)原生 C/C++ 庫

許多核心 Android 系統元件和服務(例如 ART 和 HAL)建構自原生代碼,需要以 C 和 C++ 編寫的原生庫。Android 平台提供 Java 架構 API 以向應用顯示其中部分原生庫的功能。例如,您可以通過 Android 架構的 Java OpenGL API 通路 OpenGL ES,以支援在應用中繪制和操作 2D 和 3D 圖形。如果開發的是需要 C 或 C++ 代碼的應用,可以使用 Android NDK 直接從原生代碼通路某些原生平台庫。

2)Android Runtime

對于運作 Android 5.0(API 級别 21)或更高版本的裝置,每個應用都在其自己的程序中運作,并且有其自己的 Android Runtime (ART) 執行個體。ART 編寫為通過執行 DEX 檔案在低記憶體裝置上運作多個虛拟機,DEX 檔案是一種專為 Android 設計的位元組碼格式,經過優化,使用的記憶體很少。編譯工具鍊(例如 Jack)将 Java 源代碼編譯為 DEX 位元組碼,使其可在 Android 平台上運作。

ART 的部分主要功能包括:

  • 預先 (AOT) 和即時 (JIT) 編譯
  • 優化的垃圾回收 (GC)
  • 更好的調試支援,包括專用采樣分析器、詳細的診斷異常和崩潰報告,并且能夠設定監視點以監控特定字段

在 Android 版本 5.0(API 級别 21)之前,Dalvik 是 Android Runtime。如果您的應用在 ART 上運作效果很好,那麼它應該也可在 Dalvik 上運作,但反過來不一定。

Android 還包含一套核心運作時庫,可提供 Java API 架構使用的 Java 程式設計語言大部分功能,包括一些 Java 8 語言功能。

4、硬體抽象層 (HAL)

硬體抽象層 (HAL) 提供标準界面,向更進階别的 Java API 架構顯示裝置硬體功能。HAL 包含多個庫子產品,其中每個子產品都為特定類型的硬體元件實作一個界面,例如相機或藍牙子產品。當架構 API 要求通路裝置硬體時,Android 系統将為該硬體元件加載庫子產品。

5、Linux 核心

Android 平台的基礎是 Linux 核心。例如,Android Runtime (ART) 依靠 Linux 核心來執行底層功能,例如線程和低層記憶體管理。使用 Linux 核心可讓 Android 利用主要安全功能,并且允許裝置制造商為著名的核心開發硬體驅動程式。

對于Android應用開發來說,最好能手繪下面的系統架構圖:

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

2、View的事件分發機制?滑動沖突怎麼解決?

了解Activity的構成

一個Activity包含了一個Window對象,這個對象是由PhoneWindow來實作的。PhoneWindow将DecorView作為整個應用視窗的根View,而這個DecorView又将螢幕劃分為兩個區域:一個是TitleView,另一個是ContentView,而我們平時所寫的就是展示在ContentView中的。

觸摸事件的類型

觸摸事件對應的是MotionEvent類,事件的類型主要有如下三種:

  • ACTION_DOWN
  • ACTION_MOVE(移動的距離超過一定的門檻值會被判定為ACTION_MOVE操作)
  • ACTION_UP

View事件分發本質就是對MotionEvent事件分發的過程。即當一個MotionEvent發生後,系統将這個點選事件傳遞到一個具體的View上。

事件分發流程

事件分發過程由三個方法共同完成:

dispatchTouchEvent:方法傳回值為true表示事件被目前視圖消費掉;傳回為super.dispatchTouchEvent表示繼續分發該事件,傳回為false表示交給父類的onTouchEvent處理。

onInterceptTouchEvent:方法傳回值為true表示攔截這個事件并交由自身的onTouchEvent方法進行消費;傳回false表示不攔截,需要繼續傳遞給子視圖。如果return super.onInterceptTouchEvent(ev), 事件攔截分兩種情況:  

  • 1.如果該View存在子View且點選到了該子View, 則不攔截, 繼續分發 給子View 處理, 此時相當于return false。
  • 2.如果該View沒有子View或者有子View但是沒有點選中子View(此時ViewGroup 相當于普通View), 則交由該View的onTouchEvent響應,此時相當于return true。

注意:一般的LinearLayout、 RelativeLayout、FrameLayout等ViewGroup預設不攔截, 而 ScrollView、ListView等ViewGroup則可能攔截,得看具體情況。

onTouchEvent:方法傳回值為true表示目前視圖可以處理對應的事件;傳回值為false表示目前視圖不處理這個事件,它會被傳遞給父視圖的onTouchEvent方法進行處理。如果return super.onTouchEvent(ev),事件處理分為兩種情況:

  • 1.如果該View是clickable或者longclickable的,則會傳回true, 表示消費 了該事件, 與傳回true一樣;
  • 2.如果該View不是clickable或者longclickable的,則會傳回false, 表示不 消費該事件,将會向上傳遞,與傳回false一樣。

注意:在Android系統中,擁有事件傳遞處理能力的類有以下三種:

  • Activity:擁有分發和消費兩個方法。
  • ViewGroup:擁有分發、攔截和消費三個方法。
  • View:擁有分發、消費兩個方法。

三個方法的關系用僞代碼表示如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    } else {
        coonsume = child.dispatchTouchEvent(ev);
    }
    
    return consume;
}
複制代碼
           

通過上面的僞代碼,我們可以大緻了解點選事件的傳遞規則:對應一個根ViewGroup來說,點選事件産生後,首先會傳遞給它,這是它的dispatchTouchEvent就會被調用,如果這個ViewGroup的onInterceptTouchEvent方法傳回true就表示它要攔截目前事件,接着事件就會交給這個ViewGroup處理,這時如果它的mOnTouchListener被設定,則onTouch會被調用,否則onTouchEvent會被調用。在onTouchEvent中,如果設定了mOnCLickListener,則onClick會被調用。隻要View的CLICKABLE和LONG_CLICKABLE有一個為true,onTouchEvent()就會傳回true消耗這個事件。如果這個ViewGroup的onInterceptTouchEvent方法傳回false就表示它不攔截目前事件,這時目前事件就會繼續傳遞給它的子元素,接着子元素的dispatchTouchEvent方法就會被調用,如此反複直到事件被最終處理。

一些重要的結論:

1、事件傳遞優先級:onTouchListener.onTouch > onTouchEvent > onClickListener.onClick。

2、正常情況下,一個時間序列隻能被一個View攔截且消耗。因為一旦一個元素攔截了此事件,那麼同一個事件序列内的所有事件都會直接交給它處理(即不會再調用這個View的攔截方法去詢問它是否要攔截了,而是把剩餘的ACTION_MOVE、ACTION_DOWN等事件直接交給它來處理)。特例:通過将重寫View的onTouchEvent傳回false可強行将事件轉交給其他View處理。

3、如果View不消耗除ACTION_DOWN以外的其他事件,那麼這個點選事件會消失,此時父元素的onTouchEvent并不會被調用,并且目前View可以持續收到後續的事件,最終這些消失的點選事件會傳遞給Activity處理。

4、ViewGroup預設不攔截任何事件(傳回false)。

5、View的onTouchEvent預設都會消耗事件(傳回true),除非它是不可點選的(clickable和longClickable同時為false)。View的longClickable屬性預設都為false,clickable屬性要分情況,比如Button的clickable屬性預設為true,而TextView的clickable預設為false。

6、View的enable屬性不影響onTouchEvent的預設傳回值。

7、通過requestDisallowInterceptTouchEvent方法可以在子元素中幹預父元素的事件分發過程,但是ACTION_DOWN事件除外。

記住這個圖的傳遞順序,面試的時候能夠畫出來,就很詳細了:

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

ACTION_CANCEL什麼時候觸發,觸摸button然後滑動到外部擡起會觸發點選事件嗎,再滑動回去擡起會麼?

  • 一般ACTION_CANCEL和ACTION_UP都作為View一段事件處理的結束。如果在父View中攔截ACTION_UP或ACTION_MOVE,在第一次父視圖攔截消息的瞬間,父視圖指定子視圖不接受後續消息了,同時子視圖會收到ACTION_CANCEL事件。
  • 如果觸摸某個控件,但是又不是在這個控件的區域上擡起(移動到别的地方了),就會出現action_cancel。

點選事件被攔截,但是想傳到下面的View,如何操作?

重寫子類的requestDisallowInterceptTouchEvent()方法傳回true就不會執行父類的onInterceptTouchEvent(),即可将點選事件傳到下面的View。

如何解決View的事件沖突?舉個開發中遇到的例子?

常見開發中事件沖突的有ScrollView與RecyclerView的滑動沖突、RecyclerView内嵌同時滑動同一方向。

滑動沖突的處理規則:

  • 對于由于外部滑動和内部滑動方向不一緻導緻的滑動沖突,可以根據滑動的方向判斷誰來攔截事件。
  • 對于由于外部滑動方向和内部滑動方向一緻導緻的滑動沖突,可以根據業務需求,規定何時讓外部View攔截事件,何時由内部View攔截事件。
  • 對于上面兩種情況的嵌套,相對複雜,可同樣根據需求在業務上找到突破點。

滑動沖突的實作方法:

  • 外部攔截法:指點選事件都先經過父容器的攔截處理,如果父容器需要此事件就攔截,否則就不攔截。具體方法:需要重寫父容器的onInterceptTouchEvent方法,在内部做出相應的攔截。
  • 内部攔截法:指父容器不攔截任何事件,而将所有的事件都傳遞給子容器,如果子容器需要此事件就直接消耗,否則就交由父容器進行處理。具體方法:需要配合requestDisallowInterceptTouchEvent方法。

加深了解,GOGOGO

3、View的繪制流程?

DecorView被加載到Window中

  • 從Activity的startActivity開始,最終調用到ActivityThread的handleLaunchActivity方法來建立Activity,首先,會調用performLaunchActivity方法,内部會執行Activity的onCreate方法,進而完成DecorView和Activity的建立。然後,會調用handleResumeActivity,裡面首先會調用performResumeActivity去執行Activity的onResume()方法,執行完後會得到一個ActivityClientRecord對象,然後通過r.window.getDecorView()的方式得到DecorView,然後會通過a.getWindowManager()得到WindowManager,最終調用其addView()方法将DecorView加進去。
  • WindowManager的實作類是WindowManagerImpl,它内部會将addView的邏輯委托給WindowManagerGlobal,可見這裡使用了接口隔離和委托模式将實作和抽象充分解耦。在WindowManagerGlobal的addView()方法中不僅會将DecorView添加到Window中,同時會建立ViewRootImpl對象,并将ViewRootImpl對象和DecorView通過root.setView()把DecorView加載到Window中。這裡的ViewRootImpl是ViewRoot的實作類,是連接配接WindowManager和DecorView的紐帶。View的三大流程均是通過ViewRoot來完成的。

了解繪制的整體流程

繪制會從根視圖ViewRoot的performTraversals()方法開始,從上到下周遊整個視圖樹,每個View控件負責繪制自己,而ViewGroup還需要負責通知自己的子View進行繪制操作。

了解MeasureSpec

MeasureSpec表示的是一個32位的整形值,它的高2位表示測量模式SpecMode,低30位表示某種測量模式下的規格大小SpecSize。MeasureSpec是View類的一個靜态内部類,用來說明應該如何測量這個View。它由三種測量模式,如下:

  • EXACTLY:精确測量模式,視圖寬高指定為match_parent或具體數值時生效,表示父視圖已經決定了子視圖的精确大小,這種模式下View的測量值就是SpecSize的值。
  • AT_MOST:最大值測量模式,當視圖的寬高指定為wrap_content時生效,此時子視圖的尺寸可以是不超過父視圖允許的最大尺寸的任何尺寸。
  • UNSPECIFIED:不指定測量模式, 父視圖沒有限制子視圖的大小,子視圖可以是想要的任何尺寸,通常用于系統内部,應用開發中很少用到。

MeasureSpec通過将SpecMode和SpecSize打包成一個int值來避免過多的對象記憶體配置設定,為了友善操作,其提供了打包和解包的方法,打包方法為makeMeasureSpec,解包方法為getMode和getSize。

普通View的MeasureSpec的建立規則如下:

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

對于DecorView而言,它的MeasureSpec由視窗尺寸和其自身的LayoutParams共同決定;對于普通的View,它的MeasureSpec由父視圖的MeasureSpec和其自身的LayoutParams共同決定。

如何根據MeasureSpec去實作一個瀑布流的自定義ViewGroup?

View繪制流程之Measure

  • 首先,在ViewGroup中的measureChildren()方法中會周遊測量ViewGroup中所有的View,當View的可見性處于GONE狀态時,不對其進行測量。
  • 然後,測量某個指定的View時,根據父容器的MeasureSpec和子View的LayoutParams等資訊計算子View的MeasureSpec。
  • 最後,将計算出的MeasureSpec傳入View的measure方法,這裡ViewGroup沒有定義測量的具體過程,因為ViewGroup是一個抽象類,其測量過程的onMeasure方法需要各個子類去實作。不同的ViewGroup子類有不同的布局特性,這導緻它們的測量細節各不相同,如果需要自定義測量過程,則子類可以重寫這個方法。(setMeasureDimension方法用于設定View的測量寬高,如果View沒有重寫onMeasure方法,則會預設調用getDefaultSize來獲得View的寬高)

getSuggestMinimumWidth分析

如果View沒有設定背景,那麼傳回android:minWidth這個屬性所指定的值,這個值可以為0;如果View設定了背景,則傳回android:minWidth和背景的最小寬度這兩者中的最大值。

自定義View時手動處理wrap_content時的情形

直接繼承View的控件需要重寫onMeasure方法并設定wrap_content時的自身大小,否則在布局中使用wrap_content就相當于使用match_parent。此時,可以在wrap_content的情況下(對應MeasureSpec.AT_MOST)指定内部寬/高(mWidth和mHeight)。

LinearLayout的onMeasure方法實作解析(這裡僅分析measureVertical核心源碼)

系統會周遊子元素并對每個子元素執行measureChildBeforeLayout方法,這個方法内部會調用子元素的measure方法,這樣各個子元素就開始依次進入measure過程,并且系統會通過mTotalLength這個變量來存儲LinearLayout在豎直方向的初步高度。每測量一個子元素,mTotalLength就會增加,增加的部分主要包括了子元素的高度以及子元素在豎直方向上的margin等。

在Activity中擷取某個View的寬高

由于View的measure過程和Activity的生命周期方法不是同步執行的,如果View還沒有測量完畢,那麼獲得的寬/高就是0。是以在onCreate、onStart、onResume中均無法正确得到某個View的寬高資訊。解決方式如下:

  • Activity/View#onWindowFocusChanged:此時View已經初始化完畢,當Activity的視窗得到焦點和失去焦點時均會被調用一次,如果頻繁地進行onResume和onPause,那麼onWindowFocusChanged也會被頻繁地調用。
  • view.post(runnable): 通過post可以将一個runnable投遞到消息隊列的尾部,始化好了然後等待Looper調用次runnable的時候,View也已經初始化好了。
  • ViewTreeObserver#addOnGlobalLayoutListener:當View樹的狀态發生改變或者View樹内部的View的可見性發生改變時,onGlobalLayout方法将被回調。
  • View.measure(int widthMeasureSpec, int heightMeasureSpec):match_parent時不知道parentSize的大小,測不出;具體數值時,直接makeMeasureSpec固定值,然後調用view..measure就可以了;wrap_content時,在最大化模式下,用View理論上能支援的最大值去構造MeasureSpec是合理的。

View的繪制流程之Layout

首先,會通過setFrame方法來設定View的四個頂點的位置,即View在父容器中的位置。然後,會執行到onLayout空方法,子類如果是ViewGroup類型,則重寫這個方法,實作ViewGroup中所有View控件布局流程。

LinearLayout的onLayout方法實作解析(layoutVertical核心源碼)

其中會周遊調用每個子View的setChildFrame方法為子元素确定對應的位置。其中的childTop會逐漸增大,意味着後面的子元素會被放置在靠下的位置。

注意:在View的預設實作中,View的測量寬/高和最終寬/高是相等的,隻不過測量寬/高形成于View的measure過程,而最終寬/高形成于View的layout過程,即兩者的指派時機不同,測量寬/高的指派時機稍微早一些。在一些特殊的情況下則兩者不相等:

  • 重寫View的layout方法,使最終寬度總是比測量寬/高大100px。
  • View需要多次measure才能确定自己的測量寬/高,在前幾次測量的過程中,其得出的測量寬/高有可能和最終寬/高不一緻,但最終來說,測量寬/高還是和最終寬/高相同。

View的繪制流程之Draw

Draw的基本流程

繪制基本上可以分為六個步驟:

  • 首先繪制View的背景;
  • 如果需要的話,保持canvas的圖層,為fading做準備;
  • 然後,繪制View的内容;
  • 接着,繪制View的子View;
  • 如果需要的話,繪制View的fading邊緣并恢複圖層;
  • 最後,繪制View的裝飾(例如滾動條等等)。

setWillNotDraw的作用

如果一個View不需要繪制任何内容,那麼設定這個标記位為true以後,系統會進行相應的優化。

  • 預設情況下,View沒有啟用這個優化标記位,但是ViewGroup會預設啟用這個優化标記位。
  • 當我們的自定義控件繼承于ViewGroup并且本身不具備繪制功能時,就可以開啟這個标記位進而便于系統進行後續的優化。
  • 當明确知道一個ViewGroup需要通過onDraw來繪制内容時,我們需要顯示地關閉WILL_NOT_DRAW這個标記位。

Requestlayout,onlayout,onDraw,DrawChild差別與聯系?

requestLayout()方法 :會導緻調用 measure()過程 和 layout()過程,将會根據标志位判斷是否需要ondraw。

onLayout()方法:如果該View是ViewGroup對象,需要實作該方法,對每個子視圖進行布局。

onDraw()方法:繪制視圖本身 (每個View都需要重載該方法,ViewGroup不需要實作該方法)。

drawChild():去重新回調每個子視圖的draw()方法。

invalidate() 和 postInvalidate()的差別 ?

invalidate()與postInvalidate()都用于重新整理View,主要差別是invalidate()在主線程中調用,若在子線程中使用需要配合handler;而postInvalidate()可在子線程中直接調用。

更詳細的内容請點選這裡

4、跨程序通信。

Android中程序和線程的關系?差別?

  • 線程是CPU排程的最小單元,同時線程是一種有限的系統資源;而程序一般指一個執行單元,在PC和移動裝置上指一個程式或者一個應用。
  • 一般來說,一個App程式至少有一個程序,一個程序至少有一個線程(包含與被包含的關系),通俗來講就是,在App這個工廠裡面有一個程序,線程就是裡面的生産線,但主線程(即主生産線)隻有一條,而子線程(即副生産線)可以有多個。
  • 程序有自己獨立的位址空間,而程序中的線程共享此位址空間,都可以并發執行。

如何開啟多程序?應用是否可以開啟N個程序?

在AndroidManifest中給四大元件指定屬性android:process開啟多程序模式,在記憶體允許的條件下可以開啟N個程序。

為何需要IPC?多程序通信可能會出現的問題?

所有運作在不同程序的四大元件(Activity、Service、Receiver、ContentProvider)共享資料都會失敗,這是由于Android為每個應用配置設定了獨立的虛拟機,不同的虛拟機在記憶體配置設定上有不同的位址空間,這會導緻在不同的虛拟機中通路同一個類的對象會産生多份副本。比如常用例子(通過開啟多程序擷取更大記憶體空間、兩個或者多個應用之間共享資料、微信全家桶)。

一般來說,使用多程序通信會造成如下幾方面的問題:

  • 靜态成員和單例模式完全失效:獨立的虛拟機造成。
  • 線程同步機制完全失效:獨立的虛拟機造成。
  • SharedPreferences的可靠性下降:這是因為Sp不支援兩個程序并發進行讀寫,有一定幾率導緻資料丢失。
  • Application會多次建立:Android系統在建立新的程序時會配置設定獨立的虛拟機,是以這個過程其實就是啟動一個應用的過程,自然也會建立新的Application。

Android中IPC方式、各種方式優缺點?

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

講講AIDL?如何優化多子產品都使用AIDL的情況?

AIDL(Android Interface Definition Language,Android接口定義語言):如果在一個程序中要調用另一個程序中對象的方法,可使用AIDL生成可序列化的參數,AIDL會生成一個服務端對象的代理類,通過它用戶端可以實作間接調用服務端對象的方法。

AIDL的本質是系統提供了一套可快速實作Binder的工具。關鍵類和方法:

  • AIDL接口:繼承IInterface。
  • Stub類:Binder的實作類,服務端通過這個類來提供服務。
  • Proxy類:服務端的本地代理,用戶端通過這個類調用服務端的方法。
  • asInterface():用戶端調用,将服務端傳回的Binder對象,轉換成用戶端所需要的AIDL接口類型的對象。如果用戶端和服務端位于同一程序,則直接傳回Stub對象本身,否則傳回系統封裝後的Stub.proxy對象。
  • asBinder():根據目前調用情況傳回代理Proxy的Binder對象。
  • onTransact():運作在服務端的Binder線程池中,當用戶端發起跨程序請求時,遠端請求會通過系統底層封裝後交由此方法來處理。
  • transact():運作在用戶端,當用戶端發起遠端請求的同時将目前線程挂起。之後調用服務端的onTransact()直到遠端請求傳回,目前線程才繼續執行。

當有多個業務子產品都需要AIDL來進行IPC,此時需要為每個子產品建立特定的aidl檔案,那麼相應的Service就會很多。必然會出現系統資源耗費嚴重、應用過度重量級的問題。解決辦法是建立Binder連接配接池,即将每個業務子產品的Binder請求統一轉發到一個遠端Service中去執行,進而避免重複建立Service。

工作原理:每個業務子產品建立自己的AIDL接口并實作此接口,然後向服務端提供自己的唯一辨別和其對應的Binder對象。服務端隻需要一個Service并提供一個queryBinder接口,它會根據業務子產品的特征來傳回相應的Binder對象,不同的業務子產品拿到所需的Binder對象後就可以進行遠端方法的調用了。

為什麼選擇Binder?

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

  • 管道:在建立時配置設定一個page大小的記憶體,緩存區大小比較有限;
  • 消息隊列:資訊複制兩次,額外的CPU消耗;不合适頻繁或資訊量大的通信;
  • 共享記憶體:無須複制,共享緩沖區直接附加到程序虛拟位址空間,速度快;但程序間的同步問題作業系統無法實作,必須各程序利用同步工具解決;
  • 套接字:作為更通用的接口,傳輸效率低,主要用于不同機器或跨網絡的通信;
  • 信号量:常作為一種鎖機制,防止某程序正在通路共享資源時,其他程序也通路該資源。是以,主要作為程序間以及同一程序内不同線程之間的同步手段。 不适用于資訊交換,更适用于程序中斷控制,比如非法記憶體通路,殺死某個程序等;

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

  • 1、效率:傳輸效率主要影響因素是記憶體拷貝的次數,拷貝次數越少,傳輸速率越高。從Android程序架構角度分析:對于消息隊列、Socket和管道來說,資料先從發送方的緩存區拷貝到核心開辟的緩存區中,再從核心緩存區拷貝到接收方的緩存區,一共兩次拷貝,如圖:
2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

而對于Binder來說,資料從發送方的緩存區拷貝到核心的緩存區,而接收方的緩存區與核心的緩存區是映射到同一塊實體位址的,節省了一次資料拷貝的過程,如圖:

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

共享記憶體不需要拷貝,Binder的性能僅次于共享記憶體。

  • 2、穩定性:上面說到共享記憶體的性能優于Binder,那為什麼不采用共享記憶體呢,因為共享記憶體需要處理并發同步問題,容易出現死鎖和資源競争,穩定性較差。Socket雖然是基于C/S架構的,但是它主要是用于網絡間的通信且傳輸效率較低。Binder基于C/S架構 ,Server端與Client端相對獨立,穩定性較好。
  • 3、安全性:傳統Linux IPC的接收方無法獲得對方程序可靠的UID/PID,進而無法鑒别對方身份;而Binder機制為每個程序配置設定了UID/PID,且在Binder通信時會根據UID/PID進行有效性檢測。

Binder機制的作用和原理?

Linux系統将一個程序分為使用者空間和核心空間。對于程序之間來說,使用者空間的資料不可共享,核心空間的資料可共享,為了保證安全性和獨立性,一個程序不能直接操作或者通路另一個程序,即Android的程序是互相獨立、隔離的,這就需要跨程序之間的資料通信方式。普通的跨程序通信方式一般需要2次記憶體拷貝,如下圖所示:

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

一次完整的 Binder IPC 通信過程通常是這樣:

  • 首先 Binder 驅動在核心空間建立一個資料接收緩存區。
  • 接着在核心空間開辟一塊核心緩存區,建立核心緩存區和核心中資料接收緩存區之間的映射關系,以及核心中資料接收緩存區和接收程序使用者空間位址的映射關系。
  • 發送方程序通過系統調用 copyfromuser() 将資料 copy 到核心中的核心緩存區,由于核心緩存區和接收程序的使用者空間存在記憶體映射,是以也就相當于把資料發送到了接收程序的使用者空間,這樣便完成了一次程序間的通信。
2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

Binder架構中ServiceManager的作用?

Binder架構 是基于 C/S 架構的。由一系列的元件組成,包括 Client、Server、ServiceManager、Binder驅動,其中 Client、Server、Service Manager 運作在使用者空間,Binder 驅動運作在核心空間。如下圖所示:

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)
  • Server&Client:伺服器&用戶端。在Binder驅動和Service Manager提供的基礎設施上,進行Client-Server之間的通信。
  • ServiceManager(如同DNS域名伺服器)服務的管理者,将Binder名字轉換為Client中對該Binder的引用,使得Client可以通過Binder名字獲得Server中Binder實體的引用。
  • Binder驅動(如同路由器):負責程序之間binder通信的建立,計數管理以及資料的傳遞互動等底層支援。

最後,結合Android跨程序通信:圖文詳解 Binder機制 的總結圖來綜合了解一下:

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

Binder 的完整定義

  • 從程序間通信的角度看,Binder 是一種程序間通信的機制;
  • 從 Server 程序的角度看,Binder 指的是 Server 中的 Binder 實體對象;
  • 從 Client 程序的角度看,Binder 指的是 Binder 代理對象,是 Binder 實體對象的一個遠端代理;
  • 從傳輸過程的角度看,Binder 是一個可以跨程序傳輸的對象;Binder 驅動會對這個跨越程序邊界的對象對一點點特殊處理,自動完成代理對象和本地對象之間的轉換。

手寫實作簡化版AMS(AIDL實作)

與Binder相關的幾個類的職責:

  • IBinder:跨程序通信的Base接口,它聲明了跨程序通信需要實作的一系列抽象方法,實作了這個接口就說明可以進行跨程序通信,Client和Server都要實作此接口。
  • IInterface:這也是一個Base接口,用來表示Server提供了哪些能力,是Client和Server通信的協定。
  • Binder:提供Binder服務的本地對象的基類,它實作了IBinder接口,所有本地對象都要繼承這個類。
  • BinderProxy:在Binder.java這個檔案中還定義了一個BinderProxy類,這個類表示Binder代理對象它同樣實作了IBinder接口,不過它的很多實作都交由native層處理。Client中拿到的實際上是這個代理對象。
  • Stub:這個類在編譯aidl檔案後自動生成,它繼承自Binder,表示它是一個Binder本地對象;它是一個抽象類,實作了IInterface接口,表明它的子類需要實作Server将要提供的具體能力(即aidl檔案中聲明的方法)。
  • Proxy:它實作了IInterface接口,說明它是Binder通信過程的一部分;它實作了aidl中聲明的方法,但最終還是交由其中的mRemote成員來處理,說明它是一個代理對象,mRemote成員實際上就是BinderProxy。

aidl檔案隻是用來定義C/S互動的接口,Android在編譯時會自動生成相應的Java類,生成的類中包含了Stub和Proxy靜态内部類,用來封裝資料轉換的過程,實際使用時隻關心具體的Java接口類即可。為什麼Stub和Proxy是靜态内部類呢?這其實隻是為了将三個類放在一個檔案中,提高代碼的聚合性。通過上面的分析,我們其實完全可以不通過aidl,手動編碼來實作Binder的通信,下面我們通過編碼來實作ActivityManagerService:

1、首先定義IActivityManager接口:

public interface IActivityManager extends IInterface {
    //binder描述符
    String DESCRIPTOR = "android.app.IActivityManager";
    //方法編号
    int TRANSACTION_startActivity = IBinder.FIRST_CALL_TRANSACTION + 0;
    //聲明一個啟動activity的方法,為了簡化,這裡隻傳入intent參數
    int startActivity(Intent intent) throws RemoteException;
}
複制代碼
           

2、然後,實作ActivityManagerService側的本地Binder對象基類:

// 名稱随意,不一定叫Stub
public abstract class ActivityManagerNative extends Binder implements IActivityManager {

    public static IActivityManager asInterface(IBinder obj) {
        if (obj == null) {
            return null;
        }
        IActivityManager in = (IActivityManager) obj.queryLocalInterface(IActivityManager.DESCRIPTOR);
        if (in != null) {
            return in;
        }
        //代理對象,見下面的代碼
        return new ActivityManagerProxy(obj);
    }

    @Override
    public IBinder asBinder() {
        return this;
    }

    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        switch (code) {
            // 擷取binder描述符
            case INTERFACE_TRANSACTION:
                reply.writeString(IActivityManager.DESCRIPTOR);
                return true;
            // 啟動activity,從data中反序列化出intent參數後,直接調用子類startActivity方法啟動activity。
            case IActivityManager.TRANSACTION_startActivity:
                data.enforceInterface(IActivityManager.DESCRIPTOR);
                Intent intent = Intent.CREATOR.createFromParcel(data);
                int result = this.startActivity(intent);
                reply.writeNoException();
                reply.writeInt(result);
                return true;
        }
        return super.onTransact(code, data, reply, flags);
    }
}
複制代碼
           

3、接着,實作Client側的代理對象:

public class ActivityManagerProxy implements IActivityManager {
    private IBinder mRemote;

    public ActivityManagerProxy(IBinder remote) {
        mRemote = remote;
    }

    @Override
    public IBinder asBinder() {
        return mRemote;
    }

    @Override
    public int startActivity(Intent intent) throws RemoteException {
        Parcel data = Parcel.obtain();
        Parcel reply = Parcel.obtain();
        int result;
        try {
            // 将intent參數序列化,寫入data中
            intent.writeToParcel(data, 0);
            // 調用BinderProxy對象的transact方法,交由Binder驅動處理。
            mRemote.transact(IActivityManager.TRANSACTION_startActivity, data, reply, 0);
            reply.readException();
            // 等待server執行結束後,讀取執行結果
            result = reply.readInt();
        } finally {
            data.recycle();
            reply.recycle();
        }
        return result;
    }
}
複制代碼
           

4、最後,實作Binder本地對象(IActivityManager接口):

public class ActivityManagerService extends ActivityManagerNative {
    @Override
    public int startActivity(Intent intent) throws RemoteException {
        // 啟動activity
        return 0;
    }
}
複制代碼
           

簡化版的ActivityManagerService到這裡就已經實作了,剩下就是Client隻需要擷取到AMS的代理對象IActivityManager就可以通信了。

簡單講講 binder 驅動吧?

從 Java 層來看就像通路本地接口一樣,用戶端基于 BinderProxy 服務端基于 IBinder 對象,從 native 層來看來看用戶端基于 BpBinder 到 ICPThreadState 到 binder 驅動,服務端由 binder 驅動喚醒 IPCThreadSate 到 BbBinder 。跨程序通信的原理最終是要基于核心的,是以最會會涉及到 binder_open 、binder_mmap 和 binder_ioctl這三種系統調用。

跨程序傳遞大記憶體資料如何做?

binder 肯定是不行的,因為映射的最大記憶體隻有 1M-8K,可以采用 binder + 匿名共享記憶體的形式,像跨程序傳遞大的 bitmap 需要打開系統底層的 ashmem 機制。

請按順序仔細閱讀下列文章提升對Binder機制的了解程度:

寫給 Android 應用工程師的 Binder 原理剖析

Binder學習指南

Binder設計與實作

老羅Binder機制分析系列或Android系統源代碼情景分析Binder章節

5、Android系統啟動流程是什麼?(提示:init程序 -> Zygote程序 –> SystemServer程序 –> 各種系統服務 –> 應用程序)

Android系統啟動的核心流程如下:

  • 1、啟動電源以及系統啟動:當電源按下時引導晶片從預定義的地方(固化在ROM)開始執行,加載引導程式BootLoader到RAM,然後執行。
  • 2、引導程式BootLoader:BootLoader是在Android系統開始運作前的一個小程式,主要用于把系統OS拉起來并運作。
  • 3、Linux核心啟動:當核心啟動時,設定緩存、被保護存儲器、計劃清單、加載驅動。當其完成系統設定時,會先在系統檔案中尋找init.rc檔案,并啟動init程序。
  • 4、init程序啟動:初始化和啟動屬性服務,并且啟動Zygote程序。
  • 5、Zygote程序啟動:建立JVM并為其注冊JNI方法,建立伺服器端Socket,啟動SystemServer程序。
  • 6、SystemServer程序啟動:啟動Binder線程池和SystemServiceManager,并且啟動各種系統服務。
  • 7、Launcher啟動:被SystemServer程序啟動的AMS會啟動Launcher,Launcher啟動後會将已安裝應用的快捷圖示顯示到系統桌面上。

需要更詳細的分析請檢視以下系列文章:

Android系統啟動流程之init程序啟動

Android系統啟動流程之Zygote程序啟動

Android系統啟動流程之SystemServer程序啟動

Android系統啟動流程之Launcher程序啟動

系統是怎麼幫我們啟動找到桌面應用的?

通過意圖,PMS 會解析所有 apk 的 AndroidManifest.xml ,如果解析過會存到 package.xml 中不會反複解析,PMS 有了它就能找到了。

6、啟動一個程式,可以主界面點選圖示進入,也可以從一個程式中跳轉過去,二者有什麼差別?

是因為啟動程式(主界面也是一個app),發現了在這個程式中存在一個設定為的activity, 是以這個launcher會把icon提出來,放在主界面上。當使用者點選icon的時候,發出一個Intent:

Intent intent = mActivity.getPackageManager().getLaunchIntentForPackage(packageName);
mActivity.startActivity(intent);   
複制代碼
           

跳過去可以跳到任意允許的頁面,如一個程式可以下載下傳,那麼真正下載下傳的頁面可能不是首頁(也有可能是首頁),這時還是構造一個Intent,startActivity。這個intent中的action可能有多種view,download都有可能。系統會根據第三方程式向系統注冊的功能,為你的Intent選擇可以打開的程式或者頁面。是以唯一的一點 不同的是從icon的點選啟動的intent的action是相對單一的,從程式中跳轉或者啟動可能樣式更多一些。本質是相同的。

7、AMS家族重要術語解釋。

1.ActivityManagerServices,簡稱AMS,服務端對象,負責系統中所有Activity的生命周期。

2.ActivityThread,App的真正入口。當開啟App之後,調用main()開始運作,開啟消息循環隊列,這就是傳說的UI線程或者叫主線程。與ActivityManagerService一起完成Activity的管理工作。

3.ApplicationThread,用來實作ActivityManagerServie與ActivityThread之間的互動。在ActivityManagerSevice需要管理相關Application中的Activity的生命周期時,通過ApplicationThread的代理對象與ActivityThread通信。

4.ApplicationThreadProxy,是ApplicationThread在伺服器端的代理,負責和用戶端的ApplicationThread通信。AMS就是通過該代理與ActivityThread進行通信的。

5.Instrumentation,每一個應用程式隻有一個Instrumetation對象,每個Activity内都有一個對該對象的引用,Instrumentation可以了解為應用程序的管家,ActivityThread要建立或暫停某個Activity時,都需要通過Instrumentation來進行具體的操作。

6.ActivityStack,Activity在AMS的棧管理,用來記錄經啟動的Activity的先後關系,狀态資訊等。通過ActivtyStack決定是否需要啟動新的程序。

7.ActivityRecord,ActivityStack的管理對象,每個Acivity在AMS對應一個ActivityRecord,來記錄Activity狀态以及其他的管理資訊。其實就是伺服器端的Activit對象的映像。

8.TaskRecord,AMS抽象出來的一個“任務”的概念,是記錄ActivityRecord的棧,一個“Task”包含若幹個ActivityRecord。AMS用TaskRecord確定Activity啟動和退出的順序。如果你清楚Activity的4種launchMode,那麼對這概念應該不陌生。

8、App啟動流程(Activity的冷啟動流程)。

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

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

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

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

  • Instrumentation: 監控應用與系統相關的互動行為。
  • AMS:元件管理排程中心,什麼都不幹,但是什麼都管。
  • ActivityStarter:Activity啟動的控制器,處理Intent與Flag對Activity啟動的影響,具體說來有:1 尋找符合啟動條件的Activity,如果有多個,讓使用者選擇;2 校驗啟動參數的合法性;3 傳回int參數,代表Activity是否啟動成功。
  • ActivityStackSupervisior:這個類的作用你從它的名字就可以看出來,它用來管理任務棧。
  • ActivityStack:用來管理任務棧裡的Activity。
  • 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的啟動。

最後,再看看另一幅啟動流程圖來加深了解:

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

9、ActivityThread工作原理。

10、說下四大元件的啟動過程,四大元件的啟動與銷毀的方式。

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

  • 繼承BroadcastReceiver,重寫onReceive()方法。
  • 通過Binder機制向ActivityManagerService注冊廣播。
  • 通過Binder機制向ActivityMangerService發送廣播。
  • ActivityManagerService查找符合相應條件的廣播(IntentFilter/Permission)的BroadcastReceiver,将廣播發送到BroadcastReceiver所在的消息隊列中。
  • BroadcastReceiver所在消息隊列拿到此廣播後,回調它的onReceive()方法。

11、AMS是如何管理Activity的?

12、了解Window和WindowManager。

1.Window用于顯示View和接收各種事件,Window有三種型:應用Window(每個Activity對應一個Window)、子Widow(不能單獨存在,附屬于特定Window)、系統window(toast和狀态欄)

2.Window分層級,應用Window在1-99、子Window在1000-1999、系統Window在2000-2999.WindowManager提供了增改View的三個功能。

3.Window是個抽象概念:每一個Window對應着一個ViewRootImpl,Window通過ViewRootImpl來和View建立聯系,View是Window存在的實體,隻能通過WindowManager來通路Window。

4.WindowManager的實作是WindowManagerImpl,其再委托WindowManagerGlobal來對Window進行操作,其中有四種List分别儲存對應的View、ViewRootImpl、WindowManger.LayoutParams和正在被删除的View。

5.Window的實體是存在于遠端的WindowMangerService,是以增删改Window在本端是修改上面的幾個List然後通過ViewRootImpl重繪View,通過WindowSession(每Window個對應一個)在遠端修改Window。

6.Activity建立Window:Activity會在attach()中建立Window并設定其回調(onAttachedToWindow()、dispatchTouchEvent()),Activity的Window是由Policy類建立PhoneWindow實作的。然後通過Activity#setContentView()調用PhoneWindow的setContentView。

13、WMS是如何管理Window的?

14、大體說清一個應用程式安裝到手機上時發生了什麼?

APK的安裝流程如下所示:

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

複制APK到/data/app目錄下,解壓并掃描安裝包。

資料總管解析APK裡的資源檔案。

解析AndroidManifest檔案,并在/data/data/目錄下建立對應的應用資料目錄。

然後對dex檔案進行優化,并儲存在dalvik-cache目錄下。

将AndroidManifest檔案解析出的四大元件資訊注冊到PackageManagerService中。

安裝完成後,發送廣播。

15、Android的打包流程?(即描述清點選 Android Studio 的 build 按鈕後發生了什麼?)apk裡有哪些東西?簽名算法的原理?

apk打包流程

Android的封包件APK分為兩個部分:代碼和資源,是以打包方面也分為資源打包和代碼打包兩個方面,下面就來分析資源和代碼的編譯打包原理。

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

2020年中進階Android面試秘籍(Android進階篇-1)前言Android進階面試題 (⭐⭐⭐)

具體說來:

  • 通過AAPT工具進行資源檔案(包括AndroidManifest.xml、布局檔案、各種xml資源等)的打包,生成R.java檔案。
  • 通過AIDL工具處理AIDL檔案,生成相應的Java檔案。
  • 通過Java Compiler編譯R.java、Java接口檔案、Java源檔案,生成.class檔案。
  • 通過dex指令,将.class檔案和第三方庫中的.class檔案處理生成classes.dex,該過程主要完成Java位元組碼轉換成Dalvik位元組碼,壓縮常量池以及清除備援資訊等工作。
  • 通過ApkBuilder工具将資源檔案、DEX檔案打包生成APK檔案。
  • 通過Jarsigner工具,利用KeyStore對生成的APK檔案進行簽名。
  • 如果是正式版的APK,還會利用ZipAlign工具進行對齊處理,對齊的過程就是将APK檔案中所有的資源檔案距離檔案的起始距位置都偏移4位元組的整數倍,這樣通過記憶體映射通路APK檔案的速度會更快,并且會減少其在裝置上運作時的記憶體占用。

apk組成

  • dex:最終生成的Dalvik位元組碼。
  • res:存放資源檔案的目錄。
  • asserts:額外建立的資源檔案夾。
  • lib:如果存在的話,存放的是ndk編出來的so庫。
  • META-INF:存放簽名資訊

MANIFEST.MF(清單檔案):其中每一個資源檔案都有一個SHA-256-Digest簽名,MANIFEST.MF檔案的SHA256(SHA1)并base64編碼的結果即為CERT.SF中的SHA256-Digest-Manifest值。

CERT.SF(待簽名檔案):除了開頭處定義的SHA256(SHA1)-Digest-Manifest值,後面幾項的值是對MANIFEST.MF檔案中的每項再次SHA256并base64編碼後的值。

CERT.RSA(簽名結果檔案):其中包含了公鑰、加密算法等資訊。首先對前一步生成的MANIFEST.MF使用了SHA256(SHA1)-RSA算法,用開發者私鑰簽名,然後在安裝時使用公鑰解密。最後,将其與未加密的摘要資訊(MANIFEST.MF檔案)進行對比,如果相符,則表明内容沒有被修改。

  • androidManifest:程式的全局清單配置檔案。
  • resources.arsc:編譯後的二進制資源檔案。

簽名算法的原理

為什麼要簽名?

  • 確定Apk來源的真實性。
  • 確定Apk沒有被第三方篡改。

什麼是簽名?

在Apk中寫入一個“指紋”。指紋寫入以後,Apk中有任何修改,都會導緻這個指紋無效,Android系統在安裝Apk進行簽名校驗時就會不通過,進而保證了安全性。

數字摘要

對一個任意長度的資料,通過一個Hash算法計算後,都可以得到一個固定長度的二進制資料,這個資料就稱為“摘要”。

補充:

  • 雜湊演算法的基礎原理:将資料(如一段文字)運算變為另一固定長度值。
  • SHA-1:在密碼學中,SHA-1(安全雜湊演算法1)是一種加密散列函數,它接受輸入并産生一個160 位(20 位元組)散列值,稱為消息摘要 。
  • MD5:MD5消息摘要算法(英語:MD5 Message-Digest Algorithm),一種被廣泛使用的密碼散列函數,可以産生出一個128位(16位元組)的散列值(hash value),用于確定資訊傳輸完整一緻。
  • SHA-2:名稱來自于安全雜湊演算法2(英語:Secure Hash Algorithm 2)的縮寫,一種密碼散列函數算法标準,其下又可再分為六個不同的算法标準,包括了:SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256。

特征:

  • 唯一性
  • 固定長度:比較常用的Hash算法有MD5和SHA1,MD5的長度是128拉,SHA1的長度是160位。
  • 不可逆性

簽名和校驗的主要過程

簽名就是在摘要的基礎上再進行一次加密,對摘要加密後的資料就可以當作數字簽名。

簽名過程:

  • 1、計算摘要:通過Hash算法提取出原始資料的摘要。
  • 2、計算簽名:再通過基于密鑰(私鑰)的非對稱加密算法對提取出的摘要進行加密,加密後的資料就是簽名資訊。
  • 3、寫入簽名:将簽名資訊寫入原始資料的簽名區塊内。

校驗過程:

  • 1、首先用同樣的Hash算法從接收到的資料中提取出摘要。
  • 2、解密簽名:使用發送方的公鑰對數字簽名進行解密,解密出原始摘要。
  • 3、比較摘要:如果解密後的資料和提取的摘要一緻,則校驗通過;如果資料被第三方篡改過,解密後的資料和摘要将會不一緻,則校驗不通過。

數字證書

如何保證公鑰的可靠性呢?答案是數字證書,數字證書是身份認證機構(Certificate Authority)頒發的,包含了以下資訊:

  • 證書頒發機構
  • 證書頒發機構簽名
  • 證書綁定的伺服器域名
  • 證書版本、有效期
  • 簽名使用的加密算法(非對稱算法,如RSA)
  • 公鑰等

接收方收到消息後,先向CA驗證證書的合法性,再進行簽名校驗。

注意:Apk的證書通常是自簽名的,也就是由開發者自己制作,沒有向CA機構申請。Android在安裝Apk時并沒有校驗證書本身的合法性,隻是從證書中提取公鑰和加密算法,這也正是對第三方Apk重新簽名後,還能夠繼續在沒有安裝這個Apk的系統中繼續安裝的原因。

keystore和證書格式

keystore檔案中包含了私鑰、公鑰和數字證書。根據編碼不同,keystore檔案分為很多種,Android使用的是Java标準keystore格式JKS(Java Key Storage),是以通過Android Studio導出的keystore檔案是以.jks結尾的。

keystore使用的證書标準是X.509,X.509标準也有多種編碼格式,常用的有兩種:pem(Privacy Enhanced Mail)和der(Distinguished Encoding Rules)。jks使用的是der格式,Android也支援直接使用pem格式的證書進行簽名。

兩種證書編碼格式的差別:

  • DER(Distinguished Encoding Rules)

二進制格式,所有類型的證書和私鑰都可以存儲為der格式。

  • PEM(Privacy Enhanced Mail)

base64編碼,内容以-----BEGIN xxx----- 開頭,以-----END xxx----- 結尾。

jarsigner和apksigner的差別

Android提供了兩種對Apk的簽名方式,一種是基于JAR的簽名方式,另一種是基于Apk的簽名方式,它們的主要差別在于使用的簽名檔案不一樣:jarsigner使用keystore檔案進行簽名;apksigner除了支援使用keystore檔案進行簽名外,還支援直接指定pem證書檔案和私鑰進行簽名。

在簽名時,除了要指定keystore檔案和密碼外,也要指定alias和key的密碼,這是為什麼呢?

keystore是一個密鑰庫,也就是說它可以存儲多對密鑰和證書,keystore的密碼是用于保護keystore本身的,一對密鑰和證書是通過alias來區分的。是以jarsigner是支援使用多個證書對Apk進行簽名的,apksigner也同樣支援。

Android Apk V1 簽名原理

  • 1、解析出 CERT.RSA 檔案中的證書、公鑰,解密 CERT.RSA 中的加密資料。
  • 2、解密結果和 CERT.SF 的指紋進行對比,保證 CERT.SF 沒有被篡改。
  • 3、而 CERT.SF 中的内容再和 MANIFEST.MF 指紋對比,保證 MANIFEST.MF 檔案沒有被篡改。
  • 4、MANIFEST.MF 中的内容和 APK 所有檔案指紋逐一對比,保證 APK 沒有被篡改。

16、說下安卓虛拟機和java虛拟機的原理和不同點?(JVM、Davilk、ART三者的原理和差別)

JVM 和Dalvik虛拟機的差別

JVM:.java -> javac -> .class -> jar -> .jar

架構: 堆和棧的架構.

DVM:.java -> javac -> .class -> dx.bat -> .dex

架構: 寄存器(cpu上的一塊高速緩存)

Android2個虛拟機的差別(一個5.0之前,一個5.0之後)

什麼是Dalvik:Dalvik是Google公司自己設計用于Android平台的Java虛拟機。Dalvik虛拟機是Google等廠商合作開發的Android移動裝置平台的核心組成部分之一,它可以支援已轉換為.dex(即Dalvik Executable)格式的Java應用程式的運作,.dex格式是專為Dalvik應用設計的一種壓縮格式,适合記憶體和處理器速度有限的系統。Dalvik經過優化,允許在有限的記憶體中同時運作多個虛拟機的執行個體,并且每一個Dalvik應用作為獨立的Linux程序執行。獨立的程序可以防止在虛拟機崩潰的時候所有程式都被關閉。

什麼是ART:Android作業系統已經成熟,Google的Android團隊開始将注意力轉向一些底層元件,其中之一是負責應用程式運作的Dalvik運作時。Google開發者已經花了兩年時間開發更快執行效率更高更省電的替代ART運作時。ART代表Android Runtime,其處理應用程式執行的方式完全不同于Dalvik,Dalvik是依靠一個Just-In-Time(JIT)編譯器去解釋位元組碼。開發者編譯後的應用代碼需要通過一個解釋器在使用者的裝置上運作,這一機制并不高效,但讓應用能更容易在不同硬體和架構上運作。ART則完全改變了這套做法,在應用安裝的時候就預編譯位元組碼為機器語言,這一機制叫Ahead-Of-Time(AOT)編譯。在移除解釋代碼這一過程後,應用程式執行将更有效率,啟動更快。

ART優點:

  • 系統性能的顯著提升。
  • 應用啟動更快、運作更快、體驗更流暢、觸感回報更及時。
  • 更長的電池續航能力。
  • 支援更低的硬體。

ART缺點:

  • 更大的存儲空間占用,可能會增加10%-20%。
  • 更長的應用安裝時間。

ART和Davlik中垃圾回收的差別?

17、安卓采用自動垃圾回收機制,請說下安卓記憶體管理的原理?

開放性問題:如何設計垃圾回收算法?

18、Android中App是如何沙箱化的,為何要這麼做?

19、一個圖檔在app中調用R.id後是如何找到的?

20、JNI

Java調用C++

  • 在Java中聲明Native方法(即需要調用的本地方法)
  • 編譯上述 Java源檔案javac(得到 .class檔案) 3。 通過 javah 指令導出JNI的頭檔案(.h檔案)
  • 使用 Java需要互動的本地代碼 實作在 Java中聲明的Native方法
  • 編譯.so庫檔案
  • 通過Java指令執行 Java程式,最終實作Java調用本地代碼

C++調用Java

  • 從classpath路徑下搜尋ClassMethod這個類,并傳回該類的Class對象。
  • 擷取類的預設構造方法ID。
  • 查找執行個體方法的ID。
  • 建立該類的執行個體。
  • 調用對象的執行個體方法。
  • 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);  
    }  
    複制代碼
               

如何在jni中注冊native函數,有幾種注冊方式?

so 的加載流程是怎樣的,生命周期是怎樣的?

這個要從 java 層去看源碼分析,是從 ClassLoader 的 PathList 中去找到目标路徑加載的,同時 so 是通過 mmap 加載映射到虛拟空間的。生命周期加載庫和解除安裝庫時分别調用 JNI_OnLoad 和 JNI_OnUnload() 方法。

21、請介紹一下NDK?