點擊關注異步圖書,置頂公衆號html
天天與你分享 IT好書 技術乾貨 職場知識前端
首先說一下,今日頭條的面試主要分爲三輪到四輪,若是是旺季面三輪,首先是基礎面試,基本面試通常10個題左右,最近面試了一下今日頭條的移動Android資深工程師,記錄下。
java
第一面是北京的開發進行視頻面試,有理論和編程題組成。用的是在線編程工具,以下圖。 android
1,請編程實現單例模式,懶漢和飽漢寫法。c++
2,請編程實現Java的生產者-消費者模型 程序員
看到這個有點懵逼,要是大學畢業的時候寫這個確定沒問題,這都工做多年,這也只能按照本身的思路寫了。這裏使用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。spring
5,Android的進程間通訊,Liunx操做系統的進程間通訊。
關於這個問題也是被問的不少,此處也不作解釋。編程
6,JVM虛擬機內存結構,以及它們的做用。
這個問題也比較基礎,JVM的內存結構以下圖所示。
能夠經過下面的問題來學習:
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)
顯然,第二種實現的難點比較高,不過只要此種知足條件。
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
5,主線程中的Looper.loop()一直無限循環爲何不會形成ANR?
ActivityThread.java 是主線程入口的類,ActivityThread.java 的main函數的內容以下。
而後再看Looper.loop()的源碼,能夠發現:
顯然,ActivityThread的main方法主要就是作消息循環,一旦退出消息循環,那麼你的應用也就退出了。那麼這個死循環不會形成ANR異常呢?
說明:由於Android 的是由事件驅動的,looper.loop() 不斷地接收事件、處理事件,每個點擊觸摸或者說Activity的生命週期都是運行在 Looper.loop() 的控制之下,若是它中止了,應用也就中止了。只能是某一個消息或者說對消息的處理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。也就說咱們的代碼其實就是在這個循環裏面去執行的,固然不會阻塞了。來看一下handleMessage的源碼:
能夠看見Activity的生命週期都是依靠主線程的Looper.loop,當收到不一樣Message時則採用相應措施。
若是某個消息處理時間過長,好比你在onCreate(),onResume()裏面處理耗時操做,那麼下一次的消息好比用戶的點擊事件不能處理了,整個循環就會產生卡頓,時間一長就成了ANR。
總結:Looer.loop()方法可能會引發主線程的阻塞,但只要它的消息循環沒有被阻塞,能一直處理事件就不會產生ANR異常。
6,圖片框架的一些原理知識
7,其餘的一些Android的模塊化開發,熱更新,組件化等知識。
在Android面試的時候,常常會被問到一些Android開發中用到的一些開發框架,如常見的網絡請求框架Retrofit/OkHttp,組件通訊框架EventBus/Dagger2,異步編程RxJava/RxAndroid等。本文給你們整理下上面的幾個框架,以備面試用。
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,投遞到主線程中去執行。
首先要明確EventBus中最核心的就是動態代理技術。
首先動態代理是區別於靜態代理的,代理模式中須要代理類和實際執行類同時實現一個相同的接口,而且在每一個接口定義的方法先後都要加入相同的代碼,這樣有可能不少方法代理類都須要重複。而動態代理就是將這個步驟放入運行時的過程,一個代理類只須要實現InvocationHandler接口中的invoke方法,當須要動態代理時只須要根據接口和一個實現了InvocationHandler的代理對象A生成一個最終的自動生成的代理對象A*。這樣最終的代理對象A*不管調用什麼方法,都會執行InvocationHandler的代理對象A的invoke函數,你就能夠在這個invoke函數中實現真正的代理邏輯。
動態代理的實現機制實際上就是使用Proxy.newProxyInstance函數爲動態代理對象A生成一個代理對象A*的類的字節碼從而生成具體A*對象過程,這個A*類具備幾個特色,一是它須要實現傳入的接口,第二就是全部接口的實現中都會調用A的invoke方法,而且傳入相應的調用實際方法(即接口中的方法)。
Retrofit中使用了動態代理是不錯,可是並非爲了真正的代理才使用的,它只是爲了動態代理一個很是重要的功能,就是「攔截」功能。咱們知道動態代理中自動生成的A*對象的全部方法執行都會調用實際代理類A中的invoke方法,再由咱們在invoke中實現真正代理的邏輯,實際上也就是A*的全部方法都被A對象給攔截了。
而Retrofit的功能就是將代理變成像方法調用那麼簡單。
再用這個retrofit對象建立一個ServiceApi對象,並經過getAuthor函數來調用函數。
也就是一個網絡調用你只須要在你建立的接口裏面經過註解進行設置,而後經過retrofit建立一個api而後調用,就能夠自動完成一個Okhttp的Call的建立。Retrofit的create()函數的代碼以下:
咱們能夠看出怎麼從接口類建立成一個API對象?就是使用了動態代理中的攔截技術,經過建立一個符合此接口的動態代理對象A*,那A呢?就是這其中建立的這個匿名類了,它在內部實現了invoke函數,這樣A*調用的就是A中的invoke函數,也就是被攔截了,實際運行invoke。而invoke就是根據調用的method的註解(,從而生成一個符合條件的Okhttp的Call對象,並進行真正的請求。
Retrofit其實是爲了更方便的使用Okhttp,由於Okhttp的使用就是構建一個Call,而構建Call的大部分過程都是類似的,而Retrofit正是利用了代理機制帶咱們動態的建立Call,而Call的建立信息就來自於你的註解。
關於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的返回值決定,在等待時間到了以後再進行清理任務。相關代碼以下:
其中,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函數的源碼以下:
RxJava
從15年開始,前端掀起了一股異步編程的熱潮,在移動Android編程過程當中,常常會聽到觀察者與被觀察者等概念。
Observable的經過create函數建立一個觀察者對象。
Observable的構造函數以下:
建立了一個Observable咱們記爲Observable1,保存了傳入的OnSubscribe對象爲onSubscribe,這個很重要,後面會說到。
onSubscribe方法
Rxjava的變換過程
在RxJava中常常會數據轉換,如map函數,filtmap函數和lift函數。
lift函數
咱們能夠看到這裏咱們又建立了一個新的Observable對象,咱們記爲Observable2,也就是說當咱們執行map時,實際上返回了一個新的Observable對象,咱們以後的subscribe函數實際上執行再咱們新建立的Observable2上,這時他調用的就是咱們新的call函數,也就是Observable2的call函數(加粗部分),咱們來看一下這個operator的call的實現。這裏call傳入的就是咱們的Subscriber1對象,也就是調用最終的subscribe的處理對象。
call函數
這裏的transformer就是咱們在map調用是傳進去的func函數,也就是變換的具體過程。那看以後的onSubscribe.call(回到call中),這裏的onSubscribe是誰呢?就是咱們Observable1保存的onSubscribe對象,也就是咱們前面說很重要的那個對象。而這個o(又回來了)就是咱們的Subscriber1,這裏能夠看出,在調用了轉換函數以後咱們仍是調用了一開始的Subscriber1的onNext,最終事件通過轉換傳給了咱們的結果。
RxJava最好用的特色就是提供了方便的線程切換,但它的原理歸根結底仍是lift,使用subscribeOn()的原理就是建立一個新的Observable,把它的call過程開始的執行投遞到須要的線程中;而 observeOn() 則是把線程切換的邏輯放在本身建立的Subscriber中來執行。把對於最終的Subscriber1的執行過程投遞到須要的線程中來進行。
從圖中能夠看出,subscribeOn() 和 observeOn() 都作了線程切換的工做(圖中的 「schedule…」 部位)。不一樣的是, subscribeOn()的線程切換髮生在 OnSubscribe 中,即在它通知上一級 OnSubscribe 時,這時事件尚未開始發送,所以 subscribeOn() 的線程控制能夠從事件發出的開端就形成影響;而 observeOn() 的線程切換則發生在它內建的 Subscriber 中,即發生在它即將給下一級 Subscriber 發送事件時,所以 observeOn() 控制的是它後面的線程。
爲何subscribeOn()只有第一個有效?
由於它是從通知開始將後面的執行所有投遞到須要的線程來執行,可是以後的投遞會受到在它的上級的(可是執行在它以後)的影響,若是上面還有subscribeOn() ,又會投遞到不一樣的線程中去,這樣就不受到它的控制了。
本文來源於異步社區,做者:xiangzhihong ,做品《今日頭條Android面試》,未經受權,禁止轉載。
推薦閱讀
長按二維碼,能夠關注咱們喲
天天與你分享IT好文。
點擊閱讀原文,查看更多信息