2019年爲android開發準備的面試題(含答案)

2018年算是互聯網的寒冬,一大波公司宣揚裁員,也確實裁掉一部分,有的拿到了高額的補償,可以慢慢拿着工資、喝着小酒、找着工作,甚至找個一年半載也不是問題。雖然自己公司還沒揚言裁員,且自己還茫目自信,再怎麼裁也不會裁到我這等優秀員工身上啊。但寒冬歸寒冬,飯還是要喫、酒還是要喝,做技術的不管需不需要面試,也要時不時抽空更新下自己,讓自己漲漲知識,一些基本知識點重新拾起,也爲自己不久的將來面試做準備吧。

2019,加油〜〜

下面的題目全是從別人那copy過來的,答案全部是自己總結的,希望自己能溫故而知新〜〜

(一) java基礎面試知識點

1、java中==和equals和hashCode的區別

1)==若是基本數據類型比較,是比較值,若是引用類型,則比較的是他們在內存中的存放地址。對象是存放在堆中,棧中存放的對象的引用,所以==是對棧中的值進行比較,若返回true代表變量的內存地址相等;

2)equals是Object類中的方法,Object類的equals方法用於判斷對象的內存地址引用是不是同一個地址(是不是同一個對象)。若是類中覆蓋了equals方法,就要根據具體代碼來確定,一般覆蓋後都是通過對象的內容是否相等來判斷對象是否相等。

3)hashCode()計算出對象實例的哈希碼,在對象進行散列時作爲key存入。之所以有hashCode方法,因爲在批量的對象比較中,hashCode比較要比equals快。在添加新元素時,先調用這個元素的hashCode方法,一下子能定位到它應該旋轉的物理位置,若該位置沒有元素,可直接存儲;若該位置有元素,就調用它的equals方法與新元素進行比較,若相同則不存,不相同,就放到該位置的鏈表末端。

4)equals與hashCode方法關係:

hashCode()是一個本地方法,實現是根據本地機器上關的。equals()相等的對象,hashCode()也一定相等;hashCode()不等,equals()一定也不等;hashCode()相等,equals()可能相等,也可能不等。

所以在重寫equals(Object obj)方法,有必要重寫hashCode()方法,確保通過equals(Object obj)方法判斷結果爲true的兩個對象具備相等的hashCode()返回值。

5)equals與==的關係:

Integer b1 = 127;在java編譯時被編譯成Integer b1 = Integer.valueOf(127);對於-128到127之間的Integer值,用的是原生數據類型int,會在內存裏供重用,也就是這之間的Integer值進行==比較時,只是進行int原生數據類型的數值進行比較。而超出-128〜127的範圍,進行==比較時是進行地址及數值比較。

 

2、int、char、long各佔多少字節數

int\float佔用4個字節,short\char佔用2個字節,long\double佔用8個字節,byte佔用1個字節 boolean佔一位

基本數據類型存放在棧裏,包裝類棧裏存放的是對象的引用,即值的地址,而值存放在堆裏。

3、int與integer的區別

Integer是int的包裝類,int則是java的一種基本數據類型,Integer變量必須實例化才能使用,當new一個Integer時,實際是生成一個指向此對象的引用,而int是直接存儲數據的值,Integer默認值是null,而int默認值是0

4、談談對java多態的理解

同一個消息可以根據發送對象的不同而採用多種不同的行爲方式,在執行期間判斷所引用的對象的實際類型,根據其實際的類型調用其相應的方法。

作用:消除類型之間的耦合關係。實現多態的必要條件:繼承、重寫(因爲必須調用父類中存在的方法)、父類引用指向子類對象

5、String、StringBuffer、StringBuilder區別

都是字符串類,String類中使用字符數組保存字符串,因有final修飾符,String對象是不可變的,每次對String操作都會生成新的String對象,這樣效率低,且浪費內存空間。但線程安全。

StringBuilder和StringBuffer也是使用字符數組保存字符,但這兩種對象都是可變的,即對字符串進行append操作,不會產生新的對象。它們的區別是:StringBuffer對方法加了同步鎖,是線程安全的,StringBuilder非線程安全。

6、什麼是內部類?內部類的作用

內部類指在類的內部再定義另一個類。

內部類的作用:1)實現多重繼承,因爲java中類的繼承只能單繼承,使用內部類可達到多重繼承;2)內部類可以很好的實現隱藏,一般非內部類,不允許有private或protected權限的,但內部類可以;3)減少了類文件編譯後產生的字節碼文件大小;

內部類在編譯完後也會產生.class文件,但文件名稱是:外部類名稱$內部類名稱.class。分爲以下幾種:

1)成員內部類,作爲外部類的一個成員存在,與外部類的屬性、方法並列,成員內部類持有外部類的引用,成員內部類不能定義static變量和方法。應用場合:每一個外部類都需要一個內部類實例,內部類離不開外部類存在。

2)靜態內部類,內部類以static聲明,其他類可通過外部類.內部類來訪問。特點:不會持有外部類的引用,可以訪問外部類的靜態變量,若要訪問成員變量須通過外部類的實例訪問。應用場合:內部類不需要外部類的實例,僅爲外部類提供或邏輯上屬於外部類,邏輯上可單獨存在。設計的意義:加強了類的封裝性(靜態內部類是外部類的子行爲或子屬性,兩者保持着一定關係),提高了代碼的可讀性(相關聯的代碼放在一起)。

3)匿名內部類,在整個操作中只使用一次,沒有名字,使用new創建,沒有具體位置。

4)局部內部類,在方法內或是代碼塊中定義類,

7、抽象類和接口區別

抽象類在類前面須用abstract關鍵字修飾,一般至少包含一個抽象方法,抽象方法指只有聲明,用關鍵字abstract修飾,沒有具體的實現的方法。因抽象類中含有無具體實現的方法,固不能用抽象類創建對象。當然如果只是用abstract修飾類而無具體實現,也是抽象類。抽象類也可以有成員變量和普通的成員方法。抽象方法必須爲public或protected(若爲private,不能被子類繼承,子類無法實現該方法)。若一個類繼承一個抽象類,則必須實現父類中所有的抽象方法,若子類沒有實現父類的抽象方法,則也應該定義爲抽象類。

接口用關鍵字interface修飾,接口也可以含有變量和方法,接口中的變量會被隱式指定爲public static final變量。方法會被隱式的指定爲public abstract,接口中的所有方法均不能有具體的實現,即接口中的方法都必須爲抽象方法。若一個非抽象類實現某個接口,必須實現該接口中所有的方法。

區別:1)抽象類可以提供成員方法實現的細節,而接口只能存在抽象方法;

2)抽象類的成員變量可以是各種類型,而接口中成員變量只能是public static final類型;

3)接口中不能含有靜態方法及靜態代碼塊,而抽象類可以有靜態方法和靜態代碼塊;

4)一個類只能繼承一個抽象類,用extends來繼承,卻可以實現多個接口,用implements來實現接口。

7.1、抽象類的意義

抽象類是用來提供子類的通用性,用來創建繼承層級裏子類的模板,減少代碼編寫,有利於代碼規範化。

7.2、抽象類與接口的應用場景

抽象類的應用場景:1)規範了一組公共的方法,與狀態無關,可以共享的,無需子類分別實現;而另一些方法卻需要各個子類根據自己特定狀態來實現特定功能;

2)定義一組接口,但不強迫每個實現類都必須實現所有的方法,可用抽象類定義一組方法體可以是空方法體,由子類選擇自己感興趣的方法來覆蓋;

7.3、抽象類是否可以沒有方法和屬性?

可以

7.4、接口的意義

1)有利於代碼的規範,對於大型項目,對一些接口進行定義,可以給開發人員一個清晰的指示,防止開發人員隨意命名和代碼混亂,影響開發效率。

2)有利於代碼維護和擴展,當前類不能滿足要求時,不需要重新設計類,只需要重新寫了個類實現對應的方法。

3)解耦作用,全局變量的定義,當發生需求變化時,只需改變接口中的值即可。

4)直接看接口,就可以清楚知道具體實現類間的關係,代碼交給別人看,別人也能立馬明白。

8、泛型中extends和super的區別

<? extends T>限定參數類型的上界,參數類型必須是T或T的子類型,但對於List<? extends T>,不能通過add()來加入元素,因爲不知道<? extends T>是T的哪一種子類;

<? super T>限定參數類型的下界,參數類型必須是T或T的父類型,不能能過get()獲取元素,因爲不知道哪個超類;

9、父類的靜態方法能否被子類重寫?靜態屬性和靜態方法是否可以被繼承?

父類的靜態方法和屬性不能被子類重寫,但子類可以繼承父類靜態方法和屬性,如父類和子類都有同名同參同返回值的靜態方法show(),聲明的實例Father father = new Son(); (Son extends Father),會調用father對象的靜態方法。靜態是指在編譯時就會分配內存且一直存在,跟對象實例無關。

10、進程和線程的區別

進程:具有一定獨立功能的程序,是系統進行資源分配和調度運行的基本單位。每一個Android應用可以理解爲一個進程,但一個應用也可以設置多個進程。

線程:進程的一個實體,是CPU調度的苯單位,也是進程中執行運算的最小單位,即執行處理機調度的基本單位,如果把進程理解爲邏輯上操作系統所完成的任務,線程則表示完成該任務的許多可能的子任務之一。Android中分主線程和子線程,UI更新只能在主線程中進行。

關係:一個進程可有多個線程,至少一個;一個線程只能屬於一個進程。同一進程的所有線程共享該進程的所有資源。不同進程的線程間要利用消息通信方式實現同步。

區別:進程有獨立的地址空間,而多個線程共享內存;進程具有一個獨立功能的程序,線程不能獨立運行,必須依存於應用程序中;

11、final,finally,finalize的區別

final:變量、類、方法的修飾符,被final修飾的類不能被繼承,變量或方法被final修飾則不能被修改和重寫。

finally:異常處理時提供finally塊來執行清除操作,不管有沒有異常拋出,此處代碼都會被執行。如果try語句塊中包含return語句,finally語句塊是在return之後運行;

finalize:Object類中定義的方法,若子類覆蓋了finalize()方法,在在垃圾收集器將對象從內存中清除前,會執行該方法,確定對象是否會被回收。

12、序列化Serializable 和Parcelable 的區別

序列化:將一個對象轉換成可存儲或可傳輸的狀態,序列化後的對象可以在網絡上傳輸,也可以存儲到本地,或實現跨進程傳輸;

爲什麼要進行序列化:開發過程中,我們需要將對象的引用傳給其他activity或fragment使用時,需要將這些對象放到一個Intent或Bundle中,再進行傳遞,而Intent或Bundle只能識別基本數據類型和被序列化的類型。

Serializable:表示將一個對象轉換成可存儲或可傳輸的狀態。

Parcelable:與Serializable實現的效果相同,也是將一個對象轉換成可傳輸的狀態,但它的實現原理是將一個完整的對象進行分解,分解後的每一部分都是Intent所支持的數據類型,這樣實現傳遞對象的功能。

Parcelable實現序列化的重要方法:序列化功能是由writeToParcel完成,通過Parcel中的write方法來完成;反序列化由CREATOR完成,內部標明瞭如何創建序列化對象及數級,通過Parcel的read方法完成;內容描述功能由describeContents方法完成,一般直接返回0。

區別:Serializable在序列化時會產生大量臨時變量,引起頻繁GC。Serializable本質上使用了反射,序列化過程慢。Parcelable不能將數據存儲在磁盤上,在外界變化時,它不能很好的保證數據的持續性。

選擇原則:若僅在內存中使用,如activity\service間傳遞對象,優先使用Parcelable,它性能高。若是持久化操作,優先使用Serializable

注意:靜態成員變量屬於類,不屬於對象,固不會參與序列化的過程;用transient關鍵字編輯的成員變量不會參與序列化過程;可以通過重寫writeObject()和readObject()方法來重寫系統默認的序列化和反序列化。

13、談談對kotlin的理解

特點:1)代碼量少且代碼末尾沒有分號;2)空類型安全(編譯期處理了各種null情況,避免執行時異常);3)函數式的,可使用lambda表達式;4)可擴展方法(可擴展任意類的的屬性);5)互操作性強,可以在一個項目中使用kotlin和java兩種語言混合開發;

14、string 轉換成 integer的方式及原理

1)parseInt(String s)內部調用parseInt(s, 10)默認爲10進制 。2)正常判斷null\進制範圍,length等。3)判斷第一個字符是否是符號位。4)循環遍歷確定每個字符的十進制值。5)通過*=和-=進行計算拼接。6)判斷是否爲負值返回結果。

(二) java深入源碼級的面試題(有難度)

1、哪些情況下的對象會被垃圾回收機制處理掉?

利用可達性分析算法,虛擬機會將一些對象定義爲GC Roots,從GC Roots出發沿着引用鏈向下尋找,如果某個對象不能通過GC Roots尋找到,虛擬機就認爲該對象可以被回收掉。

1.1 哪些對象可以被看做是GC Roots呢?

1)虛擬機棧(棧幀中的本地變量表)中引用的對象;

2)方法區中的類靜態屬性引用的對象,常量引用的對象;

3)本地方法棧中JNI(Native方法)引用的對象;

1.2 對象不可達,一定會被垃圾收集器回收麼?

即使不可達,對象也不一定會被垃圾收集器回收,1)先判斷對象是否有必要執行finalize()方法,對象必須重寫finalize()方法且沒有被運行過。2)若有必要執行,會把對象放到一個隊列中,JVM會開一個線程去回收它們,這是對象最後一次可以逃逸清理的機會。

2、講一下常見編碼方式?

編碼的意義:計算機中存儲的最小單元是一個字節即8bit,所能表示的字符範圍是255個,而人類要表示的符號太多,無法用一個字節來完全表示,固需要將符號編碼,將各種語言翻譯成計算機能懂的語言。

1)ASCII碼:總共128個,用一個字節的低7位表示,0〜31控制字符如換回車刪除等;32~126是打印字符,可通過鍵盤輸入並顯示出來;

2)ISO-8859-1,用來擴展ASCII編碼,256個字符,涵蓋了大多數西歐語言字符。

3)GB2312:雙字節編碼,總編碼範圍是A1-A7,A1-A9是符號區,包含682個字符,B0-B7是漢字區,包含6763個漢字;

4)GBK爲了擴展GB2312,加入了更多的漢字,編碼範圍是8140~FEFE,有23940個碼位,能表示21003個漢字。

5)UTF-16: ISO試圖想創建一個全新的超語言字典,世界上所有語言都可通過這本字典Unicode來相互翻譯,而UTF-16定義了Unicode字符在計算機中存取方法,用兩個字節來表示Unicode轉化格式。不論什麼字符都可用兩字節表示,即16bit,固叫UTF-16。

6)UTF-8:UTF-16統一採用兩字節表示一個字符,但有些字符只用一個字節就可表示,浪費存儲空間,而UTF-8採用一種變長技術,每個編碼區域有不同的字碼長度。  不同類型的字符可以由1~6個字節組成。                                                                                                                                                                                                                       

3、utf-8編碼中的中文佔幾個字節;int型幾個字節?

utf-8是一種變長編碼技術,utf-8編碼中的中文佔用的字節不確定,可能2個、3個、4個,int型佔4個字節。

4、靜態代理和動態代理的區別,什麼場景使用?

代理是一種常用的設計模式,目的是:爲其他對象提供一個代理以控制對某個對象的訪問,將兩個類的關係解耦。代理類和委託類都要實現相同的接口,因爲代理真正調用的是委託類的方法。

區別:1)靜態代理:由程序員創建或是由特定工具生成,在代碼編譯時就確定了被代理的類是哪一個是靜態代理。靜態代理通常只代理一個類;

2)動態代理:在代碼運行期間,運用反射機制動態創建生成。動態代理代理的是一個接口下的多個實現類;

實現步驟:a.實現InvocationHandler接口創建自己的調用處理器;b.給Proxy類提供ClassLoader和代理接口類型數組創建動態代理類;c.利用反射機制得到動態代理類的構造函數;d.利用動態代理類的構造函數創建動態代理類對象;

使用場景:Retrofit中直接調用接口的方法;Spring的AOP機制;

5、Java的異常體系

Java中Throwable是所有異常和錯誤的超類,兩個直接子類是Error(錯誤)和Exception(異常):

1)Error是程序無法處理的錯誤,由JVM產生和拋出,如OOM、ThreadDeath等。這些異常發生時,JVM一般會選擇終止程序。

2)Exception是程序本身可以處理的異常,又分爲運行時異常(RuntimeException)(也叫Checked Eception)和非運行時異常(不檢查異常Unchecked Exception)。運行時異常有NullPointerException\IndexOutOfBoundsException等,這些異常一般是由程序邏輯錯誤引起的,應儘可能避免。非運行時異常有IOException\SQLException\FileNotFoundException以及由用戶自定義的Exception異常等。

6、談談你對解析與分派的認識。

解析指方法在運行前,即編譯期間就可知的,有一個確定的版本,運行期間也不會改變。解析是靜態的,在類加載的解析階段就可將符號引用轉變成直接引用。

分派可分爲靜態分派和動態分派,重載屬於靜態分派,覆蓋屬於動態分派。靜態分派是指在重載時通過參數的靜態類型而非實際類型作爲判斷依據,在編譯階段,編譯器可根據參數的靜態類型決定使用哪一個重載版本。動態分派則需要根據實際類型來調用相應的方法。

7、修改對象A的equals方法的簽名,那麼使用HashMap存放這個對象實例的時候,會調用哪個equals方法?

會調用對象的equals方法,如果對象的equals方法沒有被重寫,equals方法和==都是比較棧內局部變量表中指向堆內存地址值是否相等。

8、Java中實現多態的機制是什麼?

多態是指程序中定義的引用變量所指向的具體類型和通過該引用變量發出的方法調用在編譯時不確定,在運行期間才確定,一個引用變量到底會指向哪個類的實例。這樣就可以不用修改源程序,就可以讓引用變量綁定到各種不同的類實現上。Java實現多態有三個必要條件:繼承、重定、向上轉型,在多態中需要將子類的引用賦值給父類對象,只有這樣該引用才能夠具備調用父類方法和子類的方法。

9、如何將一個Java對象序列化到文件裏?

ObjectOutputStream.writeObject()負責將指定的流寫入,ObjectInputStream.readObject()從指定流讀取序列化數據。

