經歷的某度的一場面試
如夢朦朧
九月份的時候有了換工作的躁動,然后投了某度的Android崗位,本以為像我這種非211、985沒工作經驗的渣渣只能被直接pass,結果卻意外的收到了電話,真是受寵若驚.經過電面,技術三面,然后就是等通知到最后拿到了OFFER,如夢一般,真是挺激動的.
面試的準備
當收到HR的面試的通知還是很懵逼的,因為感覺自己突然啥都不會了,迅速鎮定下來,去網上找了一下某度的面試題,但是發現都只有提問了什么并沒有對所提問題的解答,那只能自力更生,像做試卷一樣,一遍總結一遍溫顧.其實大多都是平時開發中用到的,只是我們沒有總結過,被問起來的時候回答的難免會有點捉襟見肘,不能回答的很全面.下面為我個人總(bai)結(du)的,希望對你能有所幫助,但畢竟能力有限,有寫的不對的地方,還望輕噴.雖然噴我我也不會改的.
因為本文篇幅較長建議收藏,在用到時候找出來看一眼.有一些知識點可能沒涉及到,以后會加以補足.因為面試無非是考察你對技術的理解和總結,所以本篇的每個點總結的比較精簡,只是讓你大概的說出來,有的部分是需要能夠畫出原理圖并進行解釋說明,這個要在工作中多積累.
JAVA
一. 類的加載過程,Person person = new Person();為例進行說明。
- 因為new用到了Person.class,所以會先找到Person.class文件,并加載到內存中;
- 執行該類中的static代碼塊,如果有的話,給Person.class類進行初始化;
- 在堆內存中開辟空間分配內存地址;
- 在堆內存中建立對象的特有屬性,并進行默認初始化;
- 對屬性進行顯示初始化;
- 對對象進行構造代碼塊初始化;
- 對對象進行與之對應的構造函數進行初始化;
- 將內存地址付給棧內存中的p變量
二. JVM相關知識,GC機制。
JVM基本構成

