【建議收藏】2020年中高級Android大廠面試祕籍,爲你保駕護航金三銀四,直通大廠(Android高級篇上)...

前言

成爲一名優秀的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熱修復成功之後才允許用戶進入。

需要更全面更深入的理解請查看深入探索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的方法和注意的問題

需要更全面更深入的理解請查看深入探索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的時候,試着把它們緩存在數組或容器中實現複用。
  • 對於能夠複用的對象,同理可以使用對象池將它們緩存起來。

需要更全面更深入的理解請查看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、開放問題:優化一個列表頁面的打開速度和流暢性。

需要更全面更深入的理解請查看Android性能優化之繪製優化深入探索Android佈局優化(上)深入探索Android佈局優化(下)深入探索Android卡頓優化(上)深入探索Android卡頓優化(下)

5、App瘦身

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 服務器,有效的防止了域名劫持,提高域名解析的效率。

參考文章

2、客戶端網絡安全實現

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

7、App電量優化

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系統架構

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應用開發來說,最好能手繪下面的系統架構圖:

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事件除外。

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

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的創建規則如下:

對於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方式、各種方式優缺點?

講講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和管道來說,數據先從發送方的緩存區拷貝到內核開闢的緩存區中,再從內核緩存區拷貝到接收方的緩存區,一共兩次拷貝,如圖:

而對於Binder來說,數據從發送方的緩存區拷貝到內核的緩存區,而接收方的緩存區與內核的緩存區是映射到同一塊物理地址的,節省了一次數據拷貝的過程,如圖:

共享內存不需要拷貝,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次內存拷貝,如下圖所示:

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

  • 首先 Binder 驅動在內核空間創建一個數據接收緩存區。
  • 接着在內核空間開闢一塊內核緩存區,建立內核緩存區和內核中數據接收緩存區之間的映射關係,以及內核中數據接收緩存區和接收進程用戶空間地址的映射關係。
  • 發送方進程通過系統調用 copyfromuser() 將數據 copy 到內核中的內核緩存區,由於內核緩存區和接收進程的用戶空間存在內存映射,因此也就相當於把數據發送到了接收進程的用戶空間,這樣便完成了一次進程間的通信。

Binder框架中ServiceManager的作用?

Binder框架 是基於 C/S 架構的。由一系列的組件組成,包括 Client、Server、ServiceManager、Binder驅動,其中 Client、Server、Service Manager 運行在用戶空間,Binder 驅動運行在內核空間。如下圖所示:

  • Server&Client:服務器&客戶端。在Binder驅動和Service Manager提供的基礎設施上,進行Client-Server之間的通信。
  • ServiceManager(如同DNS域名服務器)服務的管理者,將Binder名字轉換爲Client中對該Binder的引用,使得Client可以通過Binder名字獲得Server中Binder實體的引用。
  • Binder驅動(如同路由器):負責進程之間binder通信的建立,計數管理以及數據的傳遞交互等底層支持。

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

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的啓動流程圖(放大可查看)如下所示:

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

  • 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的啓動。

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

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的安裝流程如下所示:

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

資源管理器解析APK裏的資源文件。

解析AndroidManifest文件,並在/data/data/目錄下創建對應的應用數據目錄。

然後對dex文件進行優化,並保存在dalvik-cache目錄下。

將AndroidManifest文件解析出的四大組件信息註冊到PackageManagerService中。

安裝完成後,發送廣播。

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

apk打包流程

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

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

具體說來:

  • 通過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?

公衆號

我的公衆號 JsonChao 開通啦,如果您想第一時間獲取最新文章和最新動態,歡迎掃描關注~

讚賞

如果這個庫對您有很大幫助,您願意支持這個項目的進一步開發和這個項目的持續維護。你可以掃描下面的二維碼,讓我喝一杯咖啡或啤酒。非常感謝您的捐贈。謝謝!


Contanct Me

● 微信 && 微信羣:

歡迎關注我的微信:bcce5360。由於微信羣人數太多無法生成羣邀二維碼,所以麻煩大家想進微信羣的朋友們,加我微信拉你進羣(PS:微信羣的學習氛圍與各項福利將會超乎你的想象)

● QQ羣:

2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎大家加入~

About me

很感謝您閱讀這篇文章,希望您能將它分享給您的朋友或技術羣,這對我意義重大。

希望我們能成爲朋友,在 Github掘金上一起分享知識。

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