//寫入
try {
    ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("D:/student.txt"));
    os.writeObject(studentList);
    os.close();
} catch(FileNotFoundException e) {
    e.printStackTrace();
} catch(IOException e) {
    e.printStackTrace();
}

10、說說你對Java反射的理解

在運行狀態中,對任意一個類,都能知道這個類的所有屬性和方法,對任意一個對象,都能調用它的任意一個方法和屬性。這種能動態獲取信息及動態調用對象方法的功能稱爲java語言的反射機制。

反射的作用:開發過程中,經常會遇到某個類的某個成員變量、方法或屬性是私有的,或只對系統應用開放,這裏就可以利用java的反射機制通過反射來獲取所需的私有成員或是方法。

1) 獲取類的Class對象實例 Class clz = Class.forName("com.zhenai.api.Apple");

2) 根據Class對象實例獲取Constructor對象  Constructor appConstructor = clz.getConstructor();

3) 使用Constructor對象的newInstance方法獲取反射類對象 Object appleObj = appConstructor.newInstance();

4) 獲取方法的Method對象  Method setPriceMethod = clz.getMethod("setPrice", int.class);

5) 利用invoke方法調用方法  setPriceMethod.invoke(appleObj, 14);

6) 通過getFields()可以獲取Class類的屬性,但無法獲取私有屬性,而getDeclaredFields()可以獲取到包括私有屬性在內的所有屬性。帶有Declared修飾的方法可以反射到私有的方法,沒有Declared修飾的只能用來反射公有的方法,其他如Annotation\Field\Constructor也是如此。

11、說說你對Java註解的理解

註解是通過@interface關鍵字來進行定義的,形式和接口差不多,只是前面多了一個@

public @interface TestAnnotation {

}

使用時@TestAnnotation來引用,要使註解能正常工作,還需要使用元註解,它是可以註解到註解上的註解。元標籤有@Retention @Documented @Target @Inherited @Repeatable五種

@Retention說明註解的存活時間,取值有RetentionPolicy.SOURCE 註解只在源碼階段保留,在編譯器進行編譯時被丟棄;RetentionPolicy.CLASS 註解只保留到編譯進行的時候,並不會被加載到JVM中。RetentionPolicy.RUNTIME可以留到程序運行的時候,它會被加載進入到JVM中,所以在程序運行時可以獲取到它們。

@Documented 註解中的元素包含到javadoc中去

@Target  限定註解的應用場景,ElementType.FIELD給屬性進行註解;ElementType.LOCAL_VARIABLE可以給局部變量進行註解;ElementType.METHOD可以給方法進行註解;ElementType.PACKAGE可以給一個包進行註解 ElementType.TYPE可以給一個類型進行註解,如類、接口、枚舉

@Inherited 若一個超類被@Inherited註解過的註解進行註解,它的子類沒有被任何註解應用的話,該子類就可繼承超類的註解;

註解的作用:

1)提供信息給編譯器:編譯器可利用註解來探測錯誤和警告信息

2)編譯階段:軟件工具可以利用註解信息來生成代碼、html文檔或做其它相應處理;

3)運行階段:程序運行時可利用註解提取代碼

註解是通過反射獲取的,可以通過Class對象的isAnnotationPresent()方法判斷它是否應用了某個註解,再通過getAnnotation()方法獲取Annotation對象

12、說一下泛型原理,並舉例說明

泛型就是將類型變成參數傳入,使得可以使用的類型多樣化,從而實現解耦。Java泛型是在Java1.5以後出現的,爲保持對以前版本的兼容,使用了擦除的方法實現泛型。擦除是指在一定程度無視類型參數T,直接從T所在的類開始向上T的父類去擦除,如調用泛型方法,傳入類型參數T進入方法內部,若沒在聲明時做類似public T methodName(T extends Father t){},Java就進行了向上類型的擦除,直接把參數t當做Object類來處理,而不是傳進去的T。即在有泛型的任何類和方法內部,它都無法知道自己的泛型參數,擦除和轉型都是在邊界上發生,即傳進去的參在進入類或方法時被擦除掉,但傳出來的時候又被轉成了我們設置的T。在泛型類或方法內,任何涉及到具體類型(即擦除後的類型的子類)操作都不能進行,如new T(),或者T.play()(play爲某子類的方法而不是擦除後的類的方法)

13、Java中String的瞭解

1)String類是final型,固String類不能被繼承,它的成員方法也都默認爲final方法。String對象一旦創建就固定不變了,對String對象的任何改變都不影響到原對象,相關的任何改變操作都會生成新的String對象。

2)String類是通過char數組來保存字符串的,String對equals方法進行了重定,比較的是值相等。

String a = "test"; String b = "test"; String c = new String("test");

a、b和字面上的test都是指向JVM字符串常量池中的"test"對象,他們指向同一個對象。而new關鍵字一定會產生一個對象test,該對象存儲在堆中。所以new String("test")產生了兩個對象,保存在棧中的c和保存在堆中的test。而在java中根本就不存在兩個完全一模一樣的字符串對象,故在堆中的test應該是引用字符串常量池中的test。

例:

String str1 = "abc"; //棧中開闢一塊空間存放引用str1,str1指向池中String常量"abc"
String str2 = "def"; //棧中開闢一塊空間存放引用str2,str2指向池中String常量"def"
String str3 = str1 + str2;//棧中開闢一塊空間存放引用str3
//str1+str2通過StringBuilder的最後一步toString()方法返回一個新的String對象"abcdef"
//會在堆中開闢一塊空間存放此對象,引用str3指向堆中的(str1+str2)所返回的新String對象。
System.out.println(str3 == "abcdef");//返回false
因爲str3指向堆中的"abcdef"對象,而"abcdef"是字符池中的對象,所以結果爲false。JVM對String str="abc"對象放在常量池是在編譯時做的,而String str3=str1+str2是在運行時才知道的,new對象也是在運行時才做的。

14、String爲什麼要設計成不可變的?

1)字符串常量池需要String不可變。因爲String設計成不可變,當創建一個String對象時,若此字符串值已經存在於常量池中,則不會創建一個新的對象,而是引用已經存在的對象。如果字符串變量允許必變,會導致各種邏輯錯誤,如改變一個對象會影響到另一個獨立對象。

2)String對象可以緩存hashCode。字符串的不可變性保證了hash碼的唯一性,因此可以緩存String的hashCode,這樣不用每次去重新計算哈希碼。在進行字符串比較時,可以直接比較hashCode,提高了比較性能;

3)安全性。String被許多java類用來當作參數,如url地址,文件path路徑,反射機制所需的Strign參數等,若String可變,將會引起各種安全隱患。

(三) 數據結構

1、常用數據結構簡介

數據結構是指相互之間存在着一種或多種關係的數據元素的集合和該集合中數據元素間的關係組成。常用的數據有:數組、棧、隊列、鏈表、樹、圖、堆、散列表。

1)數組:在內存中連續存儲多個元素的結構。數組元素通過下標訪問,下標從0開始。優點:訪問速度快;缺點:數組大小固定後無法擴容,只能存儲一種類型的數據,添加刪除操作慢。適用場景:適用於需頻繁查找,對存儲空間要求不高,很少添加刪除。

2)棧:一種特殊的線性表,只可以在棧頂操作,先進後出,從棧頂放入元素叫入棧,從棧頂取出元素叫出棧。應用場景:用於實現遞歸功能,如斐波那契數列。

3)隊列:一種線性表,在列表一端添加元素,另一端取出,先進先出。使用場景:多線程阻塞隊列管理中。

4)鏈表:物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表的指針地址實現,每個元素包含兩個結點,一個是存儲元素的數據域,一個是指向下一個結點地址的指針域。有單鏈表、雙向鏈表、循環鏈表。優點:可以任意加減元素,不需要初始化容量,添加刪除元素只需改變前後兩個元素結點的指針域即可。缺點:因爲含有大量指針域,固佔用空間大,查找耗時。適用場景:數據量小,需頻繁增加刪除操作。

5)樹:由n個有限節點組成一種具有層次關係的集合。二叉樹(每個結點最多有兩個子樹,結點的度最大爲2,左子樹和右子樹有順序)、紅黑樹(HashMap底層源碼)、B+樹(mysql的數據庫索引結構)

6)散列表(哈希表):根據鍵值對來存儲訪問。

7)堆:堆中某個節點的值總是不大於或不小於其父節點的值,堆總是一棵完全二叉樹。

8)圖:由結點的有窮集合V和邊的集合E組成。

2、併發集合瞭解哪些?

1)併發List,包括Vector和CopyOnWriteArrayList是兩個線程安全的List,Vector讀寫操作都用了同步,CopyOnWriteArrayList在寫的時候會複製一個副本,對副本寫,寫完用副本替換原值,讀時不需要同步。

2)併發Set,CopyOnWriteArraySet基於CopyOnWriteArrayList來實現的,不允許存在重複的對象。

3)併發Map,ConcurrentHashMap,內部實現了鎖分離,get操作是無鎖的。

4)併發Queue,ConcurrentLinkedQueue適用於高併發場景下的隊列,通過無鎖方式實現。 BlockingQueue阻塞隊列,應用場景,生產者-消費者模式,若生產快於消費,生產隊列裝滿時會阻塞,等待消費。

5)併發Deque, LinkedBlockingDueue沒有進行讀寫鎖分離,同一時間只能有一個線程對其操作。

6)併發鎖重入鎖ReentrantLock,互斥鎖,一次最多隻能一個線程拿到鎖。

7)讀寫鎖ReadWriteLock,有讀取和寫入鎖兩種,讀取允許多個讀取線程同時持有,而寫入只能有一個線程持有。

3、列舉java的集合以及集合之間的繼承關係

 

5、容器類介紹以及之間的區別

1)Collection接口:集合框架的根接口,它是集合類框架中最具一般性的頂層接口。

2)Map接口:提供了鍵值對的映射關係的集合,關鍵字不能有重複值,每個關鍵字至多可映射一個值。HashMap(通過散列機制,用於快速訪問),TreeMap(保持key處於排序狀態,訪問速度不如hashmap), LinkedHashMap(保持元素的插入順序)

3)Set接口:可包含重複的元素,LinkedHashSet TreeSet(用紅黑樹來存儲元素) HashSet

4)List接口:可通過索引對元素進行精準的插入和查找,實現類有ArrayList LinkedList

5)Queue接口:繼承自Collection接口,LinkedList實現了Queue接口,提供了支持隊列的行爲。

6)Iterator接口:爲了迭代集合

7)Comparable接口:用於比較

6、List,Set,Map的區別

Set是一個無序的集合,不能包含重複的元素;

list是一個有序的集合可以包含重複的元素,提供了按索引訪問的方式;

map包含了key-value對,map中key必須唯一,value可以重複。

7、HashMap的實現原理

1)數據結構

jdk1.7及以前,HashMap由數組+鏈表組成,數組Entry是HashMap的主體,Entry是HashMap中的一個靜態內部類,每一個Entry包含一個key-value鍵值對,鏈表是爲解決哈希衝突而存在。

從jdk1.8起,HashMap是由數組+鏈表/紅黑樹組成,當某個bucket位置的鏈表長度達到閥值8時,這個鏈表就轉變成紅黑樹。

2)HashMap是線程不安全的,存儲比較快,能接受null值,HashMap通過put(key, value)來儲存元素,通過get(key)來得到value值,通過hash算法來計算hashcode值,用hashcode標識Entry在bucket中存儲的位置。

3)HashMap中爲什麼要使用加載因子,爲什麼要進行擴容

加載因子是指當HashMap中存儲的元素/最大空間值的閥值,如果超過這個值,就會進行擴容。加載因子是爲了讓空間得到充分利用,如果加載因子太大,雖對空間利用更充分,但查找效率會降低;如果加載因子太小,表中的數據過於稀疏,很多空間還沒用就開始擴容,就會對空間造成浪費。

至於爲什麼要擴容,如果不擴容,HashMap中數組處的鏈表會越來越長,這樣查找效率就會大大降低。

7.1 HashMap如何put數據(從HashMap源碼角度講解)?

當我們使用put(key, value)存儲對象到HashMap中時,具體實現步驟如下:

1)先判斷table數組是否爲空,爲空以默認大小構建table,table默認空間大小爲16

2)計算key的hash值,並計算hash&(n-1)值得到在數組中的位置index,如果該位置沒值即table[index]爲空,則直接將該鍵值對存放在table[index]處。

3)如果table[index]處不爲空,說明發生了hash衝突,判斷table[index]處結點是否是TreeNode(紅黑樹結點)類型數據,如果是則執行putTreeVal方法,按紅黑樹規則將鍵值對存入;

4)如果table[index]是鏈表形式,遍歷該鏈表上的數據,將該鍵值對放在table[index]處,並將其指向原index處的鏈表。判斷鏈表上的結點數是否大於鏈表最大結點限制(默認爲8),如果超過了需執行treeifyBin()操作,則要將該鏈表轉換成紅黑樹結構。

5)判斷HashMap中數據個數是否超過了(最大容量*裝載因子),如果超過了,還需要對其進行擴容操作。

7.2 HashMap如何get數據?

get(key)方法獲取key的hash值,計算hash&(n-1)得到在鏈表數組中的位置first=table[hash&(n-1)],先判斷first(即數組中的那個)的key是否與參數key相等,不等的話,判斷結點是否是TreeNode類型,是則調用getTreeNode(hash, key)從二叉樹中查找結點,不是TreeNode類型說明還是鏈表型,就遍歷鏈表找到相同的key值返回對應的value值即可。

7.3 當兩個對象的hashcode相同,即發生碰撞時,HashMap如何處理

當兩個對象的hashcode相同,它們的bucket位置相同,hashMap會用鏈表或是紅黑樹來存儲對象。Entry類裏有一個next屬性,作用是指向下一個Entry。第一個鍵值對A進來,通過計算其key的hash得到index,記做Entry[index]=A。一會又進來一個鍵值對B,通過計算其key的hash也是index,HashMap會將B.next=A, Entry[index]=B.如果又進來C,其key的hash也是index,會將C.next=B, Entry[index]=C.這樣bucket爲index的地方存放了A\B\C三個鍵值對,它們能過next屬性鏈在一起。數組中存儲的是最後插入的元素,其他元素都在後面的鏈表裏。

7.4 如果兩個鍵的hashcode相同,如何獲取值對象?

當調用get方法時,hashmap會使用鍵對象的hashcode找到bucket位置,找到bucket位置後,會調用key.equals()方法去找到鏈表中正確的節點,最終找到值對象。

7.5 hashMap如何擴容

HashMap默認負載因爲是0.75,當一個map填滿了75%的bucket時,和其他集合類一樣,將會創建原來HashMap大小兩倍的bucket數組,來重新調整HashMap的大小,並將原來的對象放入新的bucket數組中。

在jdk1.7及以前,多線程擴容可能出現死循環。因爲在調整大小過程中,存儲在某個bucket位置中的鏈表元素次序會反過來,而多線程情況下可能某個線程翻轉完鏈表,另外一個線程又開始翻轉,條件競爭發生了,那麼就死循環了。

而在jdk1.8中,會將原來鏈表結構保存至節點e中,將原來數組中的位置設爲null,然後依次遍歷e,根據hash&n是否爲0分成兩條支鏈,保存在新數組中。如果多線程情況可能會取到null值造成數據丟失。

8、ConcurrentHashMap的實現原理

1)jdk1.7及以前:一個ConcurrentHashMap由一個segment數組和多個HashEntry組成,每一個segment都包含一個HashEntry數組, Segment繼承ReentrantLock用來充當鎖角色,每一個segment包含了對自己的HashEntry的操作,如get\put\replace操作,這些操作發生時,對自己的HashEntry進行鎖定。由於每一個segment寫操作只鎖定自己的HashEntry,可以存在多個線程同時寫的情況。

jdk1.8以後:ConcurrentHashMap取消了segments字段,採用transient volatile HashEntry<K, V> table保存數據,採用table數組元素作爲鎖,實現對每一個數組數據進行加鎖,進一小減少併發衝突概率。ConcurrentHashMap是用Node數組+鏈表+紅黑樹數據結構來實現的,併發制定用synchronized和CAS操作。

2)Segment實現了ReentrantLock重入鎖,當執行put操作,會進行第一次key的hash來定位Segment的位置,若該Segment還沒有初始化,會通過CAS操作進行賦值,再進行第二次hash操作,找到相應的HashEntry位置。

9、ArrayMap和HashMap的對比

1)存儲方式不一樣,HashMap內部有一個Node<K,V>[]對象,每個鍵值對都會存儲到這個對象裏,當用put方法添加鍵值對時,會new一個Node對象,tab[i] = newNode(hash, key, value, next);

ArrayMap存儲則是由兩個數組來維護,int[] mHashes; Object[] mArray; mHashes數組中保存的是每一項的HashCode值,mArray存的是鍵值對,每兩個元素代表一個鍵值對,前面保存key,後面保存value。mHashes[index]=hash; mArray[index<<1]=key; mArray[(index<<1)+1]=value;

ArrayMap相對於HashMap,無需爲每個鍵值對創建Node對象,且在數組中連續存放,更省空間。

2)添加數據時擴容處理不一樣,進行了new操作,重新創建對象,開銷很大;而ArrayMap用的是copy數據,所有效率相對高些;

3)ArrayMap提供了數組收縮功能,在clear或remove後,會重新收縮數組,釋放空間;

4)ArrayMap採用二分法查找,mHashes中的hash值是按照從小到大的順序連續存放的,通過二分查找來獲取對應hash下標index,去mArray中查找鍵值對。mHashes中的index*2是mArray中的key下標,index*2+1爲value的下標,由於存在hash碰撞情況,二分查找到的下標可能是多個連續相同的hash值中的任意一個,此時需要用equals比對命中的key對象是否相等,不相等,應當從當前index先向後再向前遍歷所有相同hash值。

5)sparseArray比ArrayMap進一步優化空間,SparseArray專門對基本類型做了優化,Key只能是可排序的基本類型,如int\long,對value,除了泛型Value,還對每種基本類型有單獨實現,如SparseBooleanArray\SparseLongArray等。無需包裝,直接使用基本類型值,無需hash,直接使用基本類型值索引和判斷相等,無碰撞,無需調用hashCode方法,無需equals比較。SparseArray延遲刪除。

10、HashTable實現原理