從上圖可知,JVM主要包括四個部分:
1.類加載器(ClassLoader):在JVM啟動時或者在類運行時將需要的class加載到JVM中。(下圖表示了從java源文件到JVM的整個過程,可配合理解。

2.執行引擎:負責執行class文件中包含的字節碼指令;
3.內存區(也叫運行時數據區):是在JVM運行的時候操作所分配的內存區。運行時內存區主要可以劃分為5個區域,如圖:

方法區(Method Area):用于存儲類結構信息的地方,包括常量池、靜態變量、構造函數等。雖然JVM規范把方法區描述為堆的一個邏輯部分, 但它卻有個別名non-heap(非堆),所以大家不要搞混淆了。方法區還包含一個運行時常量池。
java堆(Heap):存儲java實例或者對象的地方。這塊是GC的主要區域。從存儲的內容我們可以很容易知道,方法區和堆是被所有java線程共享的。
java棧(Stack):java棧總是和線程關聯在一起,每當創建一個線程時,JVM就會為這個線程創建一個對應的java棧。在這個java棧中又會包含多個棧幀,每運行一個方法就創建一個棧幀,用于存儲局部變量表、操作棧、方法返回值等。每一個方法從調用直至執行完成的過程,就對應一個棧幀在java棧中入棧到出棧的過程。所以java棧是現成私有的。
程序計數器(PC Register):用于保存當前線程執行的內存地址。由于JVM程序是多線程執行的(線程輪流切換),所以為了保證線程切換回來后,還能恢復到原先狀態,就需要一個獨立的計數器,記錄之前中斷的地方,可見程序計數器也是線程私有的。
本地方法棧(Native Method Stack):和java棧的作用差不多,只不過是為JVM使用到的native方法服務的。
4.本地方法接口:主要是調用C或C++實現的本地方法及返回結果。
GC機制
垃圾收集器一般必須完成兩件事:檢測出垃圾;回收垃圾。怎么檢測出垃圾?一般有以下幾種方法:
給一個對象添加引用計數器,每當有個地方引用它,計數器就加1;引用失效就減1。好了,問題來了,如果我有兩個對象A和B,互相引用,除此之外,沒有其他任何對象引用它們,實際上這兩個對象已經無法訪問,即是我們說的垃圾對象。但是互相引用,計數不為0,導致無法回收,所以還有另一種方法:
以根集對象為起始點進行搜索,如果有對象不可達的話,即是垃圾對象。這里的根集一般包括java棧中引用的對象、方法區常良池中引用的對象、本地方法中引用的對象等。
總之,JVM在做垃圾回收的時候,會檢查堆中的所有對象是否會被這些根集對象引用,不能夠被引用的對象就會被垃圾收集器回收。一般回收算法也有如下幾種:
- 標記-清除(Mark-sweep)
- 復制(Copying
- 標記-整理(Mark-Compact)
- 分代收集算法
三. 類的加載器,雙親機制,Android的類加載器。
類的加載器
大家都知道,當我們寫好一個Java程序之后,不是管是CS還是BS應用,都是由若干個.class文件組織而成的一個完整的Java應用程序,當程序在運行時,即會調用該程序的一個入口函數來調用系統的相關功能,而這些功能都被封裝在不同的class文件當中,所以經常要從這個class文件中要調用另外一個class文件中的方法,如果另外一個文件不存在的,則會引發系統異常。而程序在啟動的時候,并不會一次性加載程序所要用的所有class文件,而是根據程序的需要,通過Java的類加載機制(ClassLoader)來動態加載某個class文件到內存當中的,從而只有class文件被載入到了內存之后,才能被其它class所引用。所以ClassLoader就是用來動態加載class文件到內存當中用的。
雙親機制
ClassLoader使用的是雙親委托模型來搜索類的,每個ClassLoader實例都有一個父類加載器的引用(不是繼承的關系,是一個包含的關系),虛擬機內置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但可以用作其它ClassLoader實例的的父類加載器。當一個ClassLoader實例需要加載某個類時,它會試圖親自搜索某個類之前,先把這個任務委托給它的父類加載器,這個過程是由上至下依次檢查的,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,如果沒加載到,則把任務轉交給Extension ClassLoader試圖加載,如果也沒加載到,則轉交給App ClassLoader 進行加載,如果它也沒有加載得到的話,則返回給委托的發起者,由它到指定的文件系統或網絡等URL中加載該類。如果它們都沒有加載到這個類時,則拋出ClassNotFoundException異常。否則將這個找到的類生成一個類的定義,并將它加載到內存當中,最后返回這個類在內存中的Class實例對象。
因為這樣可以避免重復加載,當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。考慮到安全因素,我們試想一下,如果不使用這種委托模式,那我們就可以隨時使用自定義的String來動態替代java核心api中定義的類型,這樣會存在非常大的安全隱患,而雙親委托的方式,就可以避免這種情況,因為String已經在啟動時就被引導類加載器(Bootstrcp ClassLoader)加載,所以用戶自定義的ClassLoader永遠也無法加載一個自己寫的String,除非你改變JDK中ClassLoader搜索類的默認算法。
JVM在判定兩個class是否相同時,不僅要判斷兩個類名是否相同,而且要判斷是否由同一個類加載器實例加載的。只有兩者同時滿足的情況下,JVM才認為這兩個class是相同的。就算兩個class是同一份class字節碼,如果被兩個不同的ClassLoader實例所加載,JVM也會認為它們是兩個不同class。比如網絡上的一個Java類org.classloader.simple.NetClassLoaderSimple,javac編譯之后生成字節碼文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB這兩個類加載器并讀取了NetClassLoaderSimple.class文件,并分別定義出了java.lang.Class實例來表示這個類,對于JVM來說,它們是兩個不同的實例對象,但它們確實是同一份字節碼文件,如果試圖將這個Class實例生成具體的對象進行轉換時,就會拋運行時異常java.lang.ClassCaseException,提示這是兩個不同的類型。
Android類加載器
對于Android而言,最終的apk文件包含的是dex類型的文件,dex文件是將class文件重新打包,打包的規則又不是簡單地壓縮,而是完全對class文件內部的各種函數表,變量表進行優化,產生一個新的文件,即dex文件。因此加載這種特殊的Class文件就需要特殊的類加載器DexClassLoader。
四. 集合框架,list,map,set都有哪些具體的實現類,區別都是什么?
1.List,Set都是繼承自Collection接口,Map則不是;
2.List特點:元素有放入順序,元素可重復;
- Set特點:元素無放入順序,元素不可重復,重復元素會覆蓋掉,(注意:元素雖然無放入順序,但是元素在set中的位置是有該元素的HashCode決定的,其位置其實是固定的,加入Set 的Object必須定義equals()方法;
另外list支持for循環,也就是通過下標來遍歷,也可以用迭代器,但是set只能用迭代,因為他無序,無法用下標來取得想要的值)。
3.Set和List對比:
- Set:檢索元素效率低下,刪除和插入效率高,插入和刪除不會引起元素位置改變。
- List:和數組類似,List可以動態增長,查找元素效率高,插入刪除元素效率低,因為會引起其他元素位置改變。
4.Map適合儲存鍵值對的數據。
5.線程安全集合類與非線程安全集合類
- LinkedList、ArrayList、HashSet是非線程安全的,Vector是線程安全的;
- HashMap是非線程安全的,HashTable是線程安全的;
- StringBuilder是非線程安全的,StringBuffer是線程安全的。
下面是這些類具體的使用介紹:
ArrayList與LinkedList的區別和適用場景
優點:ArrayList是實現了基于動態數組的數據結構,因為地址連續,一旦數據存儲好了,查詢操作效率會比較高(在內存里是連著放的)。
缺點:因為地址連續, ArrayList要移動數據,所以插入和刪除操作效率比較低。
優點:LinkedList基于鏈表的數據結構,地址是任意的,所以在開辟內存空間的時候不需要等一個連續的地址,對于新增和刪除操作add和remove,LinedList比較占優勢。LinkedList 適用于要頭尾操作或插入指定位置的場景。
缺點:因為LinkedList要移動指針,所以查詢操作性能比較低。
適用場景分析:
當需要對數據進行對此訪問的情況下選用ArrayList,當需要對數據進行多次增加刪除修改時采用LinkedList。
ArrayList與Vector的區別和適用場景
ArrayList有三個構造方法:
- public ArrayList(int initialCapacity)//構造一個具有指定初始容量的空列表。
- public ArrayList()//構造一個初始容量為10的空列表。
- public ArrayList(Collection c)//構造一個包含指定 collection 的元素的列表
Vector有四個構造方法:
- public Vector()//使用指定的初始容量和等于零的容量增量構造一個空向量。
- public Vector(int initialCapacity)//構造一個空向量,使其內部數據數組的大小,其標準容量增量為零。
- public Vector(Collection c)//構造一個包含指定 collection 中的元素的向量
- public Vector(int initialCapacity,int capacityIncrement)//使用指定的初始容量和容量增量構造一個空的向量
ArrayList和Vector都是用數組實現的,主要有這么三個區別:
- Vector是多線程安全的,線程安全就是說多線程訪問同一代碼,不會產生不確定的結果。而ArrayList不是,這個可以從源碼中看出,Vector類中的方法很多有synchronized進行修飾,這樣就導致了Vector在效率上無法與ArrayList相比;
- 兩個都是采用的線性連續空間存儲元素,但是當空間不足的時候,兩個類的增加方式是不同。
- Vector可以設置增長因子,而ArrayList不可以。
- Vector是一種老的動態數組,是線程同步的,效率很低,一般不贊成使用。
適用場景:
- Vector是線程同步的,所以它也是線程安全的,而ArrayList是線程異步的,是不安全的。如果不考慮到線程的安全因素,一般用ArrayList效率比較高。
- 如果集合中的元素的數目大于目前集合數組的長度時,在集合中使用數據量比較大的數據,用Vector有一定的優勢。
HashSet與Treeset的適用場景
- TreeSet 是二叉樹(紅黑樹的樹據結構)實現的,Treeset中的數據是自動排好序的,不允許放入null值。
- HashSet 是哈希表實現的,HashSet中的數據是無序的,可以放入null,但只能放入一個null,兩者中的值都不能重復,就如數據庫中唯一約束。
- HashSet要求放入的對象必須實現HashCode()方法,放入的對象,是以hashcode碼作為標識的,而具有相同內容的String對象,hashcode是一樣,所以放入的內容不能重復。但是同一個類的對象可以放入不同的實例。
適用場景分析:
HashSet是基于Hash算法實現的,其性能通常都優于TreeSet。為快速查找而設計的Set,我們通常都應該使用HashSet,在我們需要排序的功能時,我們才使用TreeSet。
HashMap與TreeMap、HashTable的區別及適用場景
HashMap:基于哈希表(散列表)實現。使用HashMap要求添加的鍵類明確定義了hashCode()和equals()[可以重寫hashCode()和equals()],為了優化HashMap空間的使用,您可以調優初始容量和負載因子。其中散列表的沖突處理主要分兩種,一種是開放定址法,另一種是鏈表法。HashMap的實現中采用的是鏈表法。
TreeMap:非線程安全基于紅黑樹實現。TreeMap沒有調優選項,因為該樹總處于平衡狀態。
適用場景分析:
HashMap和HashTable:HashMap去掉了HashTable的contains方法,但是加上了containsValue()和containsKey()方法。HashTable同步的,而HashMap是非同步的,效率上比HashTable要高。HashMap允許空鍵值,而HashTable不允許。
HashMap:適用于Map中插入、刪除和定位元素。
Treemap:適用于按自然順序或自定義順序遍歷鍵(key)。
(ps:其實我們工作的過程中對集合的使用是很頻繁的,稍加注意并總結積累一下,在面試的時候應該會回答的很輕松)
五. concurrentHashmap原理,原子類。
ConcurrentHashMap作為一種線程安全且高效的哈希表的解決方案,尤其其中的"分段鎖"的方案,相比HashTable的全表鎖在性能上的提升非常之大.
六. volatile原理
在《Java并發編程:核心理論》一文中,我們已經提到過可見性、有序性及原子性問題,通常情況下我們可以通過Synchronized關鍵字來解決這些個問題,不過如果對Synchronized原理有了解的話,應該知道Synchronized是一個比較重量級的操作,對系統的性能有比較大的影響,所以,如果有其他解決方案,我們通常都避免使用Synchronized來解決問題。而volatile關鍵字就是Java中提供的另一種解決可見性和有序性問題的方案。對于原子性,需要強調一點,也是大家容易誤解的一點:對volatile變量的單次讀/寫操作可以保證原子性的,如long和double類型變量,但是并不能保證i++這種操作的原子性,因為本質上i++是讀、寫兩次操作。
參考文章插好眼了等傳送
七. 多線程的使用場景
使用多線程就一定效率高嗎? 有時候使用多線程并不是為了提高效率,而是使得CPU能夠同時處理多個事件。
1).為了不阻塞主線程,啟動其他線程來做好事的事情,比如APP中耗時操作都不在UI中做.
2).實現更快的應用程序,即主線程專門監聽用戶請求,子線程用來處理用戶請求,以獲得大的吞吐量.感覺這種情況下,多線程的效率未必高。 這種情況下的多線程是為了不必等待, 可以并行處理多條數據。
比如JavaWeb的就是主線程專門監聽用戶的HTTP請求,然后啟動子線程去處理用戶的HTTP請求。
3).某種雖然優先級很低的服務,但是卻要不定時去做。
比如Jvm的垃圾回收。
4.)某種任務,雖然耗時,但是不耗CPU的操作時,開啟多個線程,效率會有顯著提高。
比如讀取文件,然后處理。 磁盤IO是個很耗費時間,但是不耗CPU計算的工作。 所以可以一個線程讀取數據,一個線程處理數據。肯定比一個線程讀取數據,然后處理效率高。 因為兩個線程的時候充分利用了CPU等待磁盤IO的空閑時間。
八. JAVA常量池
**
- a.當數值范圍為-128~127時:如果兩個new出來Integer對象,即使值相同,通過“==”比較結果為false,但兩個對象直接賦值,則通過“==”比較結果為“true,這一點與String非常相似。
- b.當數值不在-128~127時,無論通過哪種方式,即使兩個對象的值相等,通過“==”比較,其結果為false;
- c.當一個Integer對象直接與一個int基本數據類型通過“==”比較,其結果與第一點相同;
- d.Integer對象的hash值為數值本身;
**
在Integer類中有一個靜態內部類IntegerCache,在IntegerCache類中有一個Integer數組,用以緩存當數值范圍為-128~127時的Integer對象。
九. 簡單介紹一下java中的泛型,泛型擦除以及相關的概念。
泛型是Java SE 1.5的新特性,泛型的本質是參數化類型,也就是說所操作的數據類型被指定為一個參數。這種參數類型可以用在類、接口和方法的創建中,分別稱為泛型類、泛型接口、泛型方法。 Java語言引入泛型的好處是安全簡單。
在Java SE 1.5之前,沒有泛型的情況的下,通過對類型Object的引用來實現參數的“任意化”,“任意化”帶來的缺點是要做顯式的強制類型轉換,而這種轉換是要求開發者對實際參數類型可以預知的情況下進行的。對于強制類型轉換錯誤的情況,編譯器可能不提示錯誤,在運行的時候才出現異常,這是一個安全隱患。
泛型的好處是在編譯的時候檢查類型安全,并且所有的強制轉換都是自動和隱式的,提高代碼的重用率。
- 泛型的類型參數只能是類類型(包括自定義類),不能是簡單類型。
- 同一種泛型可以對應多個版本(因為參數類型是不確定的),不同版本的泛型類實例是不兼容的。
- 泛型的類型參數可以有多個。
- 泛型的參數類型可以使用extends語句,例如。習慣上稱為“有界類型”。
- 泛型的參數類型還可以是通配符類型。例如Class classType = Class.forName("java.lang.String");
泛型擦除以及相關的概念
Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java字節碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會在編譯器在編譯的時候去掉。這個過程就稱為類型擦除。
- 類型擦除引起的問題及解決方法
- 先檢查,在編譯,以及檢查編譯的對象和引用傳遞的問題
- 自動類型轉換
- 類型擦除與多態的沖突和解決方法
- 泛型類型變量不能是基本數據類型
- 運行時類型查詢
- 異常中使用泛型的問題
- 數組(這個不屬于類型擦除引起的問題)
- 類型擦除后的沖突
- 泛型在靜態方法和靜態類中的問題
Android
一. Handler機制
Android 的消息機制也就是 handler 機制,創建 handler 的時候會創建一個 looper ( 通過 looper.prepare() 來創建 ),looper 一般為主線程 looper.
handler 通過 send 發送消息 (sendMessage) ,當然 post 一系列方法最終也是通過 send 來實現的,在 send 方法中handler 會通過 enqueueMessage() 方法中的 enqueueMessage(msg,millis )向消息隊列 MessageQueue 插入一條消息,同時會把本身的 handler 通過 msg.target = this 傳入.
Looper 是一個死循環,不斷的讀取MessageQueue中的消息,loop 方法會調用 MessageQueue 的 next 方法來獲取新的消息,next 操作是一個阻塞操作,當沒有消息的時候 next 方法會一直阻塞,進而導致 loop 一直阻塞,當有消息的時候,Looper 就會處理消息 Looper 收到消息之后就開始處理消息: msg.target.dispatchMessage(msg),當然這里的 msg.target 就是上面傳過來的發送這條消息的 handler 對象,這樣 handler 發送的消息最終又交給他的dispatchMessage方法來處理了,這里不同的是,handler 的 dispatchMessage 方法是在創建 Handler時所使用的 Looper 中執行的,這樣就成功的將代碼邏輯切換到了主線程了.
Handler 處理消息的過程是:首先,檢查Message 的 callback 是否為 null,不為 null 就通過 handleCallBack 來處理消息,Message 的 callback 是一個 Runnable 對象,實際上是 handler 的 post 方法所傳遞的 Runnable 參數.其次是檢查 mCallback 是 否為 null,不為 null 就調用 mCallback 的handleMessage 方法來處理消息.
二. View的繪制流程
View的繪制流程:OnMeasure()——>OnLayout()——>OnDraw()
各步驟的主要工作:
測量視圖大小。從頂層父View到子View遞歸調用measure方法,measure方法又回調OnMeasure。
確定View位置,進行頁面布局。從頂層父View向子View的遞歸調用view.layout方法的過程,即父View根據上一步measure子View所得到的布局大小和布局參數,將子View放在合適的位置上。
繪制視圖:ViewRoot創建一個Canvas對象,然后調用OnDraw()。六個步驟:①、繪制視圖的背景;②、保存畫布的圖層(Layer);③、繪制View的內容;④、繪制View子視圖,如果沒有就不用;⑤、還原圖層(Layer);⑥、繪制滾動條。
三. 事件傳遞機制
- Android事件分發機制的本質是要解決:點擊事件由哪個對象發出,經過哪些對象,最終達到哪個對象并最終得到處理。這里的對象是指Activity、ViewGroup、View.
- Android中事件分發順序:Activity(Window) -> ViewGroup -> View.
- 事件分發過程由dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()三個方法協助完成
設置Button按鈕來響應點擊事件事件傳遞情況:(如下圖)
布局如下:

- 最外層:Activiy A,包含兩個子View:ViewGroup B、View C
- 中間層:ViewGroup B,包含一個子View:View C
- 最內層:View C
假設用戶首先觸摸到屏幕上View C上的某個點(如圖中黃色區域),那么Action_DOWN事件就在該點產生,然后用戶移動手指并最后離開屏幕。
- 按鈕點擊事件:
- DOWN事件被傳遞給C的onTouchEvent方法,該方法返回true,表示處理這個事件;
- 因為C正在處理這個事件,那么DOWN事件將不再往上傳遞給B和A的onTouchEvent();
- 該事件列的其他事件(Move、Up)也將傳遞給C的onTouchEvent();

(記住這個圖的傳遞順序,面試的時候能夠畫出來,就很詳細了)
四. Binder機制
1.了解Binder
在Android系統中,每一個應用程序都運行在獨立的進程中,這也保證了當其中一個程序出現異常而不會影響另一個應用程序的正常運轉。在許多情況下,我們activity都會與各種系統的service打交道,很顯然,我們寫的程序中activity與系統service肯定不是同一個進程,但是它們之間是怎樣實現通信的呢?所以Binder是android中一種實現進程間通信(IPC)的方式之一。
1).首先,Binder分為Client和Server兩個進程。
注意,Client和Server是相對的。誰發消息,誰就是Client,誰接收消息,誰就是Server。
舉個例子,兩個進程A和B之間使用Binder通信,進程A發消息給進程B,那么這時候A是Binder Client,B是Binder Server;進程B發消息給進程A,那么這時候B是Binder Client,A是Binder Server——其實這么說雖然簡單了,但還是不太嚴謹,我們先這么理解著。
2).其次,我們看下面這個圖(摘自田維術的博客),基本說明白了Binder的組成解構:

圖中的IPC就是進程間通信的意思。
圖中的ServiceManager,負責把Binder Server注冊到一個容器中。
有人把ServiceManager比喻成電話局,存儲著每個住宅的座機電話,還是很恰當的。張三給李四打電話,撥打電話號碼,會先轉接到電話局,電話局的接線員查到這個電話號碼的地址,因為李四的電話號碼之前在電話局注冊過,所以就能撥通;沒注冊,就會提示該號碼不存在。
對照著Android Binder機制,對著上面這圖,張三就是Binder Client,李四就是Binder Server,電話局就是ServiceManager,電話局的接線員在這個過程中做了很多事情,對應著圖中的Binder驅動.
3).接下來我們看Binder通信的過程,還是摘自田維術博客的一張圖:

注:圖中的SM也就是ServiceManager。
我們看到,Client想要直接調用Server的add方法,是不可以的,因為它們在不同的進程中,這時候就需要Binder來幫忙了。
- 首先是Server在SM這個容器中注冊。
- 其次,Client想要調用Server的add方法,就需要先獲取Server對象, 但是SM不會把真正的Server對象返回給Client,而是把Server的一個代理對象返回給Client,也就是Proxy。
- 然后,Client調用Proxy的add方法,SM會幫他去調用Server的add方法,并把結果返回給Client。
以上這3步,Binder驅動出了很多力,但我們不需要知道Binder驅動的底層實現,涉及到C++的代碼了——把有限的時間去做更有意義的事情。
(ps:以上節選自包建強老師的文章點我點我 ).
2.為什么android選用Binder來實現進程間通信?
1).可靠性。在移動設備上,通常采用基于Client-Server的通信方式來實現互聯網與設備間的內部通信。目前linux支持IPC包括傳統的管道,System V IPC,即消息隊列/共享內存/信號量,以及socket中只有socket支持Client-Server的通信方式。Android系統為開發者提供了豐富進程間通信的功能接口,媒體播放,傳感器,無線傳輸。這些功能都由不同的server來管理。開發都只關心將自己應用程序的client與server的通信建立起來便可以使用這個服務。毫無疑問,如若在底層架設一套協議來實現Client-Server通信,增加了系統的復雜性。在資源有限的手機 上來實現這種復雜的環境,可靠性難以保證。
2).傳輸性能。socket主要用于跨網絡的進程間通信和本機上進程間的通信,但傳輸效率低,開銷大。消息隊列和管道采用存儲-轉發方式,即數據先從發送方緩存區拷貝到內核開辟的一塊緩存區中,然后從內核緩存區拷貝到接收方緩存區,其過程至少有兩次拷貝。雖然共享內存無需拷貝,但控制復雜。比較各種IPC方式的數據拷貝次數。共享內存:0次。Binder:1次。Socket/管道/消息隊列:2次。
3).安全性。Android是一個開放式的平臺,所以確保應用程序安全是很重要的。Android對每一個安裝應用都分配了UID/PID,其中進程的UID是可用來鑒別進程身份。傳統的只能由用戶在數據包里填寫UID/PID,這樣不可靠,容易被惡意程序利用。而我們要求由內核來添加可靠的UID。
所以,出于可靠性、傳輸性、安全性。android建立了一套新的進程間通信方式。
五. Android中進程的級別,以及各自的區別。
1、前臺進程
用戶當前正在做的事情需要這個進程。如果滿足下面的條件之一,一個進程就被認為是前臺進程:
- 這個進程擁有一個正在與用戶交互的Activity(這個Activity的onResume()方法被調用)。
- 這個進程擁有一個綁定到正在與用戶交互的activity上的Service。
- 這個進程擁有一個前臺運行的Service(service調用了方法startForeground()).
- 這個進程擁有一個正在執行其任何一個生命周期回調方法(onCreate(),onStart(),或onDestroy())的Service。
- 這個進程擁有正在執行其onReceive()方法的BroadcastReceiver。
通常,在任何時間點,只有很少的前臺進程存在。它們只有在達到無法調合的矛盾時才會被殺--如內存太小而不能繼續運行時。通常,到了這時,設備就達到了一個內存分頁調度狀態,所以需要殺一些前臺進程來保證用戶界面的反應.
2、可見進程
一個進程不擁有運行于前臺的組件,但是依然能影響用戶所見。滿足下列條件時,進程即為可見:
這個進程擁有一個不在前臺但仍可見的Activity(它的onPause()方法被調用)。當一個前臺activity啟動一個對話框時,就出了這種情況。
3、服務進程
一個可見進程被認為是極其重要的。并且,除非只有殺掉它才可以保證所有前臺進程的運行,否則是不能動它的。
這個進程擁有一個綁定到可見activity的Service。
一個進程不在上述兩種之內,但它運行著一個被startService()所啟動的service。
盡管一個服務進程不直接影響用戶所見,但是它們通常做一些用戶關心的事情(比如播放音樂或下載數據),所以系統不到前臺進程和可見進程活不下去時不會殺它。
4、后臺進程
一個進程擁有一個當前不可見的activity(activity的onStop()方法被調用)。
這樣的進程們不會直接影響到用戶體驗,所以系統可以在任意時刻殺了它們從而為前臺、可見、以及服務進程們提供存儲空間。通常有很多后臺進程在運行。它們被保存在一個LRU(最近最少使用)列表中來確保擁有最近剛被看到的activity的進程最后被殺。如果一個activity正確的實現了它的生命周期方法,并保存了它的當前狀態,那么殺死它的進程將不會對用戶的可視化體驗造成影響。因為當用戶返回到這個activity時,這個activity會恢復它所有的可見狀態。
5、空進程
一個進程不擁有入何active組件。
保留這類進程的唯一理由是高速緩存,這樣可以提高下一次一個組件要運行它時的啟動速度。系統經常為了平衡在進程高速緩存和底層的內核高速緩存之間的整體系統資源而殺死它們。
六. 線程池的相關知識。
Android中的線程池都是之間或間接通過配置ThreadPoolExecutor來實現不同特性的線程池.Android中最常見的四類具有不同特性的線程池分別為FixThreadPool、CachedThreadPool、SingleThreadPool、ScheduleThreadExecutor.
1).FixThreadPool
只有核心線程,并且數量固定的,也不會被回收,所有線程都活動時,因為隊列沒有限制大小,新任務會等待執行.
優點:更快的響應外界請求.
2).SingleThreadPool
只有一個核心線程,確保所有的任務都在同一線程中按順序完成.因此不需要處理線程同步的問題.
3).CachedThreadPool
只有非核心線程,最大線程數非常大,所有線程都活動時,會為新任務創建新線程,否則會利用空閑線程(60s空閑時間,過了就會被回收,所以線程池中有0個線程的可能)處理任務.
優點:任何任務都會被立即執行(任務隊列SynchronousQueue相當于一個空集合);比較適合執行大量的耗時較少的任務.
4).ScheduledThreadPool
核心線程數固定,非核心線程(閑著沒活干會被立即回收)數沒有限制.
優點:執行定時任務以及有固定周期的重復任務
七. 內存泄露,怎樣查找,怎么產生的內存泄露。
產生的內存泄露
- 資源對象沒關閉造成的內存泄漏
- 構造Adapter時,沒有使用緩存的convertView
- Bitmap對象不在使用時調用recycle()釋放內存
- 試著使用關于application的context來替代和activity相關的context
- 注冊沒取消造成的內存泄漏
- 集合中對象沒清理造成的內存泄漏
查找內存泄漏
查找內存泄漏可以使用Android Stdio 自帶的Android Profiler工具,也可以使用Square產品的LeadCanary.
八. Android優化
性能優化
- 節制的使用Service 如果應用程序需要使用Service來執行后臺任務的話,只有當任務正在執行的時候才應該讓Service運行起來。當啟動一個Service時,系統會傾向于將這個Service所依賴的進程進行保留,系統可以在LRUcache當中緩存的進程數量也會減少,導致切換程序的時候耗費更多性能。我們可以使用IntentService,當后臺任務執行結束后會自動停止,避免了Service的內存泄漏。
- 當界面不可見時釋放內存 當用戶打開了另外一個程序,我們的程序界面已經不可見的時候,我們應當將所有和界面相關的資源進行釋放。重寫Activity的onTrimMemory()方法,然后在這個方法中監聽TRIM_MEMORY_UI_HIDDEN這個級別,一旦觸發說明用戶離開了程序,此時就可以進行資源釋放操作了。
- 當內存緊張時釋放內存 onTrimMemory()方法還有很多種其他類型的回調,可以在手機內存降低的時候及時通知我們,我們應該根據回調中傳入的級別來去決定如何釋放應用程序的資源。
- 避免在Bitmap上浪費內存 讀取一個Bitmap圖片的時候,千萬不要去加載不需要的分辨率。可以壓縮圖片等操作。
- 使用優化過的數據集合 Android提供了一系列優化過后的數據集合工具類,如SparseArray、SparseBooleanArray、LongSparseArray,使用這些API可以讓我們的程序更加高效。HashMap工具類會相對比較低效,因為它需要為每一個鍵值對都提供一個對象入口,而SparseArray就避免掉了基本數據類型轉換成對象數據類型的時間。
布局優化
1).重用布局文件
標簽可以允許在一個布局當中引入另一個布局,那么比如說我們程序的所有界面都有一個公共的部分,這個時候最好的做法就是將這個公共的部分提取到一個獨立的布局中,然后每個界面的布局文件當中來引用這個公共的布局。
Tips:如果我們要在標簽中覆寫layout屬性,必須要將layout_width和layout_height這兩個屬性也進行覆寫,否則覆寫效果將不會生效。
標簽是作為標簽的一種輔助擴展來使用的,它的主要作用是為了防止在引用布局文件時引用文件時產生多余的布局嵌套。布局嵌套越多,解析起來就越耗時,性能就越差。因此編寫布局文件時應該讓嵌套的層數越少越好。
舉例:比如在LinearLayout里邊使用一個布局。里邊又有一個LinearLayout,那么其實就存在了多余的布局嵌套,使用merge可以解決這個問題。
2).僅在需要時才加載布局
某個布局當中的元素不是一起顯示出來的,普通情況下只顯示部分常用的元素,而那些不常用的元素只有在用戶進行特定操作時才會顯示出來。
舉例:填信息時不是需要全部填的,有一個添加更多字段的選項,當用戶需要添加其他信息的時候,才將另外的元素顯示到界面上。用VISIBLE性能表現一般,可以用ViewStub。ViewStub也是View的一種,但是沒有大小,沒有繪制功能,也不參與布局,資源消耗非常低,可以認為完全不影響性能。
- <ViewStub
- android:id="@+id/view_stub"
- android:layout="@layout/profile_extra"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- />
- public void onMoreClick() {
- ViewStub viewStub = (ViewStub) findViewById(R.id.view_stub);
- if (viewStub != null) {
- View inflatedView = viewStub.inflate();
- editExtra1 = (EditText) inflatedView.findViewById(R.id.edit_extra1);
- editExtra2 = (EditText) inflatedView.findViewById(R.id.edit_extra2);
- editExtra3 = (EditText) inflatedView.findViewById(R.id.edit_extra3);
- }
- }
tips:ViewStub所加載的布局是不可以使用標簽的,因此這有可能導致加載出來出來的布局存在著多余的嵌套結構。
高性能編碼優化
都是一些微優化,在性能方面看不出有什么顯著的提升的。使用合適的算法和數據結構是優化程序性能的最主要手段。
1).避免創建不必要的對象 不必要的對象我們應該避免創建:
如果有需要拼接的字符串,那么可以優先考慮使用StringBuffer或者StringBuilder來進行拼接,而不是加號連接符,因為使用加號連接符會創建多余的對象,拼接的字符串越長,加號連接符的性能越低。
當一個方法的返回值是String的時候,通常需要去判斷一下這個String的作用是什么,如果明確知道調用方會將返回的String再進行拼接操作的話,可以考慮返回一個StringBuffer對象來代替,因為這樣可以將一個對象的引用進行返回,而返回String的話就是創建了一個短生命周期的臨時對象。
盡可能地少創建臨時對象,越少的對象意味著越少的GC操作。
2).在沒有特殊原因的情況下,盡量使用基本數據類型來代替封裝數據類型,int比Integer要更加有效,其它數據類型也是一樣。
基本數據類型的數組也要優于對象數據類型的數組。另外兩個平行的數組要比一個封裝好的對象數組更加高效,舉個例子,Foo[]和Bar[]這樣的數組,使用起來要比Custom(Foo,Bar)[]這樣的一個數組高效的多。
3).靜態優于抽象
如果你并不需要訪問一個對系那個中的某些字段,只是想調用它的某些方法來去完成一項通用的功能,那么可以將這個方法設置成靜態方法,調用速度提升15%-20%,同時也不用為了調用這個方法去專門創建對象了,也不用擔心調用這個方法后是否會改變對象的狀態(靜態方法無法訪問非靜態字段)。
4).對常量使用static final修飾符
- static int intVal = 42;
- static String strVal = "Hello, world!";
編譯器會為上面的代碼生成一個初始方法,稱為方法,該方法會在定義類第一次被使用的時候調用。這個方法會將42的值賦值到intVal當中,從字符串常量表中提取一個引用賦值到strVal上。當賦值完成后,我們就可以通過字段搜尋的方式去訪問具體的值了。
final進行優化:
- static final int intVal = 42;
- static final String strVal = "Hello, world!";
這樣,定義類就不需要方法了,因為所有的常量都會在dex文件的初始化器當中進行初始化。當我們調用intVal時可以直接指向42的值,而調用strVal會用一種相對輕量級的字符串常量方式,而不是字段搜尋的方式。
這種優化方式只對基本數據類型以及String類型的常量有效,對于其他數據類型的常量是無效的。
5).使用增強型for循環語法
- static class Counter {
- int mCount;
- }
- Counter[] mArray = ...
- public void zero() {
- int sum = 0;
- for (int i = 0; i < mArray.length; ++i) {
- sum += mArray[i].mCount;
- } }
- public void one() {
- int sum = 0;
- Counter[] localArray = mArray;
- int len = localArray.length;
- for (int i = 0; i < len; ++i) {
- sum += localArray[i].mCount;
- }
- }
- public void two() {
- int sum = 0;
- for (Counter a : mArray) {
- sum += a.mCount;
- }
- }
zero()最慢,每次都要計算mArray的長度,one()相對快得多,two()fangfa在沒有JIT(Just In Time Compiler)的設備上是運行最快的,而在有JIT的設備上運行效率和one()方法不相上下,需要注意這種寫法需要JDK1.5之后才支持。
Tips:ArrayList手寫的循環比增強型for循環更快,其他的集合沒有這種情況。因此默認情況下使用增強型for循環,而遍歷ArrayList使用傳統的循環方式。
6).多使用系統封裝好的API
系統提供不了的Api完成不了我們需要的功能才應該自己去寫,因為使用系統的Api很多時候比我們自己寫的代碼要快得多,它們的很多功能都是通過底層的匯編模式執行的。 舉個例子,實現數組拷貝的功能,使用循環的方式來對數組中的每一個元素一一進行賦值當然可行,但是直接使用系統中提供的System.arraycopy()方法會讓執行效率快9倍以上。
7).避免在內部調用Getters/Setters方法
面向對象中封裝的思想是不要把類內部的字段暴露給外部,而是提供特定的方法來允許外部操作相應類的內部字段。但在Android中,字段搜尋比方法調用效率高得多,我們直接訪問某個字段可能要比通過getters方法來去訪問這個字段快3到7倍。但是編寫代碼還是要按照面向對象思維的,我們應該在能優化的地方進行優化,比如避免在內部調用getters/setters方法。
九. 插件化相關技術,熱修補技術是怎樣實現的,和插件化有什么區別
相同點:
都使用ClassLoader來實現的加載的新的功能類,都可以使用PathClassLoader與DexClassLoader
不同點:
熱修復因為是為了修復Bug的,所以要將新的同名類替代同名的Bug類,要搶先加載新的類而不是Bug類,所以多做兩件事:在原先的app打包的時候,阻止相關類去打上CLASS_ISPREVERIFIED標志,還有在熱修復時動態改變BaseDexClassLoader對象間接引用的dexElements,這樣才能搶先代替Bug類,完成系統不加載舊的Bug類.
而插件化只是增肌新的功能類或者是資源文件,所以不涉及搶先加載舊的類這樣的使命,就避過了阻止相關類去打上CLASS_ISPREVERIFIED標志和還有在熱修復時動態改變BaseDexClassLoader對象間接引用的dexElements.
所以插件化比熱修復簡單,熱修復是在插件化的基礎上在進行替舊的Bug類
十. 怎樣計算一張圖片的大小,加載bitmap過程(怎樣保證不產生內存溢出),二級緩存,LRUCache算法。
計算一張圖片的大小
圖片占用內存的計算公式:圖片高度 圖片寬度 一個像素占用的內存大小.所以,計算圖片占用內存大小的時候,要考慮圖片所在的目錄跟設備密度,這兩個因素其實影響的是圖片的高寬,android會對圖片進行拉升跟壓縮。
加載bitmap過程(怎樣保證不產生內存溢出)
由于Android對圖片使用內存有限制,若是加載幾兆的大圖片便內存溢出。Bitmap會將圖片的所有像素(即長x寬)加載到內存中,如果圖片分辨率過大,會直接導致內存OOM,只有在BitmapFactory加載圖片時使用BitmapFactory.Options對相關參數進行配置來減少加載的像素。
BitmapFactory.Options相關參數詳解
(1).Options.inPreferredConfig值來降低內存消耗。
比如:默認值ARGB_8888改為RGB_565,節約一半內存。
(2).設置Options.inSampleSize 縮放比例,對大圖片進行壓縮 。
(3).設置Options.inPurgeable和inInputShareable:讓系統能及時回 收內存。
A:inPurgeable:設置為True時,表示系統內存不足時可以被回 收,設置為False時,表示不能被回收。
B:inInputShareable:設置是否深拷貝,與inPurgeable結合使用,inPurgeable為false時,該參數無意義。
(4).使用decodeStream代替其他方法。
decodeResource,setImageResource,setImageBitmap等方法
十一. LRUCache算法是怎樣實現的。
內部存在一個LinkedHashMap和maxSize,把最近使用的對象用強引用存儲在 LinkedHashMap中,給出來put和get方法,每次put圖片時計算緩存中所有圖片總大小,跟maxSize進行比較,大于maxSize,就將最久添加的圖片移除;反之小于maxSize就添加進來。
之前,我們會使用內存緩存技術實現,也就是軟引用或弱引用,在Android 2.3(APILevel 9)開始,垃圾回收器會更傾向于回收持有軟引用或弱引用的對象,這讓軟引用和弱引用變得不再可靠。
算法
一. 算法題
m n的矩陣,能形成幾個正方形(2 2能形成1個正方形,2 3 2個,3 3 6個)
計數的關鍵是要觀察到任意一個傾斜的正方形必然唯一內接于一個非傾斜的正方形,而一個非傾斜的邊長為k的非傾斜正方形,一條邊上有k-1個內點,每個內點恰好確定一個內接于其中的傾斜正方形,加上非傾斜正方形本身,可知,將邊長為k的非傾斜正方形數目乘以k,再按k求和即可得到所有正方形的數目。
- 設2≤n≤m,k≤n-1,則邊長為k的非傾斜有
- (n-k)(m-k)個,故所有正方形有
- ∑(m-k)(n-k)k個
- 例如m=n=4
- 正方形有33=20個
下面是面試過程中遇到的題目
大多數題目都可以在上面找到答案.
電話面試題
- ArrayList 和 Hashmap 簡單說一些,區別,底層的數據結構.
- Handler 消息機制
- 引起內存泄漏的場景
- 多線程的使用場景?
- 常用的線程池有哪幾種?
- 在公司做了什么?團隊規模?為什么離職?
面試中實際涉及到的問題
第一輪
- 知道哪些單例模式,寫一個線程安全的單例,并分析為什么是線程安全的?
- Java中的集合有哪些?解釋一下HashMap?底部的數據結構?散列表沖突的處理方法,散列表是一個什么樣的數據結構?HashMap是采用什么方法處理沖突的?
- 解釋一下什么是MVP架構,畫出圖解,一句話解釋MVP和MVC的區別?
- Handle消息機制?在使用Handler的時候要注意哪些東西,是否會引起內存泄漏?畫一下Handler機制的圖解?
- 是否做過性能優化?已經采取了哪些措施進行優化?
- 引起內存泄漏的原因是什么?以及你是怎么解決的?
這些問題應該都是比較基礎的問題,每個開發者都應該是非常熟悉并能詳細敘述的.這一輪的面試官問的技術都是平時用到的.
第二輪
- 關于并發理解多少?說幾個并發的集合?
- Handler 消息機制圖解?
- 在項目中做了哪些東西?
- 畫圖說明View 事件傳遞機制?并舉一個例子闡述
- 類加載機制,如何換膚,換膚插件中存在的問題?hotfix是否用過,原理是否了解?
- 說說項目中用到了哪些設計模式,說了一下策略模式和觀察者模式?
- 會JS么?有Hybid開發經驗么?
- 說一下快排的思想?手寫代碼
- 堆有哪些數據結構?
對于這輪米那是明顯感覺到壓力,知識的縱向了解也比較深,應該是個leader.
第三輪
- 介紹一下在項目中的角色?
- 遇到困難是怎么解決的?
- 如何與人相處,與別人意見相左的時候是怎么解決的,并舉生活中的一個例子.
- 有沒有壓力特別大的時候?
這個應該是項目經理了,問的問題偏向于生活性格方面.
以上面試中問到的題目基本上都可以在上面找到答案,所以做準備是很重要的,但技術是一點點積累的,就算你全會背了,面試過了,真正等到工作的時候還是會捉襟見肘的,所以踏實點吧騷年.