今日頭條Android面試

非同步社群發表於2018-05-16

​點選關注非同步圖書,置頂公眾號

每天與你分享 IT好書 技術乾貨 職場知識


首先說一下,今日頭條的面試主要分為三輪到四輪,如果是旺季面三輪,首先是基礎面試,基本面試一般10個題左右,最近面試了一下今日頭條的移動Android資深工程師,記錄下。

第一面是北京的開發進行視訊面試,有理論和程式設計題組成。用的是線上程式設計工具,如下圖。

今日頭條Android面試

​第一面

1,請程式設計實現單例模式,懶漢和飽漢寫法。

今日頭條Android面試

​2,請程式設計實現Java的生產者-消費者模型

今日頭條Android面試

​看到這個有點懵逼,要是大學畢業的時候寫這個肯定沒問題,這都工作多年,這也只能按照自己的思路寫了。這裡使用synchronized鎖以及wait notify實現一個比較簡單的。關於更多的知識可以zhuanlan.zhihu.com/p/20300609

3,HashMap的內部結構? 內部原理?
關於HashMap的問題,不再詳述,這方面的資料也挺多,不多需要注意的是Java1.7和1.8版本HashMap內部結構的區別。


4,請簡述Android事件傳遞機制, ACTION_CANCEL事件何時觸發?
關於第一個問題,不做任何解釋。
關於ACTION_CANCEL何時被觸發,系統文件有這麼一種使用場景:在設計設定頁面的滑動開關時,如果不監聽ACTION_CANCEL,在滑動到中間時,如果你手指上下移動,就是移動到開關控制元件之外,則此時會觸發ACTION_CANCEL,而不是ACTION_UP,造成開關的按鈕停頓在中間位置。
意思是當滑動的時候就會觸發,不知道大家搞沒搞過微信的長按錄音,有一種狀態是“鬆開手指,取消傳送”,這時候就會觸發ACTION_CANCEL。

5,Android的程式間通訊,Liunx作業系統的程式間通訊。
關於這個問題也是被問的很多,此處也不做解釋。

6,JVM虛擬機器記憶體結構,以及它們的作用。
這個問題也比較基礎,JVM的記憶體結構如下圖所示。

今日頭條Android面試

​可以通過下面的問題來學習:

www.cnblogs.com/jiyukai/p/6…
www.zhihu.com/question/65…

7,簡述Android的View繪製流程,Android的wrap_content是如何計算的。

8,有一個整形陣列,包含正數和負數,然後要求把陣列內的所有負數移至正數的左邊,且保證相對位置不變,要求時間複雜度為O(n), 空間複雜度為O(1)。例如,{10, -2, 5, 8, -4, 2, -3, 7, 12, -88, -23, 35}變化後是{-2, -4,-3, -88, -23,5, 8 ,10, 2, 7, 12, 35}。

要實現上面的效果有兩種方式:
第一種:兩個變數,一個用來記錄當前的遍歷點,一個用來記錄最左邊的負數在陣列中的索引值。然後遍歷整個陣列,遇到負數將其與負數後面的數進行交換,遍歷結束,即可實現負數在左,正數在右。

第二種:兩個變數記錄左右節點,兩邊分別開始遍歷。左邊的節點遇到負值繼續前進,遇到正值停止。右邊的節點正好相反。然後將左右節點的只進行交換,然後再開始遍歷直至左右節點相遇。這種方式的時間複雜度是O(n).空間複雜度為O(1)

今日頭條Android面試

​顯然,第二種實現的難點比較高,不過只要此種滿足條件。

第二面

1,bundle的資料結構,如何儲存,既然有了Intent.putExtra,為啥還要用bundle。

bundle的內部結構其實是Map,傳遞的資料可以是boolean、byte、int、long、float、double、string等基本型別或它們對應的陣列,也可以是物件或物件陣列。當Bundle傳遞的是物件或物件陣列時,必須實現Serializable 或Parcelable介面。