Hashtable中的無參構造方法Hashtable()中調用了this(11, 0.75f),說明它默認容量是11,加載因子是0.75,在構造方法上會new HashtableEntry<?, ?>[initialCapacity]; 會新建一個容量是初始容量的HashtableEntry數組。HashtableEntry數組中包含hash\Key\Value\next變量,鏈表形式,重寫了hashCode和equals方法。Hashtable所有public方法都在方法體上加上了synchronized鎖操作,說明它是線程安全的。它還實現了Serializable接口中的writeObject和readObject方法,分別實現了逐行讀取和寫入的功能,並且加了synchronized鎖操作。

(1) put(Key, Value)方法

1)先判斷value是否爲空,爲空拋出空指針異常;

2)根據key的hashCode()值,計算table表中的位置索引(hash&0x7FFFFFFF)%tab.length值index,如果該索引處有值,再判斷該索引處鏈表中是否包含相同的key,如果key值相同則替換舊值。

3)如果沒有相同的key值,調用addEntry方法,在addEntry中判斷count大小是否超過了最大容量限制,如果超過了需要重新rehash(),容量變成原來容量*2+1,將原表中的值都重新計算hash值放入新表中。再構造一個HashtableEntry對象放入相應的table表頭,如果原索引處有值,則將table[index].next指向原索引處的鏈表。

(2)get方法

根所key.hashCode(),計算它在table表中的位置,(hash&0x7FFFFFFF)%tab.length,遍歷該索引處表的位置中是否有值,是否存在鏈表,再判斷是key值和hash值是否相等,相等則返回對應的value值。

11、HashMap和HashTable的區別

1)Hashtable是個線程安全的類,在對外方法都添加了synchronized方法,序列化方法上也添加了synchronized同步鎖方法,而HashMap非線程安全。這也導致Hashtable的讀寫等操作比HashMap慢。

2)Hashtable不允許值和鍵爲空,若爲空會拋出空指針。而HashMap允許鍵和值爲空;

3)Hashtable根據key值的hashCode計算索引,(hash&0x7FFFFFFF)%tab.length,保證hash值始終爲正數且不超過表的長度。而HashMap中計算索引值是通過hash(key)&(tab.length-1),是通過與操作,計算出在表中的位置會比Hashtable快。

4)Hashtable容量能爲任意大於等於1的正數,而HashMap的容量必須爲2^n,Hashtable默認容量爲11,HashMap初始容量爲16

5)Hashtable每次擴容,新容量爲舊容量的2倍+1,而HashMap爲舊容量的2倍。

12、HashMap與HashSet的區別

HashSet底層實現是HashMap,內部包含一個HashMap<E, Ojbect> map變量

private transient HashMap<E,Object> map;

一個Object PRESENT變量(當成插入map中的value值)

private static final Object PRESENT = new Object();

HashSet中元素都存到HashMap鍵值對的Key上面。具體可以查看HashSet的add方法,直接調用了HashMap的put方法,將值作爲HashMap的鍵,值用一個固定的PRESENT值。

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

HashSet沒有單獨的get方法,用的是HashMap的。HashSet實現了Set接口,不允許集合中出現重複元素,將對象存儲進HashSet前,要先確保對象重寫了hashCode()和equals方法,以保證放入set對象是唯一的。

13、HashSet與HashMap怎麼判斷集合元素重複?

HashMap在放入key-value鍵值對是,先通過key計算其hashCode()值,再與tab.length-1做與操作,確定下標index處是否有值,如果有值,再調用key對象的equals方法,對象不同則插入到表頭,相同則覆蓋;

HashSet是將數據存放到HashMap的key中,HashMap是key-value形式的數據結構,它的key是唯一的,HashSet利用此原理保證放入的對象唯一性。

14、集合Set實現Hash怎麼防止碰撞

HashSet底層實現是HashMap,HashMap如果兩個不同Key對象的hashCode()值相等,會用鏈表存儲,HashSet也一樣。

15、ArrayList和LinkedList的區別,以及應用場景

ArrayList底層是用數組實現的,隨着元素添加,其大小是動態增大的;在內存中是連續存放的;如果在集合末尾添加或刪除元素,所用時間是一致的,如果在列表中間添加或刪除元素,所用時間會大大增加。通過索引查找元素速度很快。適合場合:查詢比較多的場景

LinkedList底層是通過雙向鏈表實現的,LinkedList和ArrayList相比,增刪速度快,但查詢和修改值速度慢。在內存中不是連續內存。場景:增刪操作比較多的場景。

16、二叉樹的深度優先遍歷和廣度優先遍歷的具體實現

public class Tree {
    Tree left, right;
    int data;
    public Tree(int data) {
        this.data = data;
    }

}
//深度優先遍歷(其實和前序遍歷實現一樣)
public void queryByDeepth(Tree root) {
    if(root != null) {
        print(root.data);
    }
    if(root.left != null) queryByDeepth(root.left);
    if(root.right != null) queryByDeepth(root.right);
}

//廣度優先遍歷(用隊列輔助實現)
public void queryByDeepth(Tree root) {
    if(root == null) return;
    Queue<Tree> queue = new LinkedList<Tree>();
    queue.offer(root);
    while(root != null || !queue.isEmpty()) {
        root = queue.poll();
        print(root.data);
        if(root.left != null) queue.offer(root.left);
        if(root.right != null) queue.offer(root.right);
    }
    
}

17、堆的結構

java的堆用於存放對象實例的,通過new一個對象實例,在堆上給實例分配內存空間。而數據結構中堆是一種樹,實現了優先隊列的插入和刪除的時間複雜度都爲o(logn),常用數組來表示。

18、堆和樹的區別

堆是一棵除最底層外被完全填滿的二叉樹,底層上的元素從左到右填入,即完全二叉樹,它可用一個數組表示,對於數組任一個位置上的i個元素,左兒子位置爲2*i+1,右兒子在2*i+2上,父親則在位置i/2上。

樹又分二叉樹(每個節點不能有多於兩個的子節點)、二叉查找樹(在二叉樹基礎上,對樹中每個節點X,左子樹中所有項的值小於X中的項,右子樹中所有項的值大於X中的項)、AVL樹(深度必須是O(logN),平衡二叉查找樹的每個節點的左子樹和右子樹最多差1的二叉查找樹,對於插入後破壞平衡性,通過旋轉調節)、伸展樹。

19、堆和棧在內存中的區別是什麼(解答提示:可以從數據結構方面以及實際實現方面兩個方面去回答)?

(1)數據結構方面:堆是一種二叉樹;

棧是一種後進先出的存儲結構,有壓棧和出棧兩種操作。

(2)內存方面:

棧內存:靜態變量、局部變量是以壓棧出棧的方式分配內存,系統會在一個代碼段中分配和回收局部變量,實際上每個代碼段、函數都是一個或多個嵌套的棧,不需要手動管理棧區內存。

堆內存:在Java運行時被用爲對象分配內存,GC在堆內存上釋放沒有任何引用的對象所佔的內存,任何在堆上被創建的對象都有一個全局的訪問。

20、什麼是深拷貝和淺拷貝

1)深拷貝:不僅要複製對象的所有基本數據類型的成員變量值,還要爲所有引用數據類型的成員變量申請存儲空間,複製每個引用數據類型成員變量所引用的對象,直到該對象可達的所有對象。即對象進行深拷貝要對整個對象圖進行拷貝。

實現方式:a)重寫clone方法,要爲對象圖的每一層的每個對象都實現Cloneable接口並重寫clone方法。b)將對象序列化爲字節序列後,默認會將該對象整個對象圖進行序列化,再通過反序列即可完美地實現深拷貝。

2)淺拷貝:對於基本數據類型,直接進行值傳遞,即直接複製一個值給新對象,對一個對象的值改變不會影響到拷貝的數據;對於引用數據類型的成員變量,如成員變量是某個數組或類對象,它會進行引用傳遞,只是將該成員變量的引用值(地址)複製一份給新對象,因爲兩個對象都指向同一個實例,故改變該成員變量會影響到另一個對象的該成員變量值。

實現方式:a)通過拷貝構造方法實現,指該類的構造方法的參數爲該類的對象。b)重寫clone方法,使用clone方法的類必須實現Cloneable接口。

21、手寫鏈表逆序代碼

//假設LinkedList的結點是Node
//1、遍歷法
public Node reverseLinkedList(LinkedList head) {
    if(head == null || head.next == null) return head;
    Node pre = null, next = null;
    while(head != null) {
        next = head.next;
        next.next = pre;
        pre = head;
        head = next;
    }
    return pre;
}
//2、遞歸法
public Node reverseLinkedList(LinkedList head) {
    if(head == null || head.next == null) return head;
    Node next = head.next;
    Node newNode = reverseLinkedList(next);
    next.next = head;
    head.next = null;
    return newNode;
}

22、講一下對樹,B+樹的理解

具體參考這篇文章吧:B+樹

B樹一個節點可以擁有多於2個子節點的二叉查找樹,每個節點有M-1個key,且以升序排列,位於M-1和M key的子節點的值位於M-1和M key對應的value之間,其它節點至少有M/2個子節點,普遍運用在數據庫和文件系統中。

B tree

 

23、講一下對圖的理解

圖就是一些頂點的集合,這些頂點通過一系列邊結對,頂點用圓圈表示,邊就是這些圓圈之間的連線,頂點之間通過邊連接。

它的物理存儲結構有:

1)鄰接矩陣:用兩個數組存儲圖的信息,1個以數組存儲頂點,一個二維數組存儲邊的信息。對於頂點多而邊數少的稀疏圖造成存儲空間大量浪費。

2)鄰接表:數組+鏈表,用數組存儲每個節點,數組中每個節點的所有鄰接點組成一個鏈表,鄰接表關心了出度,但查找入度就要遍歷整個圖。

遍歷圖:從圖中某一個頂點出發遍歷途中其餘頂點,每一個頂點僅被訪問一次。

1)深度優先遍歷

int[] visited = new int[g.vnum];
public void dfsVisitGraph(Graph g, int i) {
    visited[i] = 1;
    print(g.vex[i]);
    for(int j = 0; j < g.vnum; j++) {
        if(g.arc[i][j] != 0 && g.arc[i][j] != IUNFINITY && !visited[j]) {
            dfsVisitGraph(g, j);
        }
    }
}
public void dfsTraverse(Graph g) {
    for(int i = 0; i < g.vnum; i++) {
        if(!visited[i]) {
            dfsVisitGraph(g, i);
        }
    }
}
        

2)廣度優先遍歷(隊列)

public void bfsTraverse(Graph g) {
    Queue<Integer> queue = new LinkedList<>();
    int[] visited = new int[g.vnum];
    for(int i = 0; i < g.vnum; i++) {
        if(visited[i] == 0) {
            visited[i] = 1;
            queue.offer(g.vexs[i]);   
            print(g.vexs[i]);
            while(!queue.isEmpty()) {
                int link = queue.poll();
                for(int j = 0; j < g.vnum; j++) {
                    if(g.arc[link][j] == 1 && visited[j] == 0) {
                        visited[j] = 1;
                        print(g.vexs[j];
                        queue.offer(g.vexs[i]);   
                    }
                }
            }
        }
    }
}

24、判斷單鏈表成環與否?

public boolean linkHasCircle(LinkedList node) {
    if(node == null || node.next == null) return false;
    Node first = node, second = node;//first慢指針一次走一步;second快指針一次走兩步
    while(second.next != null && second.next.next != null) {
        first = node.next;
        second = node.next.next;
        if(first == second) {
            return true;
        }
    }
    return false;
}

25、合併多個單有序鏈表(假設都是遞增的)

這個主要遍歷鏈表,比較值大小,如果需要返回鏈表頭節點,則需要先把頭結點保存好

public Node merge(LinkedList node1, LinkedList node2) {
    if(node1 == null) return node2;
    if(node2 == null) return node1;
    if(node1 == null && node2 == null) return null;
    int data1 = node1.data;
    int data2 = node2.data;
    Node newNode, head;
    int data = data2;
    if(data1 <= data2) {
        data = data1;
        node1 = node1.next;
    } else {
        node2 = node2.next;
    }
    newNode = new Node(data);
    head = newNode;

    while(node1 != null && node2 != null) {
        if(node1.data <= node2.data) {
            newNode.next.data = node1.data;
            node1 = node1.next;
        } else {
            newNode.next.data = node2.data;
            node2 = node2.next;
        }
        newNode = newNode.next;
    }
    while(node1 != null) {
            newNode.next.data = node1.data;
            node1 = node1.next;
            newNode = newNode.next;
    }

    while(node2 != null) {
            newNode.next.data = node2.data;
            node2 = node2.next;
            newNode = newNode.next;
    }
    return head;
}
            

(四) 線程、多線程和線程池

1、開啓線程的三種方式?

1)繼承Thread類,重寫run()方法,在run()方法體中編寫要完成的任務 new Thread().start();

2)實現Runnable接口,實現run()方法 new Thread(new MyRunnable()).start();

3)實現Callable接口MyCallable類,實現call()方法,使用FutureTask類來包裝Callable對象,使用FutureTask對象作爲Thread對象的target創建並啓動線程;調用FutureTask對象的get()方法來獲得子線程執行結束後的返回值。

FutureTask<Integer> ft = new FutureTask<Integer>(new MyCallable());

new Thread(ft).start();

2、run()和start()方法區別

run()方法只是線程的主體方法,和普通方法一樣,不會創建新的線程。只有調用start()方法,纔會啓動一個新的線程,新線程纔會調用run()方法,線程纔會開始執行。

3、如何控制某個方法允許併發訪問線程的個數?

創建Semaphore變量,Semaphore semaphore = new Semaphore(5, true); 當方法進入時,請求一個信號,如果信號被用完則等待,方法運行完,釋放一個信號,釋放的信號新的線程就可以使用。

4、在Java中wait和seelp方法的不同

wait()方法屬於Object類,調用該方法時,線程會放棄對象鎖,只有該對象調用notify()方法後本線程才進入對象鎖定池準備獲取對象鎖進入運行狀態。

sleep()方法屬於Thread類,sleep()導致程序暫停執行指定的時間,讓出CPU,但它的監控狀態依然保存着,當指定時間到了又會回到運行狀態,sleep()方法中線程不會釋放對象鎖。

5、談談wait/notify關鍵字的理解

notify: 喚醒在此對象監視器上等待的單個線程

notifyAll(): 通知所有等待該競爭資源的線程

wait: 釋放obj的鎖,導致當前的線程等待,直接其他線程調用此對象的notify()或notifyAll()方法

當要調用wait()或notify()/notifyAll()方法時,一定要對競爭資源進行加鎖,一般放到synchronized(obj)代碼中。當調用obj.notify/notifyAll後,調用線程依舊持有obj鎖,因此等待線程雖被喚醒,但仍無法獲得obj鎖,直到調用線程退出synchronized塊,釋放obj鎖後,其他等待線程纔有機會獲得鎖繼續執行。

6、什麼導致線程阻塞?

(1)一般線程阻塞

1)線程執行了Thread.sleep(int millsecond)方法,放棄CPU,睡眠一段時間,一段時間過後恢復執行;

2)線程執行一段同步代碼,但無法獲得相關的同步鎖,只能進入阻塞狀態,等到獲取到同步鎖,才能恢復執行;

3)線程執行了一個對象的wait()方法,直接進入阻塞態,等待其他線程執行notify()/notifyAll()操作;

4)線程執行某些IO操作,因爲等待相關資源而進入了阻塞態,如System.in,但沒有收到鍵盤的輸入,則進入阻塞態。

5)線程禮讓,Thread.yield()方法,暫停當前正在執行的線程對象,把執行機會讓給相同或更高優先級的線程,但並不會使線程進入阻塞態,線程仍處於可執行態,隨時可能再次分得CPU時間。線程自閉,join()方法,在當前線程調用另一個線程的join()方法,則當前線程進入阻塞態,直到另一個線程運行結束,當前線程再由阻塞轉爲就緒態。

6)線程執行suspend()使線程進入阻塞態,必須resume()方法被調用,才能使線程重新進入可執行狀態。

7、線程如何關閉?

1) 使用標誌位,在線程的while循環中加上標誌位判斷,但標誌位要用volatile修飾,要不對標誌位的修改同步不到子線程中。

2)使用stop()方法,但該方法就像關掉電腦電源一樣,可能會發生預料不到的問題

3)使用中斷interrupt()

public class Thread {
    // 中斷當前線程
    public void interrupt();
    // 判斷當前線程是否被中斷
    public boolen isInterrupt();
    // 清除當前線程的中斷狀態,並返回之前的值
    public static boolen interrupted();   
}

但調用interrupt()方法只是傳遞中斷請求消息,並不代表要立馬停止目標線程。

8、講一下java中的同步的方法

之所以需要同步,因爲在多線程併發控制,當多個線程同時操作一個可共享的資源時,如果沒有采取同步機制,將會導致數據不準確,因此需要加入同步鎖,確保在該線程沒有完成操作前被其他線程調用,從而保證該變量的唯一一性和準確性。

1)synchronized修飾同步代碼塊或方法

由於java的每個對象都有一個內置鎖,用此關鍵字修飾方法時,內置鎖會保護整個方法。在調用該方法前,需獲得內置鎖,否則就處於陰塞狀態。

2)volatile修飾變量

保證變量在線程間的可見性,每次線程要訪問volatile修飾的變量時都從內存中讀取,而不緩存中,這樣每個線程訪問到的變量都是一樣的。且使用內存屏障。

3)ReentrantLock重入鎖,它常用的方法有ReentrantLock():創建一個ReentrantLock實例

lock()獲得鎖 unlock()釋放鎖

4)使用局部變量ThreadLocal實現線程同步,每個線程都會保存一份該變量的副本,副本之間相互獨立,這樣每個線程都可以隨意修改自己的副本,而不影響其他線程。常用方法ThreadLocal()創建一個線程本地變量;get()返回此線程局部的當前線程副本變量;initialValue()返回此線程局部變量的當前線程的初始值;set(T value)將此線程變量的當前線程副本中的值設置爲value

5) 使用原子變量,如AtomicInteger,常用方法AtomicInteger(int value)創建個有給定初始值的AtomicInteger整數;addAndGet(int data)以原子方式將給定值與當前值相加

6)使用阻塞隊列實現線程同步LinkedBlockingQueue<E>

9、如何保證線程安全?