2,android的IPC通訊方式,是否使用過
這方面的資料比較多,也不方便闡述

3,Android的多點觸控如何傳遞
核心類

4,asynctask的原理
AsyncTask是對Thread和Handler的組合包裝。
blog.csdn.net/iispring/ar…
5,android 圖片載入框架有哪些,對比下區別 主要有4種:Android-Universal-Image-Loader、Picasso、Glide和Fresco

Android-Universal-Image-Loader

優點:支援下載進度監聽(ImageLoadingListener) * 可在View滾動中暫停圖片載入(PauseOnScrollListener) * 預設實現多種記憶體快取演算法(最大最先刪除,使用最少最先刪除,最近最少使用,先進先刪除,當然自己也可以配置快取演算法)
缺點:2015年之後便不再維護,該庫使用前需要進行配置。


Picasso

優點:包較小(100k) * 取消不在視野範圍內圖片資源的載入 * 使用最少的記憶體完成複雜的圖片轉換 * 自動新增二級快取 * 任務排程優先順序處理 * 併發執行緒數根據網路型別調整 * 圖片的本地快取交給同為Square出品的okhttp處理,控制圖片的過期時間。
缺點:
功能較為簡單,自身無法實現“本地快取”功能。


Glide

優點:多種圖片格式的快取,適用於更多的內容表現形式(如Gif、WebP、縮圖、Video) * 生命週期整合(根據Activity或者Fragment的生命週期管理圖片載入請求) * 高效處理Bitmap(bitmap的複用和主動回收,減少系統回收壓力) * 高效的快取策略,靈活(Picasso只會快取原始尺寸的圖片,Glide快取的是多種規格),載入速度快且記憶體開銷小(預設Bitmap格式的不同,使得記憶體開銷是Picasso的一半)。
缺點:方法較多較複雜,因為相當於在Picasso上的改進,包較大(500k),影響不是很大。


Fresco

缺點:最大的優勢在於5.0以下(最低2.3)的bitmap載入。在5.0以下系統,Fresco將圖片放到一個特別的記憶體區域(Ashmem區) * 大大減少OOM(在更底層的Native層對OOM進行處理,圖片將不再佔用App的記憶體) * 適用於需要高效能載入大量圖片的場景。
缺點:包較大(2~3M) * 用法複雜 * 底層涉及c++領域

5,主執行緒中的Looper.loop()一直無限迴圈為什麼不會造成ANR?
ActivityThread.java 是主執行緒入口的類,ActivityThread.java 的main函式的內容如下。

今日頭條Android面試

​然後再看Looper.loop()的原始碼,可以發現:

今日頭條Android面試

顯然,ActivityThread的main方法主要就是做訊息迴圈,一旦退出訊息迴圈,那麼你的應用也就退出了。那麼這個死迴圈不會造成ANR異常呢?

說明:因為Android 的是由事件驅動的,looper.loop() 不斷地接收事件、處理事件,每一個點選觸控或者說Activity的生命週期都是執行在 Looper.loop() 的控制之下,如果它停止了,應用也就停止了。只能是某一個訊息或者說對訊息的處理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。也就說我們的程式碼其實就是在這個迴圈裡面去執行的,當然不會阻塞了。來看一下handleMessage的原始碼:

今日頭條Android面試

​可以看見Activity的生命週期都是依靠主執行緒的Looper.loop,當收到不同Message時則採用相應措施。

如果某個訊息處理時間過長,比如你在onCreate(),onResume()裡面處理耗時操作,那麼下一次的訊息比如使用者的點選事件不能處理了,整個迴圈就會產生卡頓,時間一長就成了ANR。

總結:Looer.loop()方法可能會引起主執行緒的阻塞,但只要它的訊息迴圈沒有被阻塞,能一直處理事件就不會產生ANR異常。

6,圖片框架的一些原理知識

7,其他的一些Android的模組化開發,熱更新,元件化等知識。

Android面試之主流框架