線程安全性體現在三方法:

1)原子性:提供互斥訪問,同一時刻只能有一個線和至數據進行操作。

JDK中提供了很多atomic類,如AtomicInteger\AtomicBoolean\AtomicLong,它們是通過CAS完成原子性。JDK提供鎖分爲兩種:synchronized依賴JVM實現鎖,該關鍵字作用對象的作用範圍內同一時刻只能有一個線程進行操作。另一種是LOCK,是JDK提供的代碼層面的鎖,依賴CPU指令,代表性是ReentrantLock。

2)可見性:一個線程對主內存的修改及時被其他線程看到。

JVM提供了synchronized和volatile,volatile的可見性是通過內存屏障和禁止重排序實現的,volatile會在寫操作時,在寫操作後加一條store屏障指令,將本地內存中的共享變量值刷新到主內存;會在讀操作時,在讀操作前加一條load指令,從內存中讀取共享變量。

3)有序性:指令沒有被編譯器重排序。

可通過volatile、synchronized、Lock保證有序性。

10、兩個進程同時要求寫或者讀,能不能實現?如何防止進程的同步?

我認爲可以實現,比如兩個進程都讀取日曆進程數據是沒有問題,但同時寫,應該會有衝突。

可以使用共享內存實現進程間數據共享。

11、線程間操作List

由於ArrayList不是線程安全的,如果多線程操作,會導致數據不一致,因此若要實現線程安全,需要加鎖操作。

12、Java中對象的生命週期

1)創建階段(Created):爲對象分配存儲空間,開始構造對象,從超類到子類對static成員初始化;超類成員變量按順序初始化,遞歸調用超類的構造方法,子類成員變量按順序初始化,子類構造方法調用。

2)應用階段(In Use):對象至少被一個強引用持有着。

3)不可見階段(Invisible):程序運行已超出對象作用域

4)不可達階段(Unreachable):該對象不再被強引用所持有

5)收集階段(Collected):假設該對象重寫了finalize()方法且未執行過,會去執行該方法。

6)終結階段(Finalized):對象運行完finalize()方法仍處於不可達狀態,等待垃圾回收器對該對象空間進行回收。

7)對象空間重新分配階段(De-allocated):垃圾回收器對該對象所佔用的內存空間進行回收或再分配,該對象徹底消失。

13、static synchronized 方法的多線程訪問和作用

static synchronized控制的是類的所有實例訪問,不管new了多少對象,只有一份,所以對該類的所有對象都加了鎖。限制多線程中該類的所有實例同時訪問JVM中該類對應的代碼。

14、同一個類裏面兩個synchronized方法,兩個線程同時訪問的問題

如果synchronized修飾的是靜態方法,鎖的是當前類的class對象,進入同步代碼前要獲得當前類對象的鎖;

普通方法,鎖的是當前實例對象,進入同步代碼前要獲得的是當前實例的鎖;

同步代碼塊,鎖的是括號裏面的對象,對給定的對象加鎖,進入同步代碼塊庫前要獲得給定對象鎖;

如果兩個線程訪問同一個對象的synchronized方法,會出現競爭,如果是不同對象,則不會相互影響。

15、volatile的原理

有volatile變量修飾的共享變量進行寫操作的時候會多一條彙編代碼,lock addl $0x0,lock前綴的指令在多核處理器下會將當前處理器緩存行的數據會寫回到系統內存,這個寫回內存的操作會引起在其他CPU裏緩存了該內存地址的數據無效。同時lock前綴也相當於一個內存屏障,對內存操作順序進行了限制。

16、synchronized原理

synchronized通過對象的對象頭(markword)來實現鎖機制,java每個對象都有對象頭,都可以爲synchronized實現提供基礎,都可以作爲鎖對象,在字節碼層面synchronized塊是通過插入monitorenter monitorexit完成同步的。持有monitor對象,通過進入、退出這個Monitor對象來實現鎖機制。

17、談談NIO的理解

NIO( New Input/ Output) 引入了一種基於通道和緩衝區的 I/O 方式,它可以使用 Native 函數庫直接分配堆外內存,然後通過一個存儲在 Java 堆的 DirectByteBuffer 對象作爲這塊內存的引用進行操作,避免了在 Java 堆和 Native 堆中來回複製數據。  NIO 是一種同步非阻塞的 IO 模型。同步是指線程不斷輪詢 IO 事件是否就緒,非阻塞是指線程在等待 IO 的時候,可以同時做其他任務。同步的核心就是 Selector,Selector 代替了線程本身輪詢 IO 事件,避免了阻塞同時減少了不必要的線程消耗;非阻塞的核心就是通道和緩衝區,當 IO 事件就緒時,可以通過寫道緩衝區,保證 IO 的成功,而無需線程阻塞式地等待。

18、ReentrantLock 、lock、synchronized和volatile比較

1)volatile:解決變量在多個線程間的可見性,但不能保證原子性,只能用於修飾變量,不會發生阻塞。volatile能屏蔽編譯指令重排,不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面。多用於並行計算的單例模式。volatile規定CPU每次都必須從內存讀取數據,不能從CPU緩存中讀取,保證了多線程在多CPU計算中永遠拿到的都是最新的值。

2)synchronized:互斥鎖,操作互斥,併發線程過來,串行獲得鎖,串行執行代碼。解決的是多個線程間訪問共享資源的同步性,可保證原子性,也可間接保證可見性,因爲它會將私有內存和公有內存中的數據做同步。可用來修飾方法、代碼塊。會出現阻塞。synchronized發生異常時,會自動釋放線程佔有的鎖,因此不會導致死鎖現象發生。非公平鎖,每次都是相互爭搶資源。

3)lock是一個接口,而synchronized是java中的關鍵字,synchronized是內置語言的實現。lock可以讓等待鎖的線程響應中斷。在發生異常時,如果沒有主動通過unLock()去釋放鎖,則可能造成死鎖現象,因此使用Lock時需要在finally塊中釋放鎖。

4)ReentrantLock可重入鎖,鎖的分配機制是基於線程的分配,而不是基於方法調用的分配。ReentrantLock有tryLock方法,如果鎖被其他線程持有,返回false,可避免形成死鎖。對代碼加鎖的顆粒會更小,更節省資源,提高代碼性能。ReentrantLock可實現公平鎖和非公平鎖,公平鎖就是先來的先獲取資源。ReentrantReadWriteLock用於讀多寫少的場合,且讀不需要互斥場景。

  • ReentrantLock的內部實現

AQS(AbstractQueuedSynchronizer):抽象隊列同步器,定義了一套多線程訪問共享資源的同步器框架。

  • lock原理
  • 死鎖的四個必要條件?
  • 怎麼避免死鎖?
  • 對象鎖和類鎖是否會互相影響?
  • 什麼是線程池,如何使用?
  • Java的併發、多線程、線程模型
  • 談談對多線程的理解
  • 多線程有什麼要注意的問題?
  • 談談你對併發編程的理解並舉例說明
  • 談談你對多線程同步機制的理解?
  • 如何保證多線程讀寫文件的安全?
  • 多線程斷點續傳原理
  • 斷點續傳的實現

(五)併發編程有關知識點(這個是一般Android開發用的少的,所以建議多去看看):

平時Android開發中對併發編程可以做得比較少,Thread這個類經常會用到,但是我們想提升自己的話,一定不能停留在表面,,我們也應該去了解一下java的關於線程相關的源碼級別的東西。

二、Android面試題

Android面試題包括Android基礎,還有一些源碼級別的、原理這些等。所以想去大公司面試,一定要多看看源碼和實現方式,常用框架可以試試自己能不能手寫實現一下,鍛鍊一下自己。

(一)Android基礎知識點

1、四大組件是什麼

1)Activity:用戶可操作的可視化界面,爲用戶提供一個完成操作指令的窗口。一個Activity通常是一個單獨的屏幕,Activity通過Intent來進行通信。Android中會維持一個Activity Stack,當一個新Activity創建時,它就會放到棧頂,這個Activity就處於運行狀態。

2)Service:服務,運行在手機後臺,適合執行不需和用戶交互且還需長期運行的任務。

3)ContentProvider:內容提供者,使一個應用程序的指定數據集提供給其他應用程序,其他應用可通過ContentResolver類從該內容提供者中獲取或存入數據。它提供了一種跨進程數據共享的方式,當數據被修改後,ContentResolver接口的notifyChange函數通知那些註冊監控特定URI的ContentObserver對象。

如果ContentProvider和調用者在同一進程中,ContentProvider的方法(query/insert/update/delete等)和調用者在同一線程中;如果ContentProvider和調用者不在同一進程,ContentProvider方法會運行在它自身進程的一個Binder線程中。

4)Broadcast Receiver: 廣播接收者,運用在應用程序間傳輸信息,可以使用廣播接收器來讓應用對一個外部事件做出響應。

2、四大組件的生命週期和簡單用法

1)Activity:onCreate()->onStart()->onResume()->onPause()->onStop()->onDestory()

onCreate():爲Activity設置佈局,此時界面還不可見;

onStart(): Activity可見但還不能與用戶交互,不能獲得焦點

onRestart(): 重新啓動Activity時被回調

onResume(): Activity可見且可與用戶進行交互

onPause(): 當前Activity暫停,不可與用戶交互,但還可見。在新Activity啓動前被系統調用保存現有的Activity中的持久數據、停止動畫等。

onStop(): 當Activity被新的Activity覆蓋不可見時被系統調用

onDestory(): 當Activity被系統銷燬殺掉或是由於內存不足時調用

2)Service

a) onBind方式綁定的:onCreate->onBind->onUnBind->onDestory(不管調用bindService幾次,onCreate只會調用一次,onStart不會被調用,建立連接後,service會一直運行,直到調用unBindService或是之前調用的bindService的Context不存在了,系統會自動停止Service,對應的onDestory會被調用)

b) startService啓動的:onCreate->onStartCommand->onDestory(start多次,onCreate只會被調用一次,onStart會調用多次,該service會在後臺運行,直至被調用stopService或是stopSelf)

c) 又被啓動又被綁定的服務,不管如何調用onCreate()只被調用一次,startService調用多少次,onStart就會被調用多少次,而unbindService不會停止服務,必須調用stopService或是stopSelf來停止服務。必須unbindService和stopService(stopSelf)同時都調用了纔會停止服務。

3)BroadcastReceiver

a) 動態註冊:存活週期是在Context.registerReceiver和Context.unregisterReceiver之間,BroadcastReceiver每次收到廣播都是使用註冊傳入的對象處理的。

b) 靜態註冊:進程在的情況下,receiver會正常收到廣播,調用onReceive方法;生命週期只存活在onReceive函數中,此方法結束,BroadcastReceiver就銷燬了。onReceive()只有十幾秒存活時間,在onReceive()內操作超過10S,就會報ANR。

進程不存在的情況,廣播相應的進程會被拉活,Application.onCreate會被調用,再調用onReceive。

4)ContentProvider:應該和應用的生命週期一樣,它屬於系統應用,應用啓動時,它會跟着初始化,應用關閉或被殺,它會跟着結束。

3、Activity之間的通信方式

1)通過Intent方式傳遞參數跳轉

2)通過廣播方式

3)通過接口回調方式

4)藉助類的靜態變量或全局變量

5)藉助SharedPreference或是外部存儲,如數據庫或本地文件

4、Activity各種情況下的生命週期

1) 兩個Activity(A->B)切換(B正常的Activity)的生命週期:onPause(A)->onCreate(B)->onStart(B)->onResume(B)->oStop(A)

這時如果按回退鍵回退到A  onPause(B)->onRestart(A)->onStart(A)->onResume(A)->oStop(B)

如果在切換到B後調用了A.finish(),則會走到onDestory(A),這時點回退鍵會退出應用

2) 兩個Activity(A->B)切換(B透明主題的Activity或是Dialog風格的Acivity)的生命週期:onPause(A)->onCreate(B)->onStart(B)->onResume(B)

這時如果回退到A  onPause(B)->onResume(A)->oStop(B)->onDestory(B)

3) Activity(A)啓動後點擊Home鍵再回到應用的生命週期:onPause(A)->oStop(A)->onRestart(A)->onStart(A)->onResume(A)

5、橫豎屏切換的時候,Activity 各種情況下的生命週期

1)切換橫屏時:onSaveInstanceState->onPause->onStop->onDestory->onCreate->onStart->onRestoreInstanceState->onResume

2) 切換豎屏時:會打印兩次相同的log   

onSaveInstanceState->onPause->onStop->onDestory->onCreate->onStart->onRestoreInstanceState->onResume->onSaveInstanceState->onPause->onStop->onDestory->onCreate->onStart->onRestoreInstanceState->onResume

3) 如果在AndroidMainfest.xml中修改該Activity的屬性,添加android:configChanges="orientation"

橫豎屏切換,打印的log一樣,同1)

4) 如果AndroidMainfest.xml中該Activity中的android:configChanges="orientation|keyboardHidden",則只會打印

onConfigurationChanged->

6、Activity與Fragment之間生命週期比較

Fragment生命週期:onAttach->onCreate->onCreateView->onActivityCreated->onStart->onResume->onPause->onStop->onDestoryView->onDestory->onDetach

切換到該Fragment:onAttach->onCreate->onCreateView->onActivityCreated->onStart->onResume

按下Power鍵:onPause->onSaveInstanceState->onStop

點亮屏幕解鎖:onStart->onRestoreInstanceState->onResume

切換到其他Fragment: onPause->onStop->onDestoryView

切回到該Fragment: onCreateView->onActivityCreated->onStart->onResume

退出應用:onPause->onStop->onDestoryView->onDestory->onDetach

7、Activity上有Dialog的時候按Home鍵時的生命週期

AlertDialog並不會影響Activity的生命週期,按Home鍵後纔會使Activity走onPause->onStop,AlertDialog只是一個組件,並不會使Activity進入後臺。

8、兩個Activity 之間跳轉時必然會執行的是哪幾個方法?

前一個Activity的onPause,後一個Activity的onResume

9、前臺切換到後臺,然後再回到前臺,Activity生命週期回調方法。彈出Dialog,生命值週期回調方法。

1)前臺切換到後臺,會執行onPause->onStop,再回到前臺,會執行onRestart->onStart->onResume

2) 彈出Dialog,並不會影響Activity生命週期

10、Activity的四種啓動模式對比

1)standard:標準啓動模式(默認),每啓動一次Activity,都會創建一個實例,即使從ActivityA startActivity ActivityA,也會再次創建A的實例放於棧頂,當回退時,回到上一個ActivityA的實例。

2) singleTop:棧頂複用模式,每次啓動Activity,如果待啓動的Activity位於棧頂,則不會重新創建Activity的實例,即不會走onCreate->onStart,會直接進入Activity的onPause->onNewIntent->onResume方法

3) singleInstance: 單一實例模式,整個手機操作系統裏只有一個該Activity實例存在,沒有其他Actvity,後續請求均不會創建新的Activity。若task中存在實例,執行實例的onNewIntent()。應用場景:鬧鐘、瀏覽器、電話

4) singleTask:棧內複用,啓動的Activity如果在指定的taskAffinity的task棧中存在相應的實例,則會把它上面的Activity都出棧,直到當前Activity實例位於棧頂,執行相應的onNewIntent()方法。如果指定的task不存在,創建指定的taskAffinity的task,taskAffinity的作用,進入指寫taskAffinity的task,如果指定的task存在,將task移到前臺,如果指定的task不存在,創建指定的taskAffinity的task. 應用場景:應用的主頁面

11、Activity狀態保存於恢復

Activity被主動回收時,如按下Back鍵,系統不會保存它的狀態,只有被動回收時,雖然這個Activity實例已被銷燬,但系統在新建一個Activity實例時,會帶上先前被回收Activity的信息。在當前Activity被銷燬前調用onSaveInstanceState(onPause和onStop之間保存),重新創建Activity後會在onCreate後調用onRestoreInstanceState(onStart和onResume之間被調用),它們的參數Bundle用來數據保存和讀取的。

保存View狀態有兩個前提:View的子類必須實現了onSaveInstanceState; 必須要設定Id,這個ID作爲Bundle的Key

12、fragment各種情況下的生命週期

正常情況下的生命週期:onAttach->onCreate->onCreateView->onActivityCreated->onStart->onResume->onPause->onStop->onDestoryView->onDestory->onDetach

1)Fragment在Activity中replace  onPause(舊)->onAttach->onCreate->onCreateView->onActivityCreated->onStart->onResume->onStop(舊)->onDestoryView(舊)

如果添加到backStack中,調用remove()方法fragment的方法會走到onDestoryView,但不會執行onDetach(),即fragment本身的實例是存在的,成員變量也存在,但是view被銷燬了。如果新替換的Fragment已在BackStack中,則不會執行onAttach->onCreate

13、Fragment狀態保存onSaveInstanceState是哪個類的方法,在什麼情況下使用?

在對應的FragmentActivity.onSaveInstanceState方法會調用FragmentController.saveAllState,其中會對mActive中各個Fragment的實例狀態和View狀態分別進行保存.當Activity在做狀態保存和恢復的時候, 在它其中的fragment自然也需要做狀態保存和恢復.

參考文章:Android Fragment使用(三) Activity, Fragment, WebView的狀態保存和恢復

14、Fragment.startActivityForResult是和FragmentActivity的startActivityForResult?

如果希望在Fragment的onActivityResult接收數據,就要調用Fragment.startActivityForResult, 而不是Fragment.getActivity().startActivityForResult。Fragment.startActivityForResult->FragmentActivitymHost.HostCallbacks.onStartActivityFromFragment->FragmentActivity.startActivityFromFragment。如果request=-1則直接調用FragmentActivity.startActivityForResult,它會重新計算requestCode,使其大於0xfffff。

參考文章:徹底搞懂startActivityForResult在FragmentActivity和Fragment中的異同

15、如何實現Fragment的滑動?

ViewPager+FragmentPagerAdapter+List<Fragment>

16、fragment之間傳遞數據的方式?

1)在相應的fragment中編寫方法,在需要回調的fragment裏獲取對應的Fragment實例,調用相應的方法;

2)採用接口回調的方式進行數據傳遞;

a) 在Fragment1中創建一個接口及接口對應的set方法; b) 在Fragment1中調用接口的方法;c)在Fragment2中實現該接口;

3)利用第三方開源框架EventBus

17、service和activity怎麼進行數據交互?

1)通過bindService啓動服務,可以在ServiceConnection的onServiceConnected中獲取到Service的實例,這樣就可以調用service的方法,如果service想調用activity的方法,可以在service中定義接口類及相應的set方法,在activity中實現相應的接口,這樣service就可以回調接口言法;

2)通過廣播方式

18、說說ContentProvider、ContentResolver、ContentObserver 之間的關係

ContentProvider實現各個應用程序間數據共享,用來提供內容給別的應用操作。如聯繫人應用中就使用了ContentProvider,可以在自己應用中讀取和修改聯繫人信息,不過需要獲取相應的權限。它也只是一箇中間件,真正的數據源是文件或SQLite等。

ContentResolver內容解析者,用於獲取內容提供者提供的數據,通過ContentResolver.notifyChange(uri)發出消息

ContentObserver內容監聽者,可以監聽數據的改變狀態,觀察特定Uri引起的數據庫變化,繼而做一些相應的處理,類似於數據庫中的觸發器,當ContentObserver所觀察的Uri發生變化時,便會觸發它。

19、請描述一下廣播BroadcastReceiver的理解

BroadcastReceiver是一種全局監聽器,用來實現系統中不同組件之間的通信。有時候也會用來作爲傳輸少量而且發送頻率低的數據,但是如果數據的發送頻率比較高或者數量比較大就不建議用廣播接收者來接收了,因爲這樣的效率很不好,因爲BroadcastReceiver接收數據的開銷還是比較大的。

20、廣播的分類

1)普通廣播:完全異步的,可以在同一時刻(邏輯上)被所有接收者接收到,消息傳遞的效率比較高,並且無法中斷廣播的傳播。

2)有序廣播:發送有序廣播後,廣播接收者將按預先聲明的優先級依次接收Broadcast。優先級高的優先接收到廣播,而在其onReceiver()執行過程中,廣播不會傳播到下一個接收者,此時當前的廣播接收者可以abortBroadcast()來終止廣播繼續向下傳播,也可以將intent中的數據進行修改設置,然後將其傳播到下一個廣播接收者。 sendOrderedBroadcast(intent, null);//發送有序廣播

3)粘性廣播:sendStickyBroadcast()來發送該類型的廣播信息,這種的廣播的最大特點是,當粘性廣播發送後,最後的一個粘性廣播會滯留在操作系統中。如果在粘性廣播發送後的一段時間裏,如果有新的符合廣播的動態註冊的廣播接收者註冊,將會收到這個廣播消息,雖然這個廣播是在廣播接收者註冊之前發送的,另外一點,對於靜態註冊的廣播接收者來說,這個等同於普通廣播。

21、廣播使用的方式和場景

1)App全局監聽:在AndroidManifest中靜態註冊的廣播接收器,一般我們在收到該消息後,需要做一些相應的動作,而這些動作與當前App的組件,比如Activity或者Service的是否運行無關,比如我們在集成第三方Push SDK時,一般都會添加一個靜態註冊的BroadcastReceiver來監聽Push消息,當有Push消息過來時,會在後臺做一些網絡請求或者發送通知等等。

2)組件局部監聽:這種主要是在Activity或者Service中使用registerReceiver()動態註冊的廣播接收器,因爲當我們收到一些特定的消息,比如網絡連接發生變化時,我們可能需要在當前Activity頁面給用戶一些UI上的提示,或者將Service中的網絡請求任務暫停。所以這種動態註冊的廣播接收器適合特定組件的特定消息處理。

22、在manifest 和代碼中如何註冊和使用BroadcastReceiver?

1)mainfest中註冊:靜態註冊的廣播接收者就是一個常駐在系統中的全局監聽器,也就是說如果你應用中配置了一個靜態的BroadcastReceiver,而且你安裝了應用而無論應用是否處於運行狀態,廣播接收者都是已經常駐在系統中了。

<receiver android:name=".MyBroadcastReceiver">
    <intent-filter>
        <action android:name="com.smilexie.test.intent.mybroadcastreceiver"/>
    </intent-filter>
</receiver>

2) 動態註冊:動態註冊的廣播接收者只有執行了registerReceiver(receiver, filter)纔會開始監聽廣播消息,並對廣播消息作爲相應的處理。

IntentFilter fiter = new IntentFilter("com.smilexie.test.intent.mybroadcastreceiver");
MyBroadcastReceiver receiver = new MyBroadcastReceiver();
registerReceiver(receiver, filter);

//撤銷廣播接受者的動態註冊
unregisterReceiver(receiver);

23、本地廣播和全局廣播有什麼差別?

1)LocalBroadcastReceiver僅在自己的應用內發送接收廣播,也就是隻有自己的應用能收到,數據更加安全。廣播只在這個程序裏,而且效率更高。只能動態註冊,在發送和註冊的時候採用LocalBroadcastManager的sendBroadcast方法和registerReceiver方法。

2)全局廣播:發送的廣播事件可被其他應用程序獲取,也能響應其他應用程序發送的廣播事件(可以通過 exported–是否監聽其他應用程序發送的廣播 在清單文件中控制) 全局廣播既可以動態註冊,也可以靜態註冊。

24、AlertDialog,popupWindow,Activity區別

(1)Popupwindow在顯示之前一定要設置寬高,Dialog無此限制。

(2)Popupwindow默認不會響應物理鍵盤的back,除非顯示設置了popup.setFocusable(true);而在點擊back的時候,Dialog會消失。

(3)Popupwindow不會給頁面其他的部分添加蒙層,而Dialog會。

(4)Popupwindow沒有標題,Dialog默認有標題,可以通過dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);取消標題

(5)二者顯示的時候都要設置Gravity。如果不設置,Dialog默認是Gravity.CENTER。

(6)二者都有默認的背景,都可以通過setBackgroundDrawable(new ColorDrawable(android.R.color.transparent));去掉。
(7)Popupwindow彈出後,取得了用戶操作的響應處理權限,使得其他UI控件不被觸發。而AlertDialog彈出後,點擊背景,AlertDialog會消失。

25、Application 和 Activity 的 Context 對象的區別

1)Application Context是伴隨應用生命週期;不可以showDialog, startActivity, LayoutInflation

可以startService\BindService\sendBroadcast\registerBroadcast\load Resource values

2)Activity Context指生命週期只與當前Activity有關,而Activity Context這些操作都可以,即凡是跟UI相關的,都得用Activity做爲Context來處理。

一個應用Context的數量=Activity數量+Service數量+1(Application數量)

26、Android屬性動畫特性

1)可以對非View的對象進行動畫操作;

2)補間動畫只能實現移動、縮放、旋轉、淡入淡出,而無法擴展,如對背景顏色進行改變等,但是屬性動畫就可以;

3)屬性動畫真正改變了對象的屬性

27、如何導入外部數據庫?

android系統下數據庫應該存放在/data/data/com..(package name)/目錄下,通過FileInputStream讀取原數據庫,再用FileOutputStream把讀取到的東西寫入到那個目錄下。在原數據庫放在項目源碼的/res/raw/目錄下,建立一個DBManager類,

28、談談對接口與回調的理解

原理:先創建一個對象,再創建一個控制器對象,將回調對象需要被調用的方法告訴控制器對象,控制器對象負責檢查某個場景是否出現或某個條件是否滿足,當滿足時,自動調用回調對象方法。

29、介紹下SurfView

SurfaceView擁有獨立的繪圖表面Surface,它不與其宿主窗口共享一個Surface,由於擁有獨立的Surface,所以SurfaceView的UI就可以在一個單獨的線程中進行繪製。繪圖流程:在繪圖表面的基礎上創建一塊畫布,即獲得一個Canvas對象;利用Canvas類提供的繪圖接口在前面獲得的畫布上繪製任意的UI;將已填充好了的UI數據的畫布緩衝區提交給SurfaceFlinger服務,以便SurfaceFlinger服務可以將它合成到屏幕上。

優點:1)可以在非UI線程中繪製;2)SurfaceView的頻率可以操作60FPS; 3) 在要求實時性比較高的遊戲開發中,view的onDraw一般滿足不了要求,這時只能用SurfaceView

使用步驟:

1)繼承自SurfaceView; 2)初始化時,拿到SurfaceHolder,給SurfaceHolder設置Callback; 3) 在Callback中寫幾個回調方法surfaceCreated\surfaceChanged\surfaceDestoryed;4)在surfaceCreated中起一個線程,在線程中拿到SurfaceHolder去鎖定Canvas進行繪圖。

RecycleView的使用

序列化的作用,以及Android兩種序列化的區別

差值器

實現Interpolator接口,根據時間來計算當前屬性需要改變的百分比值。設置值的變化趨勢,SDK中包含了勻速插值器LinearInterpolator、加速插值器、減速插值器DecelerateInterpolator、先加速再減速AccelerateDecelerateInterpolator、彈

  • 估值器

根據屬性變化的百分比值來計算改變後的屬性值。需要實現TypeEvaluatior接口

  • Android中數據存儲方式

1)SharedPreferences:用來存儲一些簡單的配置信息,如登錄的用戶名、密碼,採用map數據結構存儲。寫入的時候先調用edit()使其處於編輯態,再修改數據,最後用commit()來提交修改的數據。採用XML格式將數據存儲到設備上/data/data/<package name>/shares_prefs下。只能在同一個包內使用,不能在不同包之間使用。

2) 數據庫SQLite

3)SD卡,本地存儲:openFileInput()和openFileOutput()

4)使用ContentProvider存儲數據:可向其他應用共享數據,

(二)Android源碼相關分析

1、Android屬性動畫實現原理

工作原理:在一定時間間隔內,通過不斷對值進行改變,並不斷將該值賦給對象的屬性,從而實現該對象在該屬性上的動畫效果。

1)ValueAnimator:通過不斷控制值的變化(初始值->結束值),將值手動賦值給對象的屬性,再不斷調用View的invalidate()方法,去不斷onDraw重繪view,達到動畫的效果。

主要的三種方法:

a) ValueAnimator.ofInt(int values):估值器是整型估值器IntEaluator

b) ValueAnimator.ofFloat(float values):估值器是浮點型估值器FloatEaluator

c) ValueAnimator.ofObject(ObjectEvaluator, start, end):將初始值以對象的形式過渡到結束值,通過操作對象實現動畫效果,需要實現Interpolator接口,自定義估值器  

估值器TypeEvalutor,設置動畫如何從初始值過渡到結束值的邏輯。插值器(Interpolator)決定值的變化模式(勻速、加速等);估值器(TypeEvalutor)決定值的具體變化數值。

// 自定義估值器,需要實現TypeEvaluator接口
public class ObjectEvaluator implements TypeEvaluator{  

// 複寫evaluate(),在evaluate()裏寫入對象動畫過渡的邏輯
    @Override  
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
        // 參數說明
        // fraction:表示動畫完成度(根據它來計算當前動畫的值)
        // startValue、endValue:動畫的初始值和結束值

        ... // 寫入對象動畫過渡的邏輯
        
        return value;  
        // 返回對象動畫過渡的邏輯計算後的值
    } 

2) ObjectAnimator:直接對對象的屬性值進行改變操作,從而實現動畫效果

ObjectAnimator繼承自ValueAnimator類,底層的動畫實現機制還是基本值的改變。它是不斷控制值的變化,再不斷自動賦給對象的屬性,從而實現動畫效果。這裏的自動賦值,是通過調用對象屬性的set/get方法進行自動賦值,屬性動畫初始值如果有就直接取,沒有則調用屬性的get()方法獲取,當值更新變化時,通過屬性的set()方法進行賦值。每次賦值都是調用view的postInvalidate()/invalidate()方法不斷刷新視圖(實際調用了onDraw()方法進行了重繪視圖)。

//Object 需要操作的對象; propertyName 需要操作的對象的屬性; values動畫初始值&結束值,
//如果是兩個值,則從a->b值過渡,如果是三值,則從a->b->c
ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String propertyName, float ...values);

如果採用ObjectAnimator類實現動畫,操作的對象的屬性必須有get()和set()方法。

其他用法:

1)AnimatorSet組合動畫 

AnimatorSet.play(Animator anim)   :播放當前動畫
AnimatorSet.after(long delay)   :將現有動畫延遲x毫秒後執行
AnimatorSet.with(Animator anim)   :將現有動畫和傳入的動畫同時執行
AnimatorSet.after(Animator anim)   :將現有動畫插入到傳入的動畫之後執行
AnimatorSet.before(Animator anim) :  將現有動畫插入到傳入的動畫之前執行

2) ViewPropertyAnimator直接對屬性操作,View.animate()返回的是一個ViewPropertyAnimator對象,之後的調用方法都是基於該對象的操作,調用每個方法返回值都是它自身的實例

View.animate().alpha(0f).x(500).y(500).setDuration(500).setInterpolator()

3)設置動畫監聽

Animation.addListener(new AnimatorListener() {
          @Override
          public void onAnimationStart(Animation animation) {
              //動畫開始時執行
          }
      
           @Override
          public void onAnimationRepeat(Animation animation) {
              //動畫重複時執行
          }

         @Override
          public void onAnimationCancel()(Animation animation) {
              //動畫取消時執行
          }
    
          @Override
          public void onAnimationEnd(Animation animation) {
              //動畫結束時執行
          }
      });

 

2、補間動畫實現原理

主要有四種AlpahAnimation\ ScaleAnimation\ RotateAnimation\ TranslateAnimation四種,對透明度、縮放、旋轉、位移四種動畫。在調用View.startAnimation時,先調用View.setAnimation(Animation)方法給自己設置一個Animation對象,再調用invalidate來重繪自己。在View.draw(Canvas, ViewGroup, long)方法中進行了getAnimation(), 並調用了drawAnimation(ViewGroup, long, Animation, boolean)方法,此方法調用Animation.getTranformation()方法,再調用applyTranformation()方法,該方法中主要是對Transformation.getMatrix().setTranslate/setRotate/setAlpha/setScale來設置相應的值,這個方法系統會以60FPS的頻率進行調用。具體是在調Animation.start()方法中會調用animationHandler.start()方法,從而調用了scheduleAnimation()方法,這裏會調用mChoreographer.postCallback(Choregrapher.CALLBACK_ANIMATION, this, null)放入事件隊列中,等待doFrame()來消耗事件。

當一個 ChildView 要重畫時,它會調用其成員函數 invalidate() 函數將通知其 ParentView 這個 ChildView 要重畫,這個過程一直向上遍歷到 ViewRoot,當 ViewRoot 收到這個通知後就會調用ViewRoot 中的 draw 函數從而完成繪製。View::onDraw() 有一個畫布參數 Canvas, 畫布顧名思義就是畫東西的地方,Android 會爲每一個 View 設置好畫布,View 就可以調用 Canvas 的方法,比如:drawText, drawBitmap, drawPath 等等去畫內容。每一個 ChildView 的畫布是由其 ParentView 設置的,ParentView 根據 ChildView 在其內部的佈局來調整 Canvas,其中畫布的屬性之一就是定義和 ChildView 相關的座標系,默認是橫軸爲 X 軸,從左至右,值逐漸增大,豎軸爲 Y 軸,從上至下,值逐漸增大。

çªå£åæ ç³»

Android 補間動畫就是通過 ParentView 來不斷調整 ChildView 的畫布座標系來實現的,在ParentView的dispatchDraw方法會被調用。

dispatchDraw() 
{ 
.... 
Animation a = ChildView.getAnimation() 
Transformation tm = a.getTransformation(); 
Use tm to set ChildView's Canvas; 
Invalidate(); 
.... 
}

這裏有兩個類:Animation 和 Transformation,這兩個類是實現動畫的主要的類,Animation 中主要定義了動畫的一些屬性比如開始時間、持續時間、是否重複播放等,這個類主要有兩個重要的函數:getTransformation 和 applyTransformation,在 getTransformation 中 Animation 會根據動畫的屬性來產生一系列的差值點,然後將這些差值點傳給 applyTransformation,這個函數將根據這些點來生成不同的 Transformation,Transformation 中包含一個矩陣和 alpha 值,矩陣是用來做平移、旋轉和縮放動畫的,而 alpha 值是用來做 alpha 動畫的(簡單理解的話,alpha 動畫相當於不斷變換透明度或顏色來實現動畫),調用 dispatchDraw 時會調用 getTransformation 來得到當前的 Transformation。某一個 View 的動畫的繪製並不是由他自己完成的而是由它的父 view 完成。

1)補間動畫TranslateAnimation,View位置移動了,可是點擊區域還在原來的位置,爲什麼?

View在做動畫是,根據動畫時間的插值,計算出一個Matrix,不停的invalidate,在onDraw中的Canvas上使用這個計算出來的Matrix去draw view的內容。某個view的動畫繪製並不是由它自己完成,而是由它的父view完成,使它的父view畫布進行了移動,而點擊時還是點擊原來的畫布。使得它看起來變化了。

參考文章:Android 動畫框架詳解,第 1 部分

3、Android各個版本API的區別

Android API版本對照表及各個版本特性簡單描述

主要記住一些大版本變化:

android3.0 代號Honeycomb, 引入Fragments, ActionBar,屬性動畫,硬件加速

android4.0 代號Ice Cream,API14:截圖功能,人臉識別,虛擬按鍵,3D優化驅動

android5.0 代號Lollipop API21:調整桌面圖標及部件透明度等

android6.0 代號M Marshmallow API23,軟件權限管理,安卓支付,指紋支持,App關聯,

android7.0 代號N Preview API24,多窗口支持(不影響Activity生命週期),增加了JIT編譯器,引入了新的應用簽名方案APK Signature Scheme v2(縮短應用安裝時間和更多未授權APK文件更改保護),嚴格了權限訪問

android8.0 代號O  API26,取消靜態廣播註冊,限制後臺進程調用手機資源,桌面圖標自適應

android9.0 代號P API27,加強電池管理,系統界面添加了Home虛擬鍵,提供人工智能API,支持免打擾模式

4、Requestlayout,onlayout,onDraw,DrawChild區別與聯繫

requestLayout()方法 :會導致調用measure()過程 和 layout()過程 。 說明:只是對View樹重新佈局layout過程包括measure()和layout()過程,如果view的l,t,r,b沒有必變,那就不會觸發onDraw;但是如果這次刷新是在動畫裏,mDirty非空,就會導致onDraw。

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

onDraw()方法繪製視圖本身 (每個View都需要重載該方法,ViewGroup不需要實現該方法)

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