在Android面試的時候,經常會被問到一些Android開發中用到的一些開發框架,如常見的網路請求框架Retrofit/OkHttp,元件通訊框架EventBus/Dagger2,非同步程式設計RxJava/RxAndroid等。本文給大家整理下上面的幾個框架,以備面試用。

EventBus

EventBus是一個Android釋出/訂閱事件匯流排,簡化了元件間的通訊,讓程式碼更加簡介,但是如果濫用EventBus,也會讓程式碼變得更加輔助。面試EventBus的時候一般會談到如下幾點:

(1)EventBus是通過註解+反射來進行方法的獲取的

註解的使用:@Retention(RetentionPolicy.RUNTIME)表示此註解在執行期可知,否則使用CLASS或者SOURCE在執行期間會被丟棄。
通過反射來獲取類和方法:因為對映關係實際上是類對映到所有此類的物件的方法上的,所以應該通過反射來獲取類以及被註解過的方法,並且將方法和物件儲存為一個呼叫實體。

(2)使用ConcurrentHashMap來儲存對映關係

呼叫實體的構建:呼叫實體中對於Object,也就是實際執行方法的物件不應該使用強引用而是應該使用弱引用,因為Map的static的,生命週期有可能長於被呼叫的物件,如果使用強引用就會出現記憶體洩漏的問題。

說明:併發程式設計實踐中,ConcurrentHashMap是一個經常被使用的資料結構,相比於Hashtable以及Collections.synchronizedMap(),ConcurrentHashMap線上程安全的基礎上提供了更好的寫併發能力,但同時降低了對讀一致性的要求。詳情可以檢視下面的文章:
www.importnew.com/22007.html

(3)方法的執行

使用Dispatcher進行方法的分派,非同步則使用執行緒池來處理,同步就直接執行,而UI執行緒則使用MainLooper建立一個Handler,投遞到主執行緒中去執行。

Retrofit

首先要明確EventBus中最核心的就是動態代理技術。

Java中的動態代理:

首先動態代理是區別於靜態代理的,代理模式中需要代理類和實際執行類同時實現一個相同的介面,並且在每個介面定義的方法前後都要加入相同的程式碼,這樣有可能很多方法代理類都需要重複。而動態代理就是將這個步驟放入執行時的過程,一個代理類只需要實現InvocationHandler介面中的invoke方法,當需要動態代理時只需要根據介面和一個實現了InvocationHandler的代理物件A生成一個最終的自動生成的代理物件A*。這樣最終的代理物件A*無論呼叫什麼方法,都會執行InvocationHandler的代理物件A的invoke函式,你就可以在這個invoke函式中實現真正的代理邏輯。

動態代理的實現機制實際上就是使用Proxy.newProxyInstance函式為動態代理物件A生成一個代理物件A*的類的位元組碼從而生成具體A*物件過程,這個A*類具有幾個特點,一是它需要實現傳入的介面,第二就是所有介面的實現中都會呼叫A的invoke方法,並且傳入相應的呼叫實際方法(即介面中的方法)。

Retrofit中的動態代理

Retrofit中使用了動態代理是不錯,但是並不是為了真正的代理才使用的,它只是為了動態代理一個非常重要的功能,就是“攔截”功能。我們知道動態代理中自動生成的A*物件的所有方法執行都會呼叫實際代理類A中的invoke方法,再由我們在invoke中實現真正代理的邏輯,實際上也就是A*的所有方法都被A物件給攔截了。
而Retrofit的功能就是將代理變成像方法呼叫那麼簡單。

今日頭條Android面試

​再用這個retrofit物件建立一個ServiceApi物件,並通過getAuthor函式來呼叫函式。

今日頭條Android面試

​也就是一個網路呼叫你只需要在你建立的介面裡面通過註解進行設定,然後通過retrofit建立一個api然後呼叫,就可以自動完成一個Okhttp的Call的建立。Retrofit的create()函式的程式碼如下:

今日頭條Android面試