5、invalidate和postInvalidate的區別及使用

View.invalidate(): 層層上傳到父級,直到傳遞到ViewRootImpl後觸發了scheduleTraversals(),然後整個View樹開始重新按照View繪製流程進行重繪任務。

invalidate:在ui線程刷新view
postInvalidate:在工作線程刷新view(底層還是handler)其實它的原理就是invalidate+handler

View.postInvalidate最終會調用ViewRootImpl.dispatchInvalidateDelayed()方法

public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

這裏的mHandler是ViewRootHandler實例,在該Handler的handleMessage方法中調用了view.invalidate()方法。

case MSG_INVALIDATE:
                    ((View) msg.obj).invalidate();
                    break;

6、Activity-Window-View三者的差別

Activity:是安卓四大組件之一,負責界面展示、用戶交互與業務邏輯處理;
Window:就是負責界面展示以及交互的職能部門,就相當於Activity的下屬,Activity的生命週期方法負責業務的處理;
View:就是放在Window容器的元素,Window是View的載體,View是Window的具體展示。
三者的關係: Activity通過Window來實現視圖元素的展示,window可以理解爲一個容器,盛放着一個個的view,用來執行具體的展示工作。
 

7、談談對Volley的理解

8、如何優化自定義View

1)在要在onDraw或是onLayout()中去創建對象,因爲onDraw()方法可能會被頻繁調用,可以在view的構造函數中進行創建對象;

2)降低view的刷新頻率,儘可能減少不必要的調用invalidate()方法。或是調用帶四種參數不同類型的invalidate(),而不是調用無參的方法。無參變量需要刷新整個view,而帶參數的方法只需刷新指定部分的view。在onDraw()方法中減少冗餘代碼。

3)使用硬件加速,GPU硬件加速可以帶來性能增加。

4)狀態保存與恢復,如果因內存不足,Activity置於後臺被殺重啓時,View應儘可能保存自己屬性,可以重寫onSaveInstanceState和onRestoreInstanceState方法,狀態保存。

9、低版本SDK如何實現高版本api?

使用@TargetApi註解·

當代碼中有比AndroidManifest中設置的android:minSdkVersion版本更高的方法,此時編譯器會提示警告,解決方法是在方法上加上@SuppressLint("NewApi")或者@TargetApi()。但它們僅是屏蔽了android lint錯誤,在方法中還要判斷版本做不同的操作。

@SuppressLint("NewApi")屏蔽一切新api中才能使用的方法報的android lint錯誤

@TargetApi() 只屏蔽某一新api中才能使用的方法報的android lint錯誤,如@TargetApi(11)如果在方法中用了只有API14纔開始有的方法,還是會報錯。

10、描述一次網絡請求的流程

1)域名解析

瀏覽器會先搜索自身DNS緩存且對應的IP地址沒有過期;若未找到則搜索操作系統自身的DNS緩存;若還未找到則讀本地的hotsts文件;還未找到會在TCP/IP設置的本地DNS服務器上找,如果要查詢的域名在本地配置的區域資源中,則完成解析;否則根據本地DNS服務器會請求根DNS服務器;根DNS服務器是13臺根DNS,會一級一級往下找。

2)TCP三次握手

客戶端先發送SYN=1,ACK=0,序列號seq=x報文;(SYN在連接建立時用來同步序號,SYN=1,ACK=0代表這是一個連接請求報文,對方若同意建立連接,則應在響應報文中使SYN=1,ACK=1)

服務器返回SYN=1,ACK=1,seq=y, ack=x+1;

客戶端再一次確認,但不用SYN了,回覆服務端, ACK=1, seq=x+1, ack=y+1

3)建立TCP連接後發起HTTP請求

客戶端按照指定的格式開始向服務端發送HTTP請求,HTTP請求格式由四部分組成,分別是請求行、請求頭、空行、消息體,服務端接收到請求後,解析HTTP請求,處理完成邏輯,最後返回一個具有標準格式的HTTP響應給客戶端。

4)服務器響應HTTP請求

服務器接收處理完請求後返回一個HTTP響應消息給客戶端,HTTP響應信息格式包括:狀態行、響應頭、空行、消息體

5)瀏覽器解析HTML代碼,請求HTML代碼中的資源

瀏覽器拿到html文件後,就開始解析其中的html代碼,遇到js/css/image等靜態資源時,向服務器發起一個http請求,如果返回304狀態碼,瀏覽器會直接讀取本地的緩存文件。否則開啓線程向服務器請求下載。

6)瀏覽器對頁面進行渲染並呈現給用戶

7)TCP的四次揮手

當客戶端沒有東西要發送時就要釋放連接(提出中斷連接可以是Client也可以是Server),客戶端會發送一個FIN=1的沒有數據的報文,進入FIN_WAIT狀態,服務端收到後會給客戶端一個確認,此時客戶端不能發送數據,但可接收信息。

11、HttpUrlConnection 和 okhttp關係

兩者都可以用來實現網絡請求,android4.4之後的HttpUrlConnection的實現是基於okhttp

  • Bitmap對象的理解
  • looper架構
  • ActivityThread,AMS,WMS的工作原理
  • 自定義View如何考慮機型適配

在onMeasure()的getDefaultSize()的默認實現中,當view的測量模式是AT_MOST或EXACTLY時,View的大小都會被設置成子View MeasureSpec的specSize.子view的MeasureSpec值是根據子View的佈局參數和父容器的MeasureSpec值計算得來。當子view的佈局參數是wrap_content時,對應的測量模式是AT_MOST,大小是parentSize,

  • 自定義View的事件
  • AstncTask+HttpClient 與 AsyncHttpClient有什麼區別?
  • LaunchMode應用場景
  • AsyncTask 如何使用?
  • SpareArray原理
  • 請介紹下ContentProvider 是如何實現數據共享的?
  • AndroidService與Activity之間通信的幾種方式
  • IntentService原理及作用是什麼?

原理:IntentService是繼承Service的一個抽象類,它在onCreate()方法中創建了一個HandlerThread,並啓動該線程。HandlerThread是帶有自己消息隊列和Looper的線程,根據HandlerThread的looper創建一個Handler,這樣IntentService的ServiceHandler的handleMessage()方法就運行在子線程中。handleMessage中調用了onHandleIntent()方法,它是一個抽象方法,繼承IntentService類需要實現該方法,把耗時操作放在onHandleIntent()方法中,等耗時操作運行完成後,會調用stopSelf()方法,服務會調用onDestory()方法消毀自己。如果onHandleIntent()中的耗時操作未運行完前就調用了stopSelf()方法,服務調用onDestory()方法,但耗時操作會繼續運行,直至運行完畢。如果同時多次啓動IntentService,任務會放在一個隊列中,onCreate()和onDestory()方法都只會運行一次。

作用:用來處理後臺耗時操作,如讀取數據庫或是本地文件等。

  • 說說Activity、Intent、Service 是什麼關係
  • ApplicationContext和ActivityContext的區別
  • SP是進程同步的嗎?有什麼方法做到同步?
  • 談談多線程在Android中的使用
  • 進程和 Application 的生命週期
  • 封裝View的時候怎麼知道view的大小
  • RecycleView原理
  • AndroidManifest的作用與理解

(三)常見的一些原理性問題

  • Handler機制和底層實現
  • Handler、Thread和HandlerThread的差別

1)Handler線程的消息通訊的橋樑,主要用來發送消息及處理消息。

2)Thread普通線程,如果需要有自己的消息隊列,需要調用Looper.prepare()創建Looper實例,調用loop()去循環消息。

3)HandlerThread是一個帶有Looper的線程,在HandleThread的run()方法中調用了Looper.prepare()創建了Looper實例,並調用Looper.loop()開啓了Loop循環,循環從消息隊列中獲取消息並交由Handler處理。利用該線程的Looper創建Handler實例,此Handler的handleMessage()方法是運行在子線程中的。即Handler利用哪個線程的Looper創建的實例,它就和相應的線程綁定到一起,處理該線程上的消息,它的handleMessage()方法就是在那個線程中運行的,無參構造默認是主線程。HandlerThread提供了quit()/quitSafely()方法退出HandlerThread的消息循環,它們分別調用Looper的quit和quitSafely方法,quit會將消息隊列中的所有消息移除,而quitSafely會將消息隊列所有延遲消息移除,非延遲消息派發出去讓Handler去處理。

HandlerThread適合處理本地IO讀寫操作(讀寫數據庫或文件),因爲本地IO操作耗時不長,對於單線程+異步隊列不會產生較大阻塞,而網絡操作相對比較耗時,容易阻塞後面的請求,因此HandlerThread不適合加入網絡操作。

  • handler發消息給子線程,looper怎麼啓動?
  • 關於Handler,在任何地方new Handler 都是什麼線程下?
  • ThreadLocal原理,實現及如何保證Local屬性?
  • 請解釋下在單線程模型中Message、Handler、Message Queue、Looper之間的關係
  • 請描述一下View事件傳遞分發機制
  • Touch事件傳遞流程
  • 事件分發中的onTouch 和onTouchEvent 有什麼區別,又該如何使用?
  • View和ViewGroup分別有哪些事件分發相關的回調方法
  • View刷新機制
  • View繪製流程
  • 自定義控件原理
  • 自定義View如何提供獲取View屬性的接口?
  • Android代碼中實現WAP方式聯網
  • AsyncTask機制
  • AsyncTask原理及不足
  • 如何取消AsyncTask?
  • 爲什麼不能在子線程更新UI?
  • ANR產生的原因是什麼?
  • ANR定位和修正
  • oom是什麼?

oom(Out Of Memory)內存溢出,

  • 什麼情況導致oom?
  • 有什麼解決方法可以避免OOM?
  • Oom 是否可以try catch?爲什麼?

可以,當

  • 內存泄漏是什麼?

內存泄露就是指該被GC垃圾回收的,但被一個生命週期比它長的對象仍然在引用它,導致無法回收,造成內存泄露,過多的內存泄露會導致OOM。

  • 什麼情況導致內存泄漏?

1)非靜態內部類、匿名內部類:非靜態內部類、匿名內部類 都會持有外部類的一個引用,如果有一個靜態變量引用了非靜態內部類或者匿名內部類,導致非靜態內部類或者匿名內部類的生命週期比外部類(Activity)長,就會導致外部類在該被回收的時候,無法被回收掉,引起內存泄露, 除非外部類被卸載。

解決辦法:將非靜態內部類、匿名內部類 改成靜態內部類,或者直接抽離成一個外部類。 如果在靜態內部類中,需要引用外部類對象,那麼可以將這個引用封裝在一個WeakReference中。
2)靜態的View:當一個Activity經常啓動,但是對應的View讀取非常耗時,我們可以通過靜態View變量來保持對該Activity的rootView引用。這樣就可以不用每次啓動Activity都去讀取並渲染View了。但View attach到我們的Window上,就會持有一個Context(即Activity)的引用。而我們的View有事一個靜態變量,所以導致Activity不被回收。

解決辦法: 在使用靜態View時,需要確保在資源回收時,將靜態View detach掉
3)Handler:在Activity中定義Handler對象,那麼Handler持有Activty的引用。而每個Message對象是持有Handler的引用的(Message對象的target屬性持有Handler引用),從而導致Message間接引用到了Activity。如果在Activty destroy之後,消息隊列中還有Message對象,Activty是不會被回收的。
解決辦法: 將Handler放入單獨的類或者將Handler放入到靜態內部類中(靜態內部類不會持有外部類的引用)。如果想要在handler內部去調用所在的外部類Activity,可以在handler內部使用弱引用的方式指向所在Activity,在onDestory時,調用相應的方法移除回調和刪除消息。
4)監聽器(各種需要註冊的Listener,Watcher等):當我們需要使用系統服務時,比如執行某些後臺任務、爲硬件訪問提供接口等等系統服務。我們需要把自己註冊到服務的監聽器中。然而,這會讓服務持有 activity 的引用,如果程序員忘記在 activity 銷燬時取消註冊,那就會導致 activity 泄漏了。

解決辦法:在onDestory中移除註冊
5. 資源對象沒關閉造成內存泄漏:當我們打開資源時,一般都會使用緩存。比如讀寫文件資源、打開數據庫資源、使用Bitmap資源等等。當我們不再使用時,應該關閉它們,使得緩存內存區域及時回收。

解決辦法:使用try finally結合,在try塊中打開資源,在finally中關閉資源
6. 屬性動畫:在使用ValueAnimator或者ObjectAnimator時,如果沒有及時做cancel取消動畫,就可能造成內存泄露。因爲在cancel方法裏,最後調用了endAnimation(); ,在endAnimation裏,有個AnimationHandler的單例,會持有屬性動畫對象的引用。

解決辦法:在onDestory中調用動畫的cancel方法

7. RxJava:在使用RxJava時,如果在發佈了一個訂閱後,由於沒有及時取消,導致Activity/Fragment無法銷燬,導致的內存泄露。

解決辦法:使用RxLifeCycle

  • 內存泄漏和內存溢出區別?

 

(1)內存泄漏

1)內存泄漏:指程序中已動態分配的堆內存由於某種原因未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統奔潰等嚴重後果。

2)一次內存泄漏似乎不會有大的影響,但內存泄漏後堆積的結果就是內存溢出。

3)內存泄漏具有隱蔽性,積累性的特徵,比其他內存非法訪問錯誤更難檢測。這是因爲內存泄漏產生的原因是內存塊未被釋放,屬於遺漏型缺陷而不是過錯型缺陷。此外,內存泄漏不會直接產生可觀察的錯誤,而是逐漸積累,降低系統的整體性性能。

4)如何有效的進行內存分配和釋放,防止內存泄漏,是軟件開發人員的關鍵問題,比如一個服務器應用軟件要長時間服務多個客戶端,若存在內存泄漏,則會逐漸堆積,導致一系列嚴重後果。

(2)內存溢出

指程序在申請內存時,沒有足夠的內存供申請者使用,或者說,給了你一塊存儲int類型數據的存儲空間,但是你卻存儲long類型的數據,就會導致內存不夠用,報錯OOM,即出現內存溢出的錯誤。

  • LruCache默認緩存大小

4MB

  • ContentProvider的權限管理(解答:讀寫分離,權限控制-精確到表級,URL控制)
  • 如何通過廣播攔截和abort一條短信?
  • 廣播是否可以請求網絡?
  • 廣播引起anr的時間限制是多少?
  • 計算一個view的嵌套層級
  • Activity棧
  • Android線程有沒有上限?

理論上是沒有上限的,但按一般寫法一般是線程最多開到2*CPU個數+1

  • 線程池有沒有上限?

要根據用戶調用不同的線程池構造函數。

  • ListView重用的是什麼?
  • Android爲什麼引入Parcelable?
  • 有沒有嘗試簡化Parcelable的使用?

(四)開發中常見的一些問題

  • ListView 中圖片錯位的問題是如何產生的?
  • 混合開發有了解嗎?
  • 知道哪些混合開發的方式?說出它們的優缺點和各自使用場景?(解答:比如:RN,weex,H5,小程序,WPA等。做Android的瞭解一些前端js等還是很有好處的);
  • 屏幕適配的處理技巧都有哪些?
  • 服務器只提供數據接收接口,在多線程或多進程條件下,如何保證數據的有序到達?
  • 動態佈局的理解
  • 怎麼去除重複代碼?
  • 畫出 Android 的大體架構圖
  • Recycleview和ListView的區別
  • ListView圖片加載錯亂的原理和解決方案

ListView item緩存機制:爲了使得性能更優,ListView會緩存行item(某行對應的View)。ListView通過adapter的getView函數獲得每行的item。滑動過程中,1)如果某行item已經滑出屏幕,若該item不在緩存內,則put進緩存,否則更新緩存;
2)獲取滑入屏幕的行item之前會先判斷緩存中是否有可用的item,如果有,做爲convertView參數傳遞給adapter的getView。

出現的問題:

1)行item圖片顯示重複,當前行item顯示了之前某行item的圖片。
比如ListView滑動到第2行會異步加載某個圖片,但是加載很慢,加載過程中listView已經滑動到了第14行,且滑動過程中該圖片加載結束,第2行已不在屏幕內,根據上面介紹的緩存原理,第2行的view可能被第14行復用,這樣我們看到的就是第14行顯示了本該屬於第2行的圖片,造成顯示重複。

2)行item圖片顯示閃爍
如果第14行圖片又很快加載結束,所以我們看到第14行先顯示了第2行的圖片,立馬又顯示了自己的圖片進行覆蓋造成閃爍錯亂。

解決方法
通過上面的分析我們知道了出現錯亂的原因是異步加載及對象被複用造成的,如果每次getView能給對象一個標識,在異步加載完成時比較標識與當前行item的標識是否一致,一致則顯示,否則不做處理即可。

  • 動態權限適配方案,權限組的概念
  • Android系統爲什麼會設計ContentProvider?
  • 下拉狀態欄是不是影響activity的生命週期
  • 如果在onStop的時候做了網絡請求,onResume的時候怎麼恢復?
  • Bitmap 使用時候注意什麼?
  • Bitmap的recycler()
  • Android中開啓攝像頭的主要步驟
  • ViewPager使用細節,如何設置成每次只初始化當前的Fragment,其他的不初始化?
  • 點擊事件被攔截,但是想傳到下面的View,如何操作?
  • 微信主頁面的實現方式
  • 微信上消息小紅點的原理
  • CAS介紹

三、混合開發面試題

大廠除了技術深度之外,還要求你具備一些廣度的知識,比如你要會前端知識,會混合開發,至少會一種腳本語言,C c++更不用說了,也是必會的。

  • Hybrid做過嗎?
  • Hybrid通信原理是什麼,有做研究嗎?
  • react native有多少了解?講一下原理。
  • weex瞭解嗎?如何自己實現類似技術?
  • flutter瞭解嗎?內部是如何實現跨平臺的?
  • Dart語言有研究貴嗎?
  • 快應用瞭解嗎?跟其她方式相比有什麼優缺點?
  • 說說你用過的混合開發技術有哪些?各有什麼優缺點?
  • Python會嗎?
  • 會不會PHP?
  • Gradle瞭解多少?groovy語法會嗎?

四、高端技術面試題

這裏講的是大公司需要用到的一些高端Android技術,這裏專門整理了一個文檔,希望大家都可以看看。這些題目有點技術含量,需要好點時間去研究一下的。

(一)圖片

1、圖片庫對比

2、LRUCache原理

LruCache是個泛型類,主要原理是:把最近使用的對象用強引用存儲在LinkedHashMap中,當緩存滿時,把最近最少使用的對象從內存中移除,並提供get/put方法完成緩存的獲取和添加。LruCache是線程安全的,因爲使用了synchronized關鍵字。

當調用put()方法,將元素加到鏈表頭,如果鏈表中沒有該元素,大小不變,如果沒有,需調用trimToSize方法判斷是否超過最大緩存量,trimToSize()方法中有一個while(true)死循環,如果緩存大小大於最大的緩存值,會不斷刪除LinkedHashMap中隊尾的元素,即最少訪問的,直到緩存大小小於最大緩存值。當調用LruCache的get方法時,LinkedHashMap會調用recordAccess方法將此元素加到鏈表頭部。

3、圖片加載原理

 

4、自己去實現圖片庫,怎麼做?

 

5、Glide源碼解析

1)Glide.with(context)創建了一個RequestManager,同時實現加載圖片與組件生命週期綁定:在Activity上創建一個透明的ReuqestManagerFragment加入到FragmentManager中,通過添加的Fragment感知Activty\Fragment的生命週期。因爲添加到Activity中的Fragment會跟隨Activity的生命週期。在RequestManagerFragment中的相應生命週期方法中通過liftcycle傳遞給在lifecycle中註冊的LifecycleListener

2)RequestManager.load(url) 創建了一個RequestBuilder<T>對象 T可以是Drawable對象或是ResourceType等

3) RequestBuilder.into(view)

-->into(glideContext.buildImageViewTarget(view, transcodeClass))返回的是一個DrawableImageViewTarget, Target用來最終展示圖片的,buildImageViewTarget-->ImageViewTargetFactory.buildTarget()根據傳入class參數不同構建不同的Target對象,這個Class是根據構建Glide時是否調用了asBitmap()方法,如果調用了會構建出BitmapImageViewTarget,否則構建的是GlideDrawableImageViewTarget對象。

-->GenericRequestBuilder.into(Target),該方法進行了構建Request,並用RequestTracker.runRequest()

Request request = buildRequest(target);//構建Request對象,Request是用來發出加載圖片的,它調用了buildRequestRecursive()方法以,內部調用了GenericRequest.obtain()方法
target.setRequest(request);
lifecycle.addListener(target);
requestTracker.runRequest(request);//判斷Glide當前是不是處於暫停狀態,若不是則調用Request.begin()方法來執行Request,否則將Request添加到待執行隊列裏,等暫停態解除了後再執行

-->GenericRequest.begin()

1)onSizeReady()--> Engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,
            priority, isMemoryCacheable, diskCacheStrategy, this) --> a)先構建EngineKey; b) loadFromCache從緩存中獲取EngineResource,如果緩存中獲取到cache就調用cb.onResourceReady(cached); c)如果緩存中不存在調用loadFromActiveResources從active中獲取,如果獲取到就調用cb.onResourceReady(cached);d)如果active中也不存在,調用EngineJob.start(EngineRunnable), 從而調用decodeFromSource()/decodeFromCache()-->如果是調用decodeFromSource()-->ImageVideoFetcher.loadData()-->HttpUrlFetcher()調用HttpUrlConnection進行網絡請求資源-->得於InputStream()後,調用decodeFromSourceData()-->loadProvider.getSourceDecoder().decode()方法解碼-->GifBitmapWrapperResourceDecoder.decode()-->decodeStream()先從流中讀取2個字節判斷是GIF還是普通圖,若是GIF調用decodeGifWrapper()來解碼,若是普通靜圖則調用decodeBitmapWrapper()來解碼-->bitmapDecoder.decode()

6、Glide使用什麼緩存?

1) 內存緩存:LruResourceCache(memory)+弱引用activeResources

Map<Key, WeakReference<EngineResource<?>>> activeResources正在使用的資源,當acquired變量大於0,說明圖片正在使用,放到activeResources弱引用緩存中,經過release()後,acquired=0,說明圖片不再使用,會把它放進LruResourceCache中

2)磁盤緩存:DiskLruCache,這裏分爲Source(原始圖片)和Result(轉換後的圖片)

第一次獲取圖片,肯定網絡取,然後存active\disk中,再把圖片顯示出來,第二次讀取相同的圖片,並加載到相同大小的imageview中,會先從memory中取,沒有再去active中獲取。如果activity執行到onStop時,圖片被回收,active中的資源會被保存到memory中,active中的資源被回收。當再次加載圖片時,會從memory中取,再放入active中,並將memory中對應的資源回收。

之所以需要activeResources,它是一個隨時可能被回收的資源,memory的強引用頻繁讀寫可能造成內存激增頻繁GC,而造成內存抖動。資源在使用過程中保存在activeResources中,而activeResources是弱引用,隨時被系統回收,不會造成內存過多使用和泄漏。

7、Glide內存緩存如何控制大小?

Glide內存緩存最大空間(maxSize)=每個進程可用最大內存*0.4(低配手機是   每個進程可用最大內存*0.33)

磁盤緩存大小是250MB   int DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024;

(二)網絡和安全機制

  • 網絡框架對比和源碼分析
  • 自己去設計網絡請求框架,怎麼做?
  • okhttp源碼
  • 網絡請求緩存處理,okhttp如何處理網絡緩存的

(1)網絡緩存優先考慮強制緩存,再考慮對比緩存

  • 首先判斷強制緩存中的數據的是否在有效期內。如果在有效期,則直接使用緩存。如果過了有效期,則進入對比緩存。
  • 在對比緩存過程中,判斷ETag是否有變動,如果服務端返回沒有變動,說明資源未改變,使用緩存。如果有變動,判斷Last-Modified。
  • 判斷Last-Modified,如果服務端對比資源的上次修改時間沒有變化,則使用緩存,否則重新請求服務端的數據,並作緩存工作。

(2)okhttp緩存

開啓使用Okhttp的緩存其實很簡單,只需要給OkHttpClient對象設置一個Cache對象即可,創建一個Cache時指定緩存保存的目錄和緩存最大的大小即可。

//新建一個cache,指定目錄爲外部目錄下的okhttp_cache目錄,大小爲100M
Cache cache = new Cache(new File(Environment.getExternalStorageDirectory() + "/okhttp_cache/"), 100 * 1024 * 1024);
//將cache設置到OkHttpClient中,這樣緩存就開始生效了。
OkHttpClient client = new OkHttpClient.Builder().cache(cache).build();

相關的類有:

1)CacheControl( HTTP中的Cache-Control和Pragma緩存控制):指定緩存規則

2)Cache(緩存類)

3)DiskLruCache(文件化的LRU緩存類)

(1)讀取緩存:先獲限OkHttpClient的Cache緩存對象,就是上面創建OkHttpClient設置的Cahce; 傳Request請求到Cache的get方法查找緩存響應數據Response;構造一個緩存策略,再調用它的get去決策使用網絡請求還是緩存響應。若使用緩存,它的cacheResponse不爲空,networkRequest爲空,用緩存構造響應直接返回。若使用請求,則cacheResponse爲空,networkRequest不爲空,開始網絡請求流程。

Cache的get獲取緩存方法,計算request的key值(請求url進行md5加密),根據key值去DisLruCache查找是否存在緩存內容,存則則創建繪存Entry實體。ENTRY_METADATA代表響應頭信息,ENTRY_BODY代表響應體信息。如果緩存存在,在指定目錄下會有兩個文件****.0    *****.1分別存儲某個請求緩存響應頭和響應體信息。

CacheStrategy的get方法:1)若緩存響應爲空或 2)請求是https但緩存響應沒有握手信息;3)請求和緩存響應都是不可緩存的;4)請求是onCache,並且又包含if-Modified-Since或If-None-Match則不使用緩存; 再計算請求有效時間是否符合響應的過期時間,若響應在有效範圍內,則緩存策略使用緩存,否則創建一個新的有條件的請求,返回有條件的緩存策略。

(2)存儲緩存流程:從HttpEngine的readResponse()發送請求開始,判斷hasBody(userResponse),如果緩存的話,maybeCache()緩存響應頭信息,unzip(cacheWritingResponse(storeRequest, userResponse))緩存響應體。

  •  
  • 從網絡加載一個10M的圖片,說下注意事項
  • TCP的3次握手和四次揮手
  • TCP與UDP的區別
  • TCP與UDP的應用
  • HTTP協議
  • HTTP1.0與2.0的區別
  • HTTP報文結構
  • HTTP與HTTPS的區別以及如何實現安全性
  • 如何驗證證書的合法性?
  • https中哪裏用了對稱加密,哪裏用了非對稱加密,對加密算法(如RSA)等是否有了解?
  • client如何確定自己發送的消息被server收到?
  • 談談你對WebSocket的理解
  • WebSocket與socket的區別
  • 談談你對安卓簽名的理解。
  • 請解釋安卓爲啥要加簽名機制?
  • 視頻加密傳輸
  • App 是如何沙箱化,爲什麼要這麼做?
  • 權限管理系統(底層的權限是如何進行 grant 的)?

(三)數據庫

  • sqlite升級,增加字段的語句
  • 數據庫框架對比和源碼分析
  • 數據庫的優化
  • 數據庫數據遷移問題

(四)算法

  • 排序算法有哪些?
  • 最快的排序算法是哪個?
  • 手寫一個冒泡排序
  • 手寫快速排序代碼
  • 快速排序的過程、時間複雜度、空間複雜度
  • 手寫堆排序
  • 堆排序過程、時間複雜度及空間複雜度
  • 寫出你所知道的排序算法及時空複雜度,穩定性
  • 二叉樹給出根節點和目標節點,找出從根節點到目標節點的路徑
  • 給阿里2萬多名員工按年齡排序應該選擇哪個算法?
  • GC算法(各種算法的優缺點以及應用場景)
  • 蟻羣算法與蒙特卡洛算法
  • 子串包含問題(KMP 算法)寫代碼實現
  • 一個無序,不重複數組,輸出N個元素,使得N個元素的和相加爲M,給出時間複雜度、空間複雜度。手寫算法
  • 萬億級別的兩個URL文件A和B,如何求出A和B的差集C(提示:Bit映射->hash分組->多文件讀寫效率->磁盤尋址以及應用層面對尋址的優化)
  • 百度POI中如何試下查找最近的商家功能(提示:座標鏡像+R樹)。
  • 兩個不重複的數組集合中,求共同的元素。
  • 兩個不重複的數組集合中,這兩個集合都是海量數據,內存中放不下,怎麼求共同的元素?
  • 一個文件中有100萬個整數,由空格分開,在程序中判斷用戶輸入的整數是否在此文件中。說出最優的方法
  • 一張Bitmap所佔內存以及內存佔用的計算

一張圖片(bitmap)佔用的內存影響因素:圖片原始長、寬,手機屏幕密度,圖片存放路徑下的密度,單位像素佔用字節數

bitmapSize=圖片長度*(inTargetDensity手機的density / inDensity圖片存放目錄的density)*寬度*(手機的inTargetDensity / inDensity目標存放目錄的density)*單位像素佔用的字節數(圖片長寬單位是像素)

1)圖片長寬單位是像素:單位像素字節數由其參數BitmapFactory.Options.inPreferredConfig變量決定,它是Bitmap.Config類型,包括以下幾種值:ALPHA_8圖片只有alpha值,佔用一個字節;ARGB_4444 一個像素佔用2個字節,A\R\G\B各佔4bits;ARGB_8888一個像素佔用4個字節,A\R\G\B各佔8bits(高質量圖片格式,bitmap默認格式);ARGB_565一個像素佔用2字節,不支持透明和半透明,R佔5bit, Green佔6bit, Blue佔用5bit. 從Android4.0開始該項無效。

2) inTargetDensity 手機的屏幕密度(跟手機分辨率有關係)

inDensity原始資源密度(mdpi:160;   hdpi:240;   xhdpi:320;   xxhdpi:480; xxxhdpi:640)

 

當Bitmap對象在不使用時,應該先調用recycle(),再將它設置爲null,雖然Bitmap在被回收時可通過BitmapFinalizer來回收內存。但只有系統垃圾回收時纔會回收。Android4.0之前,Bitmap內存分配在Native堆中,Android4.0開始,Bitmap的內存分配在dalvik堆中,即Java堆中,調用recycle()並不能立即釋放Native內存。

  • 2000萬個整數,找出第五十大的數字?
  • 燒一根不均勻的繩,從頭燒到尾總共需要1個小時。現在有若干條材質相同的繩子,問如何用燒繩的方法來計時一個小時十五分鐘呢?
  • 求1000以內的水仙花數以及40億以內的水仙花數
  • 5枚硬幣,2正3反如何劃分爲兩堆然後通過翻轉讓兩堆中正面向上的硬8幣和反面向上的硬幣個數相同
  • 時針走一圈,時針分針重合幾次
  • N*N的方格紙,裏面有多少個正方形
  • x個蘋果,一天只能喫一個、兩個、或者三個,問多少天可以喫完?

(五)插件化、模塊化、組件化、熱修復、增量更新、Gradle

  • 對熱修復和插件化的理解
  • 插件化原理分析
  • 模塊化實現(好處,原因)
  • 熱修復,插件化
  • 項目組件化的理解
  • 描述清點擊 Android Studio 的 build 按鈕後發生了什麼

(六)架構設計和設計模式

  • 談談你對Android設計模式的理解
  • MVC MVP MVVM原理和區別
  • 你所知道的設計模式有哪些?
  • 項目中常用的設計模式
  • 手寫生產者/消費者模式
  • 寫出觀察者模式的代碼
  • 適配器模式,裝飾者模式,外觀模式的異同?
  • 用到的一些開源框架,介紹一個看過源碼的,內部實現過程。
  • 談談對RxJava的理解

RxJava是基於響應式編程,基於事件流、實現異步操(類似於Android中的AsyncTask、Handler作用)作的庫,基於事件流的鏈式調用,使得RxJava邏輯簡潔、使用簡單。RxJava原理是基於一種擴展的觀察者模式,有四種角色:被觀察者Observable 觀察者Observer 訂閱subscribe 事件Event。RxJava原理可總結爲:被觀察者Observable通過訂閱(subscribe)按順序發送事件(Emitter)給觀察者(Observer), 觀察者按順序接收事件&作出相應的響應動作。

RxJava中的操作符:

1)defer():直到有觀察者(Observer)訂閱時,纔會動態創建被觀察者對象(Observer)&發送事件,通過Observer工廠方法創建被觀察者對象,每次訂閱後,都會得到一個剛創建的最新的Observer對象,可以確保Observer對象裏的數據是最新的。defer()方法只會定義Observable對象,只有訂閱操作纔會創建對象。

Observable<T> observable = Observable.defer(new Callable<ObservableSource<? extends T>>() {
    @Override
    public ObservableSource<? extends T> call() throws Exception {
        return Observable.just();
    }
}

2)timer() 快速創建一個被觀察者(Observable),延遲指定時間後,再發送事件

Observable.timer(2, TimeUnit.SECONDS)//也可以自定義線程timer(long, TimeUnit, Scheduler)
    .subscribe(new Observer<Long>() {
        @Override
        public void onSubscribe(Disposable d) {
        }
        ...

     });

3) interval() intervalRange() 快速創建一個被觀察者對象(Observable),每隔指定時間就發送事件

//interval三個參數,參數1:第一次延遲時間  參數2:間隔時間數字   參數3:時間單位
Observable.interval(3, 1, TimeUnit.SECONDS).subscribe();
//intervalRange五個參數,參數1:事件序列起始點  參數2:事件數量  參數3:第一次延遲時間 參數4:間隔時間數字   參數5:時間單位
Observable.intervalRange(3, 10, 2, 1, TimeUnit.SECONDS).subscribe();
  • RxJava的功能與原理實現

Rxjava發送事件步驟:

1)創建被觀察者對象Observable&定義需要發送的事件

Observable.create(new ObservableOnSubscribe<T>(){
    @Override
    public void subscribe(ObservableEmitter<T> emitter) throws Exception {
        //定義發送事件的行爲
    }
});

Observable.create()方法實際創建了一個ObservableCreate對象,它是Observable的子類,傳入一個ObservableOnSubscribe對象,複寫了發送事件行爲的subscribe()方法。

2)創建觀察者對象Observer&定義響應事件的行爲

Observer observer = new Observer<T>() {

    @Override
    public void onSubscribe(Disposable d){//Disposable對象可用於結束事件
        //默認最先調用
    }
    
    @Override
    public void onNext(T t){
    
    }

    @Override
    public void onError(Throwable d){
    
    }

    @Override
    public void onComplete(){
    
    }

}

3)通過subscribe()方法使觀察者訂閱被觀察者

Observable.subscribe(Observer observer);//實際調用的是ObservableCreate.subscribeActual()方法,具體實現如下

protected void subscribeActual(Observer<? super T> observer) {

              // 1. 創建1個CreateEmitter對象用於發射事件(封裝成1個Disposable對象)
            CreateEmitter<T> parent = new CreateEmitter<T>(observer);
            // 2. 調用觀察者(Observer)的onSubscribe()
            observer.onSubscribe(parent);
            try {
                // 3. 調用source對象的(ObservableOnSubscribe對象)subscribe()
                source.subscribe(parent);
            } catch (Throwable ex) {
                Exceptions.throwIfFatal(ex);
                parent.onError(ex);
            }
    }

 

  • RxJava的作用,與平時使用的異步操作來比的優缺點
  • 說說EventBus作用,實現方式,代替EventBus的方式
  • 從0設計一款App整體架構,如何去做?
  • 說一款你認爲當前比較火的應用並設計(比如:直播APP,P2P金融,小視頻等)
  • 談談對java狀態機理解
  • Fragment如果在Adapter中使用應該如何解耦?
  • Binder機制及底層實現
  • 對於應用更新這塊是如何做的?(解答:灰度,強制更新,分區域更新)?
  • 實現一個Json解析器(可以通過正則提高速度)
  • 統計啓動時長,標準

(七)性能優化

  • 如何對Android 應用進行性能分析以及優化?
  • ddms 和 traceView
  • 性能優化如何分析systrace?
  • 用IDE如何分析內存泄漏?
  • Java多線程引發的性能問題,怎麼解決?
  • 啓動頁白屏及黑屏解決?
  • 啓動太慢怎麼解決?