​我們可以看出怎麼從介面類建立成一個API物件?就是使用了動態代理中的攔截技術,通過建立一個符合此介面的動態代理物件A*,那A呢?就是這其中建立的這個匿名類了,它在內部實現了invoke函式,這樣A*呼叫的就是A中的invoke函式,也就是被攔截了,實際執行invoke。而invoke就是根據呼叫的method的註解(,從而生成一個符合條件的Okhttp的Call物件,並進行真正的請求。

Retrofit作用

Retrofit實際上是為了更方便的使用Okhttp,因為Okhttp的使用就是構建一個Call,而構建Call的大部分過程都是相似的,而Retrofit正是利用了代理機制帶我們動態的建立Call,而Call的建立資訊就來自於你的註解。

OkHttp3

關於OkHttp3的內容大家可以訪問下面的部落格連結:OkHttp3原始碼分析。該文章主要從以下幾個方面來講解OkHttps相關的內容:
OkHttp3原始碼分析[綜述]
OkHttp3原始碼分析[複用連線池]
OkHttp3原始碼分析[快取策略]
OkHttp3原始碼分析[DiskLruCache]
OkHttp3原始碼分析[任務佇列]

請求任務佇列

Okhttp使用了一個執行緒池來進行非同步網路任務的真正執行,而對於任務的管理採用了任務佇列的模型來對任務執行進行相應的管理,有點類似伺服器的反向代理模型。Okhttp使用分發器Dispatcher來維護一個正在執行任務佇列和一個等待佇列。如果當前併發任務數量小於64,就放入執行佇列中並且放入執行緒池中執行。而如果當前併發數量大於64就放入等待佇列中,在每次有任務執行完成之後就在finally塊中呼叫分發器的finish函式,在等待佇列中檢視是否有空餘任務,如果有就進行入隊執行。Okhttp就是使用任務佇列的模型來進行任務的執行和排程的。

複用連線池

Http使用的TCP連線有長連線和短連線之分,對於訪問某個伺服器的頻繁通訊,使用短連線勢必會造成在建立連線上大量的時間消耗;而長連線的長時間無用保持又會造成資源你的浪費。Okhttp底層是採用Socket建立流連線,而連線如果不手動close掉,就會造成記憶體洩漏,那我們使用Okhttp時也沒有做close操作,其實是Okhttp自己來進行連線池的維護的。在Okhttp中,它使用類似引用計數的方式來進行連線的管理,這裡的計數物件是StreamAllocation,它被反覆執行aquire與release操作,這兩個函式其實是在改變Connection中的List<WeakReference<StreamAllocation>>大小。List中Allocation的數量也就是物理socket被引用的計數(Refference Count),如果計數為0的話,說明此連線沒有被使用,是空閒的,需要通過淘汰演算法實現回收。

在連線池內部維護了一個執行緒池,這個執行緒池執行的cleanupRunnable實際上是一個阻塞的runnable,內部有一個無限迴圈,在清理完成之後呼叫wait進行等待,等待的時間由cleanup的返回值決定,在等待時間到了之後再進行清理任務。相關程式碼如下:

今日頭條Android面試

​其中,Cleanup函式的執行過程如下:

遍歷Deque中所有的RealConnection,標記洩漏的連線;

如果被標記的連線滿足(空閒socket連線超過5個&&keepalive時間大於5分鐘),就將此連線從Deque中移除,並關閉連線,返回0,也就是將要執行wait(0),提醒立刻再次掃描;

如果(目前還可以塞得下5個連線,但是有可能洩漏的連線(即空閒時間即將達到5分鐘)),就返回此連線即將到期的剩餘時間,供下次清理;

如果(全部都是活躍的連線),就返回預設的keep-alive時間,也就是5分鐘後再執行清理;

如果(沒有任何連線),就返回-1,跳出清理的死迴圈。

說明:“併發”==(“空閒”+“活躍”)==5,而不是說併發連線就一定是活躍的連線。

如何標記空閒的連線呢?我們前面也說了,如果一個連線身上的引用為0,那麼就說明它是空閒的,那麼就要使用pruneAndGetAllocationCount來計算它身上的引用數,如同引用計數過程。
其實標記引用為0的演算法很簡單,就是遍歷它的List<Reference<StreamAllocation>>,刪除所有已經為null的弱引用,剩下的數量就是現在它的引用數量,pruneAndGetAllocationCount函式的原始碼如下:

今日頭條Android面試

​RxJava

從15年開始,前端掀起了一股非同步程式設計的熱潮,在移動Android程式設計過程中,經常會聽到觀察者與被觀察者等概念。

觀察者與被觀察者通訊

Observable的通過create函式建立一個觀察者物件。

今日頭條Android面試

​Observable的建構函式如下:

今日頭條Android面試

​建立了一個Observable我們記為Observable1,儲存了傳入的OnSubscribe物件為onSubscribe,這個很重要,後面會說到。

onSubscribe方法

今日頭條Android面試

Rxjava的變換過程

在RxJava中經常會資料轉換,如map函式,filtmap函式和lift函式。


lift函式

今日頭條Android面試

​我們可以看到這裡我們又建立了一個新的Observable物件,我們記為Observable2,也就是說當我們執行map時,實際上返回了一個新的Observable物件,我們之後的subscribe函式實際上執行再我們新建立的Observable2上,這時他呼叫的就是我們新的call函式,也就是Observable2的call函式(加粗部分),我們來看一下這個operator的call的實現。這裡call傳入的就是我們的Subscriber1物件,也就是呼叫最終的subscribe的處理物件。

call函式

今日頭條Android面試

這裡的transformer就是我們在map呼叫是傳進去的func函式,也就是變換的具體過程。那看之後的onSubscribe.call(回到call中),這裡的onSubscribe是誰呢?就是我們Observable1儲存的onSubscribe物件,也就是我們前面說很重要的那個物件。而這個o(又回來了)就是我們的Subscriber1,這裡可以看出,在呼叫了轉換函式之後我們還是呼叫了一開始的Subscriber1的onNext,最終事件經過轉換傳給了我們的結果。

執行緒切換過程(Scheduler)

RxJava最好用的特點就是提供了方便的執行緒切換,但它的原理歸根結底還是lift,使用subscribeOn()的原理就是建立一個新的Observable,把它的call過程開始的執行投遞到需要的執行緒中;而 observeOn() 則是把執行緒切換的邏輯放在自己建立的Subscriber中來執行。把對於最終的Subscriber1的執行過程投遞到需要的執行緒中來進行。

今日頭條Android面試

​從圖中可以看出,subscribeOn() 和 observeOn() 都做了執行緒切換的工作(圖中的 “schedule…” 部位)。不同的是, subscribeOn()的執行緒切換髮生在 OnSubscribe 中,即在它通知上一級 OnSubscribe 時,這時事件還沒有開始傳送,因此 subscribeOn() 的執行緒控制可以從事件發出的開端就造成影響;而 observeOn() 的執行緒切換則發生在它內建的 Subscriber 中,即發生在它即將給下一級 Subscriber 傳送事件時,因此 observeOn() 控制的是它後面的執行緒。

為什麼subscribeOn()只有第一個有效?
因為它是從通知開始將後面的執行全部投遞到需要的執行緒來執行,但是之後的投遞會受到在它的上級的(但是執行在它之後)的影響,如果上面還有subscribeOn() ,又會投遞到不同的執行緒中去,這樣就不受到它的控制了。

本文來源於非同步社群,作者:xiangzhihong ,作品《今日頭條Android面試》,未經授權,禁止轉載。

今日頭條Android面試

推薦閱讀

2018年5月新書書單(文末福利)

2018年4月新書書單

非同步圖書最全Python書單

一份程式設計師必備的演算法書單

第一本Python神經網路程式設計圖書

今日頭條Android面試

​長按二維碼,可以關注我們喲

每天與你分享IT好文。

點選閱讀原文,檢視更多資訊

閱讀原文​


相關文章