在冷啓動系統要執行三個任務:加載和啓動應用程序;在app啓動後立即顯示一個空白窗體;創建APP進程。

應用啓動後會執行:創建應用程序對象;啓動主線程;創建Main Activity;初始化構造View;在屏幕上佈局;執行初始化繪製操作;

應用啓動,空白窗口會一直存在直到系統完成了應用的首次繪製操作,此時,系統會替換啓動窗口,讓用戶能和APP進行交互。

對於熱啓動,如果應用的Activity駐留在內存中,應用就可避免重複進行對象初始化。如果系統執行了內存回收並觸發GC,如onTrimMemory(),熱啓動時對象仍需重建,這樣系統進程也會一直顯示白屏直到應用完成Activity的渲染。

測量應用啓動時間:1) 可通過logcat中查看Displayed中顯示啓動類耗時; 2) 通過adb shell am start -S -W 包名/啓動類全限定名,-S表示重啓當前應用,命令進行檢測啓動app的耗時。3) 使用reportFullyDrawn()方法來測量應用啓動到所有資源和視圖層次結構完整顯示之間的所經過的時間。

優化方法:1)減少view的層級,減少複用和佈局嵌套使佈局扁平化,不要加載對用戶不可見的佈局,如使用ViewStub;

2)將需要在主線程中初始化但可不立即完成的延遲加載,部分組件放到子線程中初始化。

3)減少Application.onCreate和啓動頁和第一個界面onCreate中方法的耗時操作。

4)設置閃屏頁,將閃屏頁設置爲啓動頁的activity窗口背景windowBackground屬性,爲啓動屏幕提供一個背景,

  • 怎麼保證應用啓動不卡頓?

應用卡頓的主要原因有:速度曲線不夠流暢,掉幀、觸摸響應速度

Android顯示機制:app->SurfaceFlinger->Display

開發者選項->GPU呈現模式分析->在屏幕上顯示爲條形圖,可以在屏幕上看到顯示每幀繪製花費的時間,有條基準線是16ms,超過這條基線很可能出現掉幀。如果藍線很長,則說明軟件draw太費時,可通過traceview來繼續分析draw的java代碼。如果中間紅色部分很長則說明是openGL ES繪製太費時,用gltrace來分析OpernGL ES調用過程。用systrace分析,用traceview來看代碼,

  • App啓動崩潰異常捕捉
  • 自定義View注意事項
  • 現在下載速度很慢,試從網絡協議的角度分析原因,並優化(提示:網絡的5層都可以涉及)。
  • Https請求慢的解決辦法(提示:DNS,攜帶數據,直接訪問IP)
  • 如何保持應用的穩定性

1)需求明確清楚,編碼時明確知道要實現的功能和實現方法,技術選型等,對一些庫進行封裝再使用。防止代碼冗餘、避免多線程導致的問題;

2)異常崩潰處理捕獲,在使用對象前做判空處理等

3)提高編碼質量,用lint\findbugs進行代碼靜態分析;

4)OOM和內存泄漏檢測

5)框架測試,兼容性測試、單元測試、monkey測試

6)發佈維度,灰度,選擇部分渠道進行發佈,收集問題;

7)熱更新。

  • RecyclerView和ListView的性能對比
  • ListView的優化
  • RecycleView優化
  • View渲染
  • Bitmap如何處理大圖,如一張30M的大圖,如何預防OOM

 

  • java中的四種引用的區別以及使用場景
  • 強引用置爲null,會不會被回收?

如果對象沒有被GC Roots引用到,會被GC回收到,但什麼時候回收需根據JVM的特性什麼時候觸發GC操作。即使調用了System.gc() JVM也不一定會觸發GC。

(八)NDK、jni、Binder、AIDL、進程通信有關

  • 請介紹一下NDK
  • 什麼是NDK庫?
  • jni用過嗎?
  • 如何在jni中註冊native函數,有幾種註冊方式?
  • Java如何調用c、c++語言?
  • jni如何調用java層代碼?

1)Java類中要調用jni方法,需要在java類中聲明本地方法,public native void methodName();//本地方法。還要在類的靜態代碼塊中導入so庫 static { System.loadLibrary("MyJni");}

2)在C/C++獲取類的對象的方法有兩種:

a)通過c/c++創建java對象,通過對象獲取類,通過類獲取類的構造方法的ID,基於方法ID和類,創建新對象。

JNIEXPORT void JNICALL JAVA_nativeMethod(JNIEnv *env, jobject this, jint i) {
    jclass clazz = (*env).GetObjectClass(thiz);
    jmethodID mid = (*env).GetMethodID(clazz, "<init>","()V");
    jobject obj = (*env).NewObject(clazz, mid);
}

b) 通過c/c++創建不同類對象,通過FindClass方法獲取需要的類;通過類獲取類的構造方法的ID,基於方法ID和類,創建對象

JNIEXPORT void JNICALL JAVA_nativeMethod(JNIEnv *env, jobject this, jint i) {
    jclass clazz = (*env).FindClass("com/packagepath/className");
    jmethodID mid = (*env).GetMethodID(clazz, "<init>","()V");
    jobject obj = (*env).NewObject(clazz, mid);
}

調用java方法跟上面調用構造函數類似,獲取類的方法ID,基於對象的方法id調用Java方法

JNIEXPORT void JNICALL JAVA_nativeMethod(JNIEnv *env, jobject thiz, jint i) {
    jclass clazz = (*env).GetObjectClass(thiz);
    m_Object = (*env).NewGlobalRef(thiz);
    m_mid = (*env).GetMethodID(clazz, "methodName", "()V");//獲取Java方法的ID
    m_fid = (*env).GetFieldID(clazz, "a","I");//獲取Java變量的ID
    (*env).SetIntField(m_Object, m_fid, i);
    (*env).CallVoidMethod(m_Object, m_mid);
}
  • 進程間通信的方式?
  • Binder機制
  • 簡述IPC?
  • 什麼是AIDL?
  • AIDL解決了什麼問題?
  • AIDL如何使用?
  • Android 上的 Inter-Process-Communication 跨進程通信時如何工作的?
  • 多進程場景遇見過麼?
  • Android進程分類?

前臺進行(當前正在前臺運行的進程,說明用戶當前正在與該進程交互), 滿足以下至少一個條件的叫做 foreground progcess:
  a.有一個Activity在前臺獲得焦點可與用戶互動
  b.有一個 BroadcastReceiver組件正在運行onReceive()方法
  c.有一個Sevice組件正在運行onCreate()/onStart()/onDestory()方法

可見進程(可見,但用戶不能直接與之交互)滿足以下條件之一稱爲可見進程:a.有一個Activity能被用戶看見但是失去焦點(處於onPause()狀態) b.有一個 Service調用了startForeground()方法 c.綁定了一個Service,系統將該Service作爲一個特殊的用戶知道的功能使用如自動更換壁紙,輸入法服務等。

 

服務進程(擁有service的進程,一般在後臺爲用戶服務的),通過startService()方法開啓的沒有綁定在activity上的Service的進程,Service長時間運行 (超過30分鐘以上)會被降級到cached process

後臺進程(對用戶作用不大,缺少該進程一般不會影響用戶對系統的體驗)

空進程(一般作爲緩存機制服務的)

  • 進程和 Application 的生命週期?
  • 進程調度
  • 談談對進程共享和線程安全的認識

Android進程共享可通過共享用戶ID來實現,

對於SharedPreferences想實現多進程共享需要設置MODE_MULTI_PROCESS,設置了這個Flag後,每次調用Context.getSharedPreferences時系統會重新從SP

SharedPreferences myPrefs = context.getSharedPreferences(MY_FILE_NAME, Context.MODE_MULTI_PROCESS | Context.MODE_PRIVATE);
  • 談談對多進程開發的理解以及多進程應用場景
  • 什麼是協程?

(九)framework層、ROM定製、Ubuntu、Linux之類的問題

  • java虛擬機的特性
  • 談談對jvm的理解
  • JVM內存區域,開線程影響哪塊內存
  • 對Dalvik、ART虛擬機有什麼瞭解?
  • Art和Dalvik對比
  • 虛擬機原理,如何自己設計一個虛擬機(內存管理,類加載,雙親委派)
  • 談談你對雙親委派模型理解
  • JVM內存模型,內存區域
  • 類加載機制
  • 談談對ClassLoader(類加載器)的理解
  • 談談對動態加載(OSGI)的理解
  • 內存對象的循環引用及避免
  • 內存回收機制、GC回收策略、GC原理時機以及GC對象
  • 垃圾回收機制與調用System.gc()區別

System.gc()只是通知垃圾回收器要進行垃圾回收操作,但並沒有立即執行垃圾回收。它只是建議JVM安排GC運行,還有可能被拒絕。

  • Ubuntu編譯安卓系統
  • 系統啓動流程是什麼?(提示:Zygote進程 –> SystemServer進程 –> 各種系統服務 –> 應用進程)
  • 大體說清一個應用程序安裝到手機上時發生了什麼
  • 簡述Activity啓動全部過程

1)Activity.startActivity-->startActivityForResult()

2)-->Instrumentation.execStartActivity()-->execStartActivity()

3)ActivityManager.getService().startActivity()通過Binder到ActivityManagerService.startActivity()

4)-->ActivityStarter.startActivityMayWait()-->startActivityLocked()-->startActivityUnchecked()

5)--ActivityStackSupervisor.resumeFocusedStackTopActivityLocked()-->ApplicationThread$scheduleLaunchActivity

6)ApplicationThread.schedulelaunchActivity()通過ActivityThread.sendMessage,再處理消息,進入handleLaunchActivity

-->Instrumentation$newActivity創建Activity的實例,使用類加載器創建Activity對象。

-->makeApplication創建Application對象,調用它的Application.onCreate()方法

 

Instrumentation這個類就是完成對Application和Activity初始化和生命週期的工具類。

  • App啓動流程,從點擊桌面開始

1)點擊桌面App圖標,Launcher進程採用Binder IPC向system_server進程發送startActivity()

2)system_server進程向Zygote發送創建進程的請求(AMS中通過startActivity()方法,調用startProcessLocked()函數),Zygote通過socket通信的方式讓Zygote進程 fork一個新進程出來作爲App進程;

3)App進程通過Binder IPC向system_server進程發起attachApplication請求(ActivityThread的main()函數裏會創建Application,還調用ActivityStackSupervisor.attachApplicationLocked);

4)system_server進程收到請求後,進行一系列準備工作,再通過binder IPC向APP進程發送scheduleLaunchActivity請求;

5)App進程的binder線程(ApplicationThread)收到請求後,通過handler向主線程發送LAUNCHER_ACTIVITY消息;

6)主線程收到Message消息後,通過反射機制創建出Activity並回調Activity.onCreate()等方法;

7)App正式啓動,開始進入Activity的生命週期。

  • 邏輯地址與物理地址,爲什麼使用邏輯地址?
  • Android爲每個應用程序分配的內存大小是多少?

根據應用實際使用情況分配,初始給進程分配爲8M,應用最大可分配的可能是64M\128M\256M等

  • Android中進程內存的分配,能不能自己分配定額內存?

進程內存分配跟手機配置有關,不同手機可能不一樣,有64M\128M\256M等,heapgrowthlimit是一個普通應用的內存限制,可通過ActivityManager.getLargeMemoryClass()獲得,在mainfest中設置了largeHeap=true後,可以使應用可用內存增大到原來兩倍。並不能自己定額分配內存,android系統是根據應用所需要內存大小,先分配初始大小heapstartsize,當應用申請更多內存,系統會再進行分配,但不得超過最大內存,超過了會報OOM。

  • 進程保活的方式

1)模擬前臺進程,startForeground()將當前進程僞裝成前臺進程,將進程優先級提高到最高,現在前臺進程會顯示在通知欄中,取消不掉。

2)JobScheduler機制喚醒,系統會根據自己實現定時去調用改接口傳遞進程去實現一些操作,且這個接口被強制停止後仍能正常啓動。在調用JobSchedule.schedule()來啓動任務。

3)實現Service類時,將onStartCommand()返回值設置爲START_STICKY,利用系統機制在service掛掉後自動拉活,但這種方式只適合原生系統,像小米、華爲等定製化比較高的第三方廠商,這些都限制了。

4)一像素的Activity,

5)應用之間相互喚醒。

  • 如何保證一個後臺服務不被殺死?(相同問題:如何保證service在後臺不被kill?)比較省電的方式是什麼?
  • App中喚醒其他進程的實現方式

1)啓動其他進程的Activity\Service或是發送一條廣播給相應的應用(該應用得靜態註冊此廣播)

 

 

OOM定位與分析,如何定位哪塊原因導致應用最終發生OOM?

OOM發生後,可以用Android Studio自帶的Android Monitor dump出HPROF文件,再用SDK中hprof-conv工具轉換爲標準的Java堆轉儲文件格式,再用MAT繼續分析。切換到histogram視圖,按shadow heap降序排序,對實例對象佔用內存大小排序,再查看實例到GC ROOT的路徑。

一般可能導致的如圖片:直接加載超大尺寸圖片(對圖片尺寸縮放預處理後再加載)、圖片加載後未及時釋放(利用LRU機制保證圖片個數和佔用內存);在頁面中,加載非常多的圖片(避免同時加載大量圖片)

 

JNI層的crash如何捕獲?

參考:JNI定位c++錯誤

通過ndk安裝包中的addr2line objdump  ndk-stack工具進行分析crash,ndk針對不同的CPU架構實現了多套相同的工具,在選擇add2line objdump時要根據目標機器CPU架構來選擇。

一般JNI發生異常,會出現一個Fatal signal信號量,大概知道是哪個函數引起的,再看下面的backtrace日誌,backtrace是JNI調用堆棧信息,以“#兩位數字 pc”開頭,找到對應的函數,再用addr2line進行定位出錯的位置。addr2line -C -f -e ./obj/armeabi/xxx.so 000eea70

使用ndk-stack協助我們獲取底層崩潰的堆棧信息,adb logcat | ndk-stack -sym ./obj/armeabi/xxx.so

應用卡頓定位

1)使用UI線程的Looper打印日誌

Android主線程更新UI,如果1S鍾刷新少於60次,即FPS小於60,一幀加載超過16.67ms的話,用戶就會產生卡頓的感覺。Android使用消息機制進行UI更新,UI線程中有個Looper,其loop方法會不斷提取message,調用其綁定的Handler在UI線程執行。如果handler.dispatchMessage方法中有耗時操作,就會發生卡頓。如果有卡頓,就打印出UI線程的堆棧信息。

優點:用戶使用app就可以監控卡頓情況,但因需另開子線程獲取堆棧信息,會消耗系統資源。

2)使用Choreographer.FrameCallback監控卡頓

Android系統每16ms會發出SYNC信息,通知界面重繪、渲染,每一次同步的週期爲16.6ms,代表一幀的刷新頻率可以在兩次回調時間週期來判斷是否發生卡頓。(Android4.1以上才支持)。可以通過Choreographer.FrameCallback回調doFrame(long)函數,如果兩次doFrame之間間隔大於16.67ms說明發生了卡頓。這種方法從app層面來監控卡頓,同時可實時計算幀率和掉幀數,實時監測APP頁面的幀率數據,一旦發現幀率過低,可自動保存現場堆棧信息。

卡頓監控系統處理流程:開發修復-》用戶上報(後臺配置下灰度0.2%的用戶量進行卡頓監控和上報,每個用戶一天上報一次,上報後刪除文件不影響存儲空間)-》後臺解析(過濾、去重、分類、反解堆棧、入庫)-》平臺展示-》自動提單

 

 

Http2.0有關多路複用:

多路複用原理:HTTP2流和多路複用   HTTP2.0原理詳細解析

原理是二進制分幀層+流實現了多路複用,OKHttp是怎麼支持的呢,那就是讀幀,讀流,Okhttp對http2的支持簡單分析

 

ViewStub是怎麼實現延時加載的?

ViewStub是一個不可見、大小爲0的View。具體體現在ViewStub的構造函數中會進行設置setVisibility(GONE)設置控件不可見,同時會設置setWillNotDraw(true),即本View不會調用onDraw()方法繪製內容。在它的onMeasure函數中會調用setMeasureDimenssion(0, 0)即不會測量視圖,直接設置一個大小爲0的View. 

對於ViewStub.inflate()機制是:1)調用LayoutInflate.flate(mLayoutResource, parent, false)來加載ViewStub中android:layout設置的佈局view(最後一個參數false是代表attachToRoot設置成false,說明忽略android:layout中佈局根節點的layoutParams參數);2)從父視圖中獲取當前ViewStub在父視圖中位置(在加載父視圖時會用一個佔位符來代表ViewStub);3)將當前ViewStub從parent中移除;4)將android:layout中的佈局view add到父視圖中,如果StubView中設置了layoutParams屬性就會用ViewStub中設置的。

應用場景:如網絡加載失敗頁面,評論區域內的listView(當沒有評論或是請求失敗的時候不加載)

加載方式:findViewById(R.id.stubViewId).setVisibility(View.VISIBLE);或是((ViewStub)findViewById(R.id.StubViewId)).inflate();其實設置Visibility最終也是調用inflate來加載佈局的。

如果ViewStub標籤下寫上width/height,在ViewStub相應的layout xml文件中也進行了寬高定義,會以誰爲準?

其實上面也分析過了,會以ViewStub中設置的layoutParams爲準。

ViewStub可不可以加載多次呢?

不能,上面也分析過了,ViewStub調用inflate()方法後,會把自己從父視圖中移除掉,並把自身所包含的內容添加到父視圖中,再重新加載,找不到在父視圖了,就會拋出ViewStub must have a non-null ViewGroup viewparent。

merge標籤

merge一般可以和include/ViewStub結合使用,當include/ViewStub從外部導入xml結構時,可以將被導入的xml用merge作爲根節點表示,當被加載到父佈局中可以將它們融合到父級結構中,而不會出現冗餘的節點。因爲它是直接將其中的子元素添加到merge標籤的parent中,這樣就保證不會引入額外的層級了。

注意:1)<merge />只可以作爲xml layout的根節點;2)當需要擴展的xml layout本身的根節點也是merge時,需要將被導入的xml layout置於ViewGroup中,且設置attachToRoot爲true。

 

 

 

 

 

 

 

 

 

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