前言
嗨,大家好,好久不見。這裡跟大家侃侃
這中間發生了什麼。
一個月前呢,想準備面試,就網上隨便找找面試題
什麼的,發現要麼就是賣課的,要麼就是不給詳細回答的或者回答不夠深的(也許是我沒找到?)。反正稍微有點苦惱,因為我畢竟是個懶人,就想看看面試題,然後自己思考下,順便看看一些參考回答,看看自己回答的全不全面
等等。
於是,我就想幹脆我自己做這個事
吧,就算沒人看,也當我自己每天覆習下了。於是,我就建了一個小小公眾號
(小到確實沒人看,哈哈哈),每天去找一些大廠的面試真題
,然後解答下
,然後自己確實也在這個過程中能複習到不少以前沒有重視的問題,今天就總結下之前一個多月總結的面試題,難度不大,大佬可以直接路過,當然發發善心點個贊
也是可以的❤️。
進入正題,下面為10月刊內容
,每三個問題為一個小節,也就是一個專題文章,我就不具體區分了,由於字數問題,也只節選了一些問題,大家見諒。另外答的不好的地方大家也可以留言敲敲我
,感謝。
10月刊內容
網頁中輸入url,到渲染整個介面的整個過程,以及中間用了什麼協議?
1)過程分析:主要分為三步
DNS解析
。使用者輸入url後,需要通過DNS解析找到域名對應的ip地址,有了ip地址才能找到伺服器端。首先會查詢瀏覽器快取,是否有對應的dns記錄。再繼續按照作業系統快取—路由快取—isp的dns伺服器—根伺服器的順序進行DNS解析,直到找到對應的ip地址。客戶端(瀏覽器)和伺服器互動
。瀏覽器根據解析到的ip地址和埠號發起HTTP請求,請求到達傳輸層,這裡也就是TCP層,開始三次握手建立連線。伺服器收到請求後,傳送相應報文給客戶端(瀏覽器),客戶端收到相應報文並進行解析,得到html頁面資料,包括html,js,css等。客戶端(瀏覽器)解析html資料
,構建DOM樹,再構造呈現樹(render樹),最終繪製到瀏覽器頁面上。
2)其中涉及到TCP/IP協議簇,包括DNS,TCP,IP,HTTP協議等等。
具體介紹下TCP/IP
TCP/IP一般指的是TCP/IP協議簇,主要包括了多個不同網路間實現資訊傳輸涉及到的各種協議
主要包括以下幾層:
應用層
:主要提供資料和服務。比如HTTP,FTP,DNS等傳輸層
:負責資料的組裝,分塊。比如TCP,UDP等網路層
:負責告訴通訊的目的地,比如IP等資料鏈路層
:負責連線網路的硬體部分,比如乙太網,WIFI等
TCP的三次握手和四次揮手,為什麼不是兩次握手?為什麼揮手多一次呢?
客戶端簡稱A,伺服器端簡稱B
1)TCP建立連線需要三次握手
- A向B表示想跟B進行連線(A傳送
syn
包,A進入SYN_SENT
狀態) - B收到訊息,表示我也準備好和你連線了(B收到
syn
包,需要確認syn
包,並且自己也傳送一個syn
包,即傳送了syn+ack
包,B進入SYN_RECV
狀態) - A收到訊息,並告訴B表示我收到你也準備連線的訊號了(A收到
syn+ack
包,向伺服器傳送確認包ack
,AB進入established
狀態)開始連線。
2)TCP斷開連線需要四次揮手
- A向B表示想跟B斷開連線(A傳送
fin
,進入FIN_WAIT_1
狀態) - B收到訊息,但是B訊息沒傳送完,只能告訴A我收到你的斷開連線訊息(B收到fin,傳送ack,進入
CLOSE_WAIT
狀態) - 過一會,B資料傳送完畢,告訴A,我可以跟你斷開了(B傳送fin,進入
LAST_ACK
狀態) - A收到訊息,告訴B,可以他斷開(A收到fin,傳送ack,B進入
close
d狀態)
3)為什麼揮手多一次
其實正常的斷開和連線都是需要四次
:
- A發訊息給B
- B反饋給A表示正確收到訊息
- B傳送訊息給A
- A反饋給B表示正確收到訊息。
但是連線中,第二步和第三步是可以合併
的,因為連線之前A和B是無聯絡的,所以沒有其他情況需要處理。而斷開的話,因為之前兩端是正常連線狀態,所以第二步的時候不能保證B之前的訊息已經傳送完畢,所以不能馬上告訴A要斷開的訊息。這就是連線為什麼可以少一步的原因。
4)為什麼連線需要三次,而不是兩次。
正常來說,我給你發訊息,你告訴我能收到,不就代表我們之前通訊是正常的嗎?
- 簡單回答就是,
TCP是雙向通訊協議
,如果兩次握手,不能保證B發給A的訊息正確到達。
TCP 協議為了實現可靠傳輸, 通訊雙方需要判斷自己已經傳送的資料包是否都被接收方收到, 如果沒收到, 就需要重發。
TCP是怎麼保證可靠傳輸的?
序列號和確認號
。比如連線的一方傳送一段80byte資料,會帶上一個序列號,比如101。接收方收到資料,回覆確認號181(180+1),這樣下一次傳送訊息就會從181開始傳送了。
所以握手過程中,比如A傳送syn訊號給B,初始序列號為120,那麼B收到訊息,回覆ack
訊息,序列號為120+1。同時B傳送syn
訊號給A,初始序列號為256,如果收不到A的回覆訊息,就會重發,否則丟失這個序列號,就無法正常完成後面的通訊了。
這就是三次握手的原因。
TCP和UDP的區別?
TCP
提供的是面向連線,可靠的位元組流服務。即客戶和伺服器交換資料前,必須現在雙方之間建立一個TCP連線(三次握手),之後才能傳輸資料。並且提供超時重發,丟棄重複資料,檢驗資料,流量控制等功能,保證資料能從一端傳到另一端。
UDP
是一個簡單的面向資料包的運輸層協議。它不提供可靠性,只是把應用程式傳給IP層的資料包傳送出去,但是不能保證它們能到達目的地。由於UDP
在傳輸資料包前不用再客戶和伺服器之間建立一個連線,且沒有超時重發等機制,所以傳輸速度很快。
所以總結下來就是:
- TCP 是面向連線的,UDP 是面向無連線的
- TCP資料包頭包括序列號,確認號,等等。相比之下UDP程式結構較簡單。
- TCP 是面向位元組流的,UDP 是基於資料包的
- TCP 保證資料正確性,UDP 可能丟包
- TCP 保證資料順序,UDP 不保證
可以看到TCP
適用於穩定的應用場景,他會保證資料的正確性和順序,所以一般的瀏覽網頁,介面訪問都使用的是TCP
傳輸,所以才會有三次握手
保證連線的穩定性。
而UDP是一種結構簡單的協議,不會考慮丟包啊,建立連線等。優點在於資料傳輸很快,所以適用於直播,遊戲等場景。
HTTP的幾種請求方法具體介紹
常見的有四種:
GET
獲取資源,沒有body,冪等性POST
增加或者修改資源,有bodyPUT
修改資源,有body,冪等性DELETE
刪除資源,冪等性
HTTP請求和響應報文的格式,以及常用狀態碼
1)請求報文:
//請求行(包括method、path、HTTP版本)
GET /s HTTP/1.1
//Headers
Host: www.baidu.com
Content-Type: text/plain
//Body
搜尋****
2)響應報文
//狀態行 (包括HTTP版本、狀態碼,狀態資訊)
HTTP/1.1 200 OK
//Headers
Content-Type: application/json; charset=utf-8
//Body
[{"info":"xixi"}]
3)常用狀態碼
主要分為五種型別:
1開頭
, 代表臨時性訊息,比如100(繼續傳送)2開頭
, 代表請求成功,比如200(OK)3開頭
, 代表重定向,比如304(內容無改變)4開頭
, 代表客戶端的一些錯誤,比如403(禁止訪問)5開頭
, 代表伺服器的一些錯誤,比如500
介紹對稱加密和非對稱加密
1)對稱加密,即加密和解密演算法不同,但是金鑰相同。比如DES,AES
演算法。
資料A --> 演算法D(金鑰S)--> 加密資料B
加密資料B --> 演算法E(金鑰S)--> 資料A
優點:
缺點:金鑰有可能被破解,容易被偽造。傳輸過程中一旦金鑰被其他人獲知則可以進行資料解密。
2)非對稱加密,即加密和解密演算法相同,但是金鑰不同。私鑰自己儲存,公鑰提供給對方。比如RSA,DSA
演算法。
資料A --> 演算法D(公鑰)--> 加密資料B
加密資料B --> 演算法D(私鑰)--> 資料A
優點:安全,公鑰即使被其他人獲知,也無法解密資料。
缺點:需要通訊雙方都有一套公鑰和私鑰
數字簽名的原理
1)首先,為什麼需要數字簽名?
防止被攻擊,被偽造
。由於公鑰是公開的,別人截獲到公鑰就能偽造資料進行傳輸,所以我們需要驗證資料的來源。
2)怎麼簽名?
由於公鑰能解密 私鑰加密的資料,所以私鑰也能解密 公鑰加密的資料。(上圖非對稱加密A和B代號互換即可)
所以我們用公鑰進行加密後,再用私鑰進行一次加密,那麼私鑰的這次加密就叫簽名
,也就是隻有我自己可以進行加密的操作。所以傳輸資料流程就變成了加密資料和簽名資料
,如果解出來都是同樣的資料,那麼則資料安全可靠
。
資料A --> 演算法D(公鑰)--> 加密資料B
資料A --> 演算法D(私鑰)--> 簽名資料C
加密資料B --> 演算法D(私鑰)--> 資料A
簽名資料C --> 演算法D(公鑰)--> 資料A
Base64演算法是什麼,是加密演算法嗎?
-
Base64
是一種將二進位制資料轉換成64種字元組成的字串的編碼演算法,主要用於非文字資料的傳輸,比如圖片。可以將圖片這種二進位制資料轉換成具體的字串,進行儲存和傳輸。 -
嚴格來說,不算。雖然它確實把一段二進位制資料轉換成另外一段資料,但是他的加密和解密是公開的,也就無祕密可言了。所以我更傾向於認為它是一種編碼,每個人都可以用base64對二進位制資料進行編碼和解碼。
-
面試加分項
:為了減少混淆,方便複製,減少資料長度,就衍生出一種base58編碼。去掉了base64中一些容易混淆的數字和字母(數字0,字母O,字母I,數字1,符號+,符號/)
大名鼎鼎的比特幣就是用的改進後的base58編碼,即Base58Check
編碼方式,有了校驗機制,加入了hash值。
為什麼多執行緒同時訪問(讀寫)同個變數,會有併發問題?
- Java 記憶體模型規定了所有的變數都儲存在主記憶體中,每條執行緒有自己的工作記憶體。
- 執行緒的工作記憶體中儲存了該執行緒中用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體。
- 執行緒訪問一個變數,首先將變數從主記憶體拷貝到工作記憶體,對變數的寫操作,不會馬上同步到主記憶體。
- 不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數的傳遞均需要自己的工作記憶體和主存之間進行資料同步。
說說原子性,可見性,有序性分別是什麼意思?
-
原子性:在一個操作中,CPU 不可以在中途暫停然後再排程,即不被中斷操作,要麼執行完成,要麼就不執行。
-
可見性:多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。
-
有序性:程式執行的順序按照程式碼的先後順序執行。
實際專案過程中,有用到多執行緒併發問題的例子嗎?
有,比如單例模式。
由於單例模式的特殊性,可能被程式中不同地方多個執行緒同時呼叫,所以為了避免多執行緒併發問題,一般要採用volatile+Synchronized
的方式進行變數,方法保護。
private volatile static Singleton singleton;
public static Singleton getSingleton4() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
介紹幾種啟動模式。
standard
,預設模式,每次啟動都會新建一個Activity例項,並進入當前任務棧singleTop
,如果要啟動的Activity在棧頂存在例項,則不會重新建立Activity,而是直接使用棧頂的Activity例項,並回撥onNewIntent方法。singleTask
,如果要啟動的Activity在棧中存在例項,則不會重新建立Activity,而是直接使用棧裡的Activity例項,並回撥onNewIntent方法。並且會把這個例項放到棧頂,之前在這個Activity之上的都會被出棧銷燬。singleInstance
,有點單例的感覺,就是所啟動的Activity會單獨放在一個任務棧裡,並且後續所有啟動該Activity都會直接用這個例項,同樣被重複呼叫的時候會呼叫並回撥onNewIntent方法。
Activity依次A→B→C→B,其中B啟動模式為singleTask,AC都為standard,生命週期分別怎麼呼叫?如果B啟動模式為singleInstance又會怎麼呼叫?B啟動模式為singleInstance不變,A→B→C的時候點選兩次返回,生命週期如何呼叫。
1)A→B→C→B,B啟動模式為singleTask
- 啟動A的過程,生命週期呼叫是 (A)onCreate→(A)onStart→(A)onResume
- 再啟動B的過程,生命週期呼叫是 (A)onPause→(B)onCreate→(B)onStart→(B)onResume→(A)onStop
- B→C的過程同上
- C→B的過程,由於B啟動模式為singleTask,所以B會呼叫onNewIntent,並且將B之上的例項移除,也就是C會被移出棧。所以生命週期呼叫是 (C)onPause→(B)onNewIntent→(B)onRestart→(B)onStart→(B)onResume→(C)onStop→(C)onDestory
2)A→B→C→B,B啟動模式為singleInstance
- 如果B為singleInstance,那麼C→B的過程,C就不會被移除,因為B和C不在一個任務棧裡面。所以生命週期呼叫是 (C)onPause→(B)onNewIntent→(B)onRestart→(B)onStart→(B)onResume→(C)onStop
3)A→B→C,B啟動模式為singleInstance
,點選兩次返回鍵
-
如果B為singleInstance,A→B→C的過程,生命週期還是同前面一樣正常呼叫。但是點選返回的時候,由於AC同任務棧,所以C點選返回,會回到A,再點選返回才回到B。所以生命週期是:(C)onPause→(A)onRestart→(A)onStart→(A)onResume→(C)onStop→(C)onDestory。
-
再次點選返回,就會回到B,所以生命週期是:(A)onPause→(B)onRestart→(B)onStart→(B)onResume→(A)onStop→(A)onDestory。
螢幕旋轉時Activity的生命週期,如何防止Activity重建。
-
切換螢幕的生命週期是:onConfigurationChanged->onPause->onSaveInstanceState->onStop->onDestroy->onCreate->onStart->onRestoreInstanceState->onResume
-
如果需要防止旋轉時候,
Activity
重新建立的話需要做如下配置:
在targetSdkVersion
的值小於或等於12時,配置 android:configChanges="orientation",
在targetSdkVersion
的值大於12時,配置 android:configChanges="orientation|screenSize"。
執行緒的三種啟動方式
1)繼承thread類
class MyThread :Thread(){
override fun run() {
super.run()
}
}
fun test(){
var t1=MyThread()
t1.start()
}
2)實現runnable介面
class MyRunnable : Runnable {
override fun run() {
}
}
fun test() {
var t1 = Thread(MyRunnable(),"test")
t1.start()
}
3)實現 Callable 介面
class MyCallThread : Callable<String> {
override fun call(): String {
return "i got it"
}
}
fun test() {
var task = FutureTask(MyCallThread())
var t1 = Thread(task, "test")
t1.start()
try {
//獲取結果
var result = task.get()
} catch (e: Exception) {
}
}
也有人表示其實是兩個方法,因為第三個方法FutureTask
也是實現了Runnable
的方法,只不過表現方法不一樣,然後帶返回值。這個大家面試的時候可以都說上,然後說說自己的見解,畢竟要讓面試官多多看到你的知識面。
執行緒run和start的區別
-
start方法,用start方法來啟動執行緒,真正實現了多執行緒執行,這時無需等待run方法體中的程式碼執行完畢而直接繼續執行後續的程式碼。通過呼叫Thread類的 start()方法來啟動一個執行緒,這時此執行緒處於就緒(可執行)狀態,並沒有執行,一旦得到cpu時間片,就開始執行run()方法,這裡的run()方法 稱為執行緒體,它包含了要執行的這個執行緒的內容,Run方法執行結束,此執行緒隨即終止。
-
run方法,run方法只是類的一個普通方法而已,如果直接呼叫Run方法,程式中依然只有主執行緒這一個執行緒,其程式執行路徑還是隻有一條,還是要順序執行,還是要等待run方法體執行完畢後才可繼續執行下面的程式碼,這樣就沒有達到寫執行緒的目的。
簡單的說就是:
呼叫start方法方可啟動執行緒,而run方法只是thread類中的一個普通方法呼叫,不用啟動新執行緒,還是在主執行緒裡執行。
執行緒的幾種狀態,相互之間是如何轉化的
1) 初始狀態(New)。新建立了一個執行緒物件就進入了初始狀態,也就是通過上述新建執行緒的幾個方法就能進入該狀態。
2) 可執行狀態,就緒狀態(RUNNABLE)。執行緒物件建立後,其他執行緒(比如main執行緒)呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,等待被執行緒排程選中,獲取cpu 的使用權。以下幾種方式會進入可執行狀態:
- 呼叫start方法。
- 拿到物件鎖
- 呼叫yield方法
3)執行狀態(RUNNING)。可執行狀態(runnable)的執行緒獲得了cpu 時間片 ,執行程式程式碼。執行緒排程程式從可執行池中選擇一個執行緒作為當前執行緒,就會進入執行狀態。
4)阻塞狀態(BLOCKED)。執行緒正在執行的時候,被暫停,通常是為了等待某個時間的發生(比如說某項資源就緒)之後再繼續執行。wait,sleep,suspend等方法都可以導致執行緒阻塞。
5)死亡狀態(DEAD)。執行緒run()、main() 方法執行結束,或者因異常退出了run()方法,則該執行緒結束生命週期。死亡的執行緒不可再次復生。
String是java中的基本資料型別嗎?是可變的嗎?是執行緒安全的嗎?
String
不是基本資料型別,java中把大資料型別是:byte, short, int, long, char, float, double, boolean
String
是不可變的String
是不可變類,一旦建立了String物件,我們就無法改變它的值。因此,它是執行緒安全的,可以安全地用於多執行緒環境中
為什麼要設計成不可變的呢?如果String是不可變的,那我們平時賦值是改的什麼呢?
1)為什麼設計不可變
安全
。由於String廣泛用於java
類中的引數,所以安全是非常重要的考慮點。包括執行緒安全,開啟檔案,儲存資料密碼等等。- String的不變性保證雜湊碼始終一,所以在用於HashMap等類的時候就不需要重新計算雜湊碼,
提高效率
。 - 因為java字串是不可變的,可以在java執行時節省大量
java堆空間
。因為不同的字串變數可以引用池中的相同的字串。如果字串是可變得話,任何一個變數的值改變,就會反射到其他變數,那字串池
也就沒有任何意義了。
2)平時使用雙引號方式賦值的時候其實是返回的字串引用
,並不是改變了這個字串物件
淺談一下String, StringBuffer,StringBuilder的區別?String的兩種建立方式,在JVM的儲存方式相同嗎?
String
是不可變類,每當我們對String進行操作的時候,總是會建立新的字串。操作String很耗資源,所以Java提供了兩個工具類來操作String - StringBuffer和StringBuilder
。
StringBuffer和StringBuilder是可變類,StringBuffer
是執行緒安全的,StringBuilder
則不是執行緒安全的。所以在多執行緒對同一個字串操作的時候,我們應該選擇用StringBuffer。由於不需要處理多執行緒的情況,StringBuilder的效率比StringBuffer高。
1) String常見的建立方式有兩種
- String s1 = “Java”
- String s2 = new String("Java")
2)儲存方式不同
-
第一種,s1會先去字串常量池中找字串"Java”,如果有相同的字元則直接返回常量控制程式碼,如果沒有此字串則會先在常量池中建立此字串,然後再返回
常量控制程式碼
,或者說字串引用。 -
第二種,s2是直接在堆上建立一個變數物件,但不儲存到字串池 ,呼叫
intern
方法才會把此字串儲存到常量池中
執行緒池是幹嘛的,優點有哪些?
執行緒池主要用作管理子執行緒,優點有:
- 重用執行緒池中的執行緒,避免頻繁建立和銷燬執行緒所帶來的
記憶體開銷
。 - 有效控制執行緒的最大併發數,避免因執行緒之間搶佔資源而導致的
阻塞現象
。 - 能夠對執行緒進行簡單的管理,提供
定時執行
以及指定時間間隔迴圈執行
等功能。
執行緒池的構造方法每個引數是什麼意思,執行任務的流程
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
corePoolSize
:核心執行緒數。預設情況下執行緒池是空的,只是任務提交時才會建立執行緒。如果當前執行的執行緒數少於corePoolSize,則會建立新執行緒來處理任務;如果等於或者等於corePoolSize,則不再建立。如果呼叫執行緒池的prestartAllcoreThread方法,執行緒池會提前建立並啟動所有的核心執行緒來等待任務。maximumPoolSize
:執行緒池允許建立的最大執行緒數。如果任務佇列滿了並且執行緒數小於maximumPoolSize時,則執行緒池仍然會建立新的執行緒來處理任務。keepAliveTime
:非核心執行緒閒置的超時事件。超過這個事件則回收。如果任務很多,並且每個任務的執行時間很短,則可以調大keepAliveTime來提高執行緒的利用率。另外,如果設定allowCoreThreadTimeOut屬性來true時,keepAliveTime也會應用到核心執行緒上。TimeUnit
:keepAliveTime引數的時間單位。可選的單位有天Days、小時HOURS、分鐘MINUTES、秒SECONDS、毫秒MILLISECONDS等。workQueue
:任務佇列。如果當前執行緒數大於corePoolSzie,則將任務新增到此任務佇列中。該任務佇列是BlockingQueue型別的,即阻塞佇列。ThreadFactory
:執行緒工廠。可以使用執行緒工廠給每個建立出來的執行緒設定名字。一般情況下無須設定該引數。RejectedExecutionHandler
:拒絕策略。這是當前任務佇列和執行緒池都滿了時所採取的應對策略,預設是AbordPolicy,表示無法處理新任務,並丟擲RejectedExecutionException異常。
其中,拒絕策略有四種:
AbordPolicy
:無法處理新任務,並丟擲RejectedExecutionException異常。CallerRunsPolicy
:用呼叫者所在的執行緒來處理任務。此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。DiscardPolicy
:不能執行的任務,並將該任務刪除。DiscardOldestPolicy
:丟棄佇列最近的任務,並執行當前的任務。
執行任務流程:
- 如果執行緒池中的執行緒數量未達到
核心執行緒的數量
,會直接啟動一個核心執行緒來執行任務。 - 如果執行緒池中的執行緒數量已經達到或者超過核心執行緒的數量,那麼任務會被插入到
任務佇列
中排隊等待執行。 - 如果任務佇列無法插入新任務,說明任務佇列已滿,如果未達到規定的最大執行緒數量,則啟動一個
非核心執行緒
來執行任務。 - 如果執行緒數量超過規定的最大值,則執行
拒絕策略
-RejectedExecutionHandler。
Android執行緒池主要分為哪幾類,分別代表了什麼?
主要有四類:FixedThreadPool、CachedThreadPool、SingleThreadExecutor、ScheduledTheadPool
1) FixedThreadPool——可重用固定執行緒數的執行緒池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- 執行緒
數量固定
且都是核心執行緒:核心執行緒數量和最大執行緒數量都是nThreads; - 都是核心執行緒且不會被回收,快速相應外界請求;
- 沒有超時機制,任務佇列也沒有大小限制;
- 新任務使用
核心執行緒
處理,如果沒有空閒的核心執行緒,則排隊等待執行。
- CachedThreadPool——按需建立的執行緒池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 執行緒數量不定,只有非核心執行緒,最大執行緒數
任意大
:傳入核心執行緒數量的引數為0,最大執行緒數為Integer.MAX_VALUE; - 有新任務時使用
空閒執行緒
執行,沒有空閒執行緒則建立新的執行緒來處理。 - 該執行緒池的每個空閒執行緒都有超時機制,時常為60s(引數:60L, TimeUnit.SECONDS),空閒超過60s則回收空閒執行緒。
- 適合執行大量的耗時較少的任務,當所有執行緒閒置
超過60s
都會被停止,所以這時幾乎不佔用系統資源。
- SingleThreadExecutor——單執行緒的執行緒池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- 只有
一個核心執行緒
,所有任務在同一個執行緒按順序執行。 - 所有的外界任務統一到一個執行緒中,所以不需要處理執行緒同步的問題。
- ScheduledThreadPool——定時和週期性的執行緒池
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
- 核心執行緒
數量固定
,非核心執行緒數量無限制
; - 非核心執行緒閒置超過10s會被回收;
- 主要用於執行定時任務和具有固定週期的重複任務;
索引是什麼,優缺點
資料庫索引,是資料庫管理系統中一個排序的資料結構
,以協助快速查詢,更新資料庫中表的資料.索引的實現通常使用B樹和變種的B+樹
(mysql常用的索引就是B+樹)
優點
- 通過建立索引,可以在查詢的過程中,
提高系統的效能
- 通過建立唯一性索引,可以保證資料庫表中每一行資料的
唯一性
- 在使用分組和排序子句進行資料檢索時,可以減少查詢中
分組和排序的時間
缺點
- 建立索引和維護索引要
耗費時間
,而且時間隨著資料量的增加而增大 - 索引需要佔用物理空間,如果要建立聚簇索引,所需要的
空間會更大
- 在對錶中的資料進行增加刪除和修改時需要
耗費較多的時間
,因為索引也要動態地維護
事務四大特性
資料庫事務必須具備ACID
特性,ACID是Atomic(原子性)、Consistency(一致性)、Isolation(隔離性)和Durability(永續性)的英文縮寫。
- 原子性
一個事務中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾
到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
- 一致性
事務的一致性指的是在一個事務執行之前和執行之後資料庫都必須處於一致性狀態
。如果事務成功地完成,那麼系統中所有變化將正確地應用,系統處於有效狀態。如果在事務中出現錯誤,那麼系統中的所有變化將自動地回滾,系統返回到原始狀態。
- 隔離性
指的是在併發環境中,當不同的事務同時操縱相同的資料時,每個事務都有各自的完整資料空間
。由併發事務所做的修改必須與任何其他併發事務所做的修改隔離。事務檢視資料更新時,資料所處的狀態要麼是另一事務修改它之前的狀態,要麼是另一事務修改它之後的狀態,事務不會檢視到中間狀態的資料。
- 永續性
指的是隻要事務成功結束,它對資料庫所做的更新就必須永久儲存下來
。即使發生系統崩潰,重新啟動資料庫系統後,資料庫還能恢復到事務成功結束時的狀態。
講講幾個正規化
正規化的英文名稱是Normal Form
,它是英國人E.F.Codd(關聯式資料庫的老祖宗)在上個世紀70年代提出關聯式資料庫模型
後總結出來的。正規化是關聯式資料庫理論的基礎,也是我們在設計資料庫結構過程中所要遵循的規則和指導方法。通常所用到的只是前三個正規化,即:第一正規化(1NF),第二正規化(2NF),第三正規化(3NF)
。
-
第一正規化
就是屬性不可分割
,每個欄位都應該是不可再拆分的。比如一個欄位是姓名(NAME),在國內的話通常理解都是姓名是一個不可再拆分的單位,這時候就符合第一正規化;但是在國外的話還要分為FIRST NAME和LAST NAME,這時候姓名這個欄位就是還可以拆分為更小的單位的欄位,就不符合第一正規化了。 -
第二正規化
就是要求表中要有主鍵,表中其他其他欄位都依賴於主鍵,因此第二正規化只要記住主鍵約束
就好了。比如說有一個表是學生表,學生表中有一個值唯一的欄位學號,那麼學生表中的其他所有欄位都可以根據這個學號欄位去獲取,依賴主鍵的意思也就是相關的意思,因為學號的值是唯一的,因此就不會造成儲存的資訊對不上的問題,即學生001的姓名不會存到學生002那裡去。 -
第三正規化
就是要求表中不能有其他表中存在的、儲存相同資訊的欄位,通常實現是在通過外來鍵去建立關聯,因此第三正規化只要記住外來鍵約束
就好了。比如說有一個表是學生表,學生表中有學號,姓名等欄位,那如果要把他的系編號,系主任,系主任也存到這個學生表中,那就會造成資料大量的冗餘,一是這些資訊在系資訊表中已存在,二是系中有1000個學生的話這些資訊就要存1000遍。因此第三正規化的做法是在學生表中增加一個系編號的欄位(外來鍵),與系資訊表做關聯。
Recycleview和listview區別
Recycleview佈局效果更多
,增加了縱向,表格,瀑布流等效果Recycleview去掉了一些api
,比如setEmptyview,onItemClickListener等等,給到使用者更多的自定義可能Recycleview去掉了設定頭部底部item的功能
,專向通過viewholder的不同type實現Recycleview實現了一些區域性重新整理
,比如notifyitemchangedRecycleview自帶了一些佈局變化的動畫效果
,也可以通過自定義ItemAnimator類實現自定義動畫效果Recycleview快取機制更全面
,增加兩級快取,還支援自定義快取邏輯
Recycleview有幾級快取,快取過程?
Recycleview有四級快取,分別是mAttachedScrap(螢幕內),mCacheViews(螢幕外),mViewCacheExtension(自定義快取),mRecyclerPool(快取池)
mAttachedScrap(螢幕內)
,用於螢幕內itemview快速重用,不需要重新createView和bindViewmCacheViews(螢幕外)
,儲存最近移出螢幕的ViewHolder,包含資料和position資訊,複用時必須是相同位置的ViewHolder才能複用,應用場景在那些需要來回滑動的列表中,當往回滑動時,能直接複用ViewHolder資料,不需要重新bindView。mViewCacheExtension(自定義快取)
,不直接使用,需要使用者自定義實現,預設不實現。mRecyclerPool(快取池)
,當cacheView滿了後或者adapter被更換,將cacheView中移出的ViewHolder放到Pool中,放之前會把ViewHolder資料清除掉,所以複用時需要重新bindView。
四級快取按照順序需要依次讀取。所以完整快取流程是:
- 儲存快取流程:
- 插入或是刪除
itemView
時,先把螢幕內的ViewHolder儲存至AttachedScrap
中 - 滑動螢幕的時候,先消失的itemview會儲存到
CacheView
,CacheView大小預設是2,超過數量的話按照先入先出原則,移出頭部的itemview儲存到RecyclerPool快取池
(如果有自定義快取就會儲存到自定義快取裡),RecyclerPool快取池會按照itemview的itemtype
進行儲存,每個itemTyep快取個數為5個,超過就會被回收。
- 獲取快取流程:
- AttachedScrap中獲取,通過pos匹配holder——>獲取失敗,從
CacheView
中獲取,也是通過pos獲取holder快取
——>獲取失敗,從自定義快取
中獲取快取——>獲取失敗,從mRecyclerPool
中獲取
——>獲取失敗,重新建立viewholder
——createViewHolder並bindview。
需要注意的是,如果從快取池找到快取,還需要重新bindview。
說說RecyclerView效能優化。
bindViewHolder
方法是在UI執行緒進行的,此方法不能耗時操作,不然將會影響滑動流暢性。比如進行日期的格式化。- 對於新增或刪除的時候,可以使用
diffutil
進行區域性重新整理,少用全域性重新整理 - 對於
itemVIew
進行佈局優化,比如少巢狀等。 - 25.1.0 (>=21)及以上使用
Prefetch
功能,也就是預取功能,巢狀時且使用的是LinearLayoutManager,子RecyclerView可通過setInitialPrefatchItemCount設定預取個數 - 加大
RecyclerView快取
,比如cacheview大小預設為2,可以設定大點,用空間來換取時間,提高流暢度 - 如果高度固定,可以設定
setHasFixedSize(true)
來避免requestLayout浪費資源,否則每次更新資料都會重新測量高度。
void onItemsInsertedOrRemoved() {
if (hasFixedSize) layoutChildren();
else requestLayout();
}
- 如果多個
RecycledView
的 Adapter 是一樣的,比如巢狀的 RecyclerView 中存在一樣的 Adapter,可以通過設定RecyclerView.setRecycledViewPool(pool);
來共用一個RecycledViewPool
。這樣就減少了建立VIewholder的開銷。 - 在RecyclerView的元素比較高,一屏只能顯示一個元素的時候,第一次滑動到第二個元素會卡頓。這種情況就可以通過設定額外的快取空間,重寫
getExtraLayoutSpace
方法即可。
new LinearLayoutManager(this) {
@Override
protected int getExtraLayoutSpace(RecyclerView.State state) {
return size;
}
};
- 設定
RecyclerView.addOnScrollListener();
來在滑動過程中停止載入的操作。 - 減少物件的建立,比如設定監聽事件,可以全域性建立一個,所有view公用一個listener,並且放到
CreateView
裡面去建立監聽,因為CreateView呼叫要少於bindview。這樣就減少了物件建立所造成的消耗 - 用
notifyDataSetChange
時,介面卡不知道整個資料集中的那些內容以及存在,再重新匹配ViewHolder
時會花生閃爍。設定adapter.setHasStableIds(true),並重寫getItemId()
來給每個Item一個唯一的ID,也就是唯一標識,就使itemview的焦點固定,解決了閃爍問題。
說說雙重校驗鎖,以及volatile的作用
先回顧下雙重校驗鎖的原型,也就是單例模式的實現:
public class Singleton {
private volatile static Singleton mSingleton;
private Singleton() {
}
public Singleton getInstance() {
if (null == mSingleton) {
synchronized (Singleton.class) {
if (null == mSingleton) {
mSingleton = new Singleton();
}
}
}
return mSingleton;
}
}
有幾個疑問需要解決:
- 為什麼要加鎖?
- 為什麼不直接給getInstance方法加鎖?
- 為什麼需要雙重判斷是否為空?
- 為什麼還要加volatile修飾變數?
接下來一一解答:
- 如果不加鎖的話,是
執行緒不安全
的,也就是有可能多個執行緒同時訪問getInstance方法會得到兩個例項化的物件。 - 如果給getInstance方法加鎖,就每次訪問mSingleton都需要加鎖,增加了
效能開銷
- 第一次判空是為了判斷是否已經例項化,如果已經例項化就直接返回變數,不需要加鎖了。第二次判空是因為走到加鎖這一步,如果執行緒A已經例項化,等B獲得鎖,進入的時候其實物件已經例項化完成了,如果不二次判空就會
再次例項化
。 - 加volatile是為了
禁止指令重排
。指令重排指的是在程式執行過程中,並不是完全按照程式碼順序執行的,會考慮到效能等原因,將不影響結果的指令順序有可能進行調換。所以初始化的順序本來是這三步:
1)分配記憶體空間
2)初始化物件
3)將物件指向分配的空間
如果進行了指令重排,由於不影響結果,所以2和3有可能被調換。所以就變成了:
1)分配記憶體空間
2)將物件指向分配的空間
3)初始化物件
就有可能會導致,假如執行緒A中已經進行到第二步,執行緒B進入第二次判空的時候,判斷mSingleton不為空,就直接返回了,但是實際此時mSingleton
還沒有初始化。
synchronized和volatile的區別
volatile
本質是在告訴jvm當前變數在暫存器中的值是不確定的,需要從主存中讀取,synchronized
則是鎖定當前變數,只有當前執行緒可以訪問該變數,其他執行緒被阻塞住.volatile
僅能使用在變數級別,synchronized
則可以使用在變數,方法.volatile
僅能實現變數的修改可見性,而synchronized
則可以保證變數的修改可見性和原子性.volatile
不會造成執行緒的阻塞,而synchronized
可能會造成執行緒的阻塞.- 當一個域的值依賴於它之前的值時,
volatile
就無法工作了,如n=n+1,n++等,也就是不保證原子性。 - 使用
volatile
而不是synchronized
的唯一安全的情況是類中只有一個可變的域。
synchronized修飾static方法和修飾普通方法有什麼區別
-
Synchronized修飾非靜態方法
,實際上是對呼叫該方法的物件加鎖,俗稱“物件鎖”。也就是鎖住的是這個物件,即this。如果同一個物件在兩個執行緒分別訪問物件的兩個同步方法,就會產生互斥,這就是物件鎖,一個物件一次只能進入一個操作。 -
Synchronized修飾靜態方法
,實際上是對該類物件加鎖,俗稱“類鎖”。也就是鎖住的是這個類,即xx.class。如果一個物件在兩個執行緒中分別呼叫一個靜態同步方法和一個非靜態同步方法,由於靜態方法會收到類鎖限制,但是非靜態方法會收到物件限制,所以兩個方法並不是同一個物件鎖,因此不會排斥。
記憶體洩漏是什麼,為什麼會發生?
記憶體洩漏(Memory Leak)是指程式中己動態分配的堆記憶體由於某種原因程式未釋放或無法釋放
,造成系統記憶體的浪費,導致程式執行速度減慢甚至系統崩潰等嚴重後果。
簡單點說,手機給我們的應用提供了一定大小的堆記憶體
,在不斷建立物件的過程中,也在不斷的GC
(java的垃圾回收機制),所以記憶體正常情況下會保持一個平穩的值。
但是出現記憶體洩漏就會導致某個例項,比如Activity的例項,應用被某個地方引用到了,不能正常釋放,從而導致記憶體佔用越來越大
,這就是記憶體洩漏。
記憶體洩漏發生的情況有哪些?
主要有四類情況
:
- 集合類洩漏
- 單例/靜態變數造成的記憶體洩漏
- 匿名內部類/非靜態內部類
- 資源未關閉造成的記憶體洩漏
1)集合類洩漏
集合類新增元素後,仍引用著集合元素物件,導致該集合中的元素物件無法被回收,從而導致記憶體洩露。
static List<Object> mList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Object obj = new Object();
mList.add(obj);
obj = null;
}
解決辦法就是把集合也釋放掉。
mList.clear();
mList = null;
2)單例/靜態變數造成的記憶體洩漏
單例模式具有其靜態特性
,它的生命週期等於應用程式的生命週期,正是因為這一點,往往很容易造成記憶體洩漏。
public class SingleInstance {
private static SingleInstance mInstance;
private Context mContext;
private SingleInstance(Context context){
this.mContext = context;
}
public static SingleInstance newInstance(Context context){
if(mInstance == null){
mInstance = new SingleInstance(context);
}
return sInstance;
}
}
比如這個單例模式,如果我們呼叫newInstance
方法時候把Activity的context
傳進去,那麼就是生命週期長的持有了生命週期短的引用,造成了記憶體洩漏。要修改的話把context改成context.getApplicationContext()
即可。
3)匿名內部類/非靜態內部類
非靜態內部類他會持有他外部類的強引用,所以就有可能導致非靜態內部類的生命週期可能比外部類更長,容易造成記憶體洩漏,最常見的就是Handler
。
public class TestActivity extends Activity {
private TextView mText;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
mHandler. sendEmptyMessageDelayed(0, 100000);
}
怎麼修改呢?改成靜態內部類,然後弱引用方式修飾外部類
public class TestActivity extends Activity {
private TextView mText;
private MyHandler myHandler = new MyHandler(TestActivity.this);
private MyThread myThread = new MyThread();
private static class MyHandler extends Handler {
WeakReference<TestActivity> weakReference;
MyHandler(TestActivity testActivity) {
this.weakReference = new WeakReference<TestActivity>(testActivity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
weakReference.get().mText.setText("do someThing");
}
}
@Override
protected void onDestroy() {
super.onDestroy();
myHandler.removeCallbacksAndMessages(null);
}
4)資源未關閉造成的記憶體洩漏
比如:
- 網路、檔案等流忘記關閉
- 手動註冊廣播時,退出時忘記
unregisterReceiver()
- Service 執行完後忘記
stopSelf()
- EventBus 等觀察者模式的框架忘記手動解除註冊
該怎麼發現和解決記憶體洩漏?
1、使用工具,比如Memory Profiler
,可以檢視app的記憶體實時情況,捕獲堆轉儲,就生成了一個記憶體快照,hprof
檔案。通過檢視檔案,可以看到哪些類發生了記憶體洩漏。
2、使用庫,比較出名的就是LeakCanary
,匯入庫,然後執行後,就可以發現app內的記憶體洩漏情況。
這裡說下LeakCanary
的原理:
-
監聽
首先通過ActivityLifecycleCallbacks
和FragmentLifeCycleCallbacks
監聽Activity和Fragment的生命週期。 -
判斷
然後在銷燬的生命週期中判斷物件是否被回收。弱引用在定義的時候可以指定引用物件和一個ReferenceQueue
,通過該弱引用是否被加入ReferenceQueue就可以判斷該物件是否被回收。 -
分析
最後通過haha庫來分析hprof
檔案,從而找出類之前的引用關係。
什麼是類載入機制?
我們編寫的java檔案會在編譯後變成.class檔案,類載入器就是負責載入class位元組碼檔案,class檔案在檔案開頭有特定的檔案標識,將class檔案位元組碼內容載入到記憶體中,並將這些內容轉換成方法區中的執行時資料結構並且ClassLoader只負責class檔案的載入,至於它是否可以執行,則由執行引擎Execution Engine決定。
簡單來說類載入機制就是從檔案系統將一系列的 class 檔案讀入 JVM 記憶體中為後續程式執行提供資源的動作。
類載入器種類。
類載入器種類主要有四種:
- BootstrapClassLoader:啟動類載入器,使用C++實現
- ExtClassLoader:擴充套件類載入器,使用Java實現
- AppClassLoader:應用程式類載入器,載入當前應用的classpath的所有類
- UserDefinedClassLoader:使用者自定義類載入器
屬於依次繼承關係,也就是上一級是下一級的父載入器。
什麼是雙親委派機制,為什麼這麼設計?
當一個類載入器收到了類載入的請求,它不會直接去載入這類,而是先把這個請求委派給父載入器去完成,依次會傳遞到最上級也就是啟動類載入器,然後父載入器會檢查是否已經載入過該類,如果沒載入過,就會去載入,載入失敗才會交給子載入器去載入,一直到最底層,如果都沒辦法能正確載入,則會跑出ClassNotFoundException異常。
舉例:
- 當Application ClassLoader 收到一個類載入請求時,他首先不會自己去嘗試載入這個類,而是將這個請求委派給父類載入器Extension ClassLoader去完成。
- 當Extension ClassLoader收到一個類載入請求時,他首先也不會自己去嘗試載入這個類,而是將請求委派給父類載入器Bootstrap ClassLoader去完成。
- 如果Bootstrap ClassLoader載入失敗(在<JAVA_HOME>\lib中未找到所需類),就會讓Extension ClassLoader嘗試載入。
- 如果Extension ClassLoader也載入失敗,就會使用Application ClassLoader載入。
- 如果Application ClassLoader也載入失敗,就會使用自定義載入器去嘗試載入。
- 如果均載入失敗,就會丟擲ClassNotFoundException異常。
這麼設計的原因是為了防止危險程式碼的植入,比如String類,如果在AppClassLoader就直接被載入,就相當於會被篡改了,所以都要經過老大,也就是BootstrapClassLoader進行檢查,已經載入過的類就不需要再去載入了。
webView與js通訊
1) Android呼叫JS程式碼
主要有兩種方法:
- 通過WebView的loadUrl()
// 呼叫javascript的callJS()方法
mWebView.loadUrl("javascript:callJS()");
但是這種不常用,因為它會自動重新整理頁面而且沒有返回值,有點影響互動。
- 通過WebView的
evaluateJavascript()
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此處為 js 返回的結果
}
});
這種就比較全面了。呼叫方法並且獲取返回值。
2) JS呼叫Android端程式碼
主要有兩種方法:
- 通過WebView的
addJavascriptInterface()
進行物件對映
public class AndroidtoJs extends Object {
// 定義JS需要呼叫的方法
// 被JS呼叫的方法必須加入@JavascriptInterface註解
@JavascriptInterface
public void hello(String msg) {
System.out.println("JS呼叫了Android的hello方法");
}
}
mWebView.addJavascriptInterface(new AndroidtoJs(), "test");
//js中:
function callAndroid(){
// 由於物件對映,所以呼叫test物件等於呼叫Android對映的物件
test.hello("js呼叫了android中的hello方法");
}
這種方法雖然很好用,但是要注意的是4.2以後,對於被呼叫的函式以@JavascriptInterface
進行註解,否則容易出發漏洞,因為js方可以通過反射呼叫一些本地命令,很危險。
- 通過 WebViewClient 的
shouldOverrideUrlLoading ()
方法回撥攔截 url
這種方法是通過shouldOverrideUrlLoading
回撥去攔截url,然後進行解析,如果是之前約定好的協議,就呼叫相應的方法。
// 複寫WebViewClient類的shouldOverrideUrlLoading方法
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Uri uri = Uri.parse(url);
// 如果url的協議 = 預先約定的 js 協議
if ( uri.getScheme().equals("js")) {
// 如果 authority = 預先約定協議裡的 webview,即代表都符合約定的協議
if (uri.getAuthority().equals("webview")) {
System.out.println("js呼叫了Android的方法");
// 可以在協議上帶有引數並傳遞到Android上
HashMap<String, String> params = new HashMap<>();
Set<String> collection = uri.getQueryParameterNames();
}
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
}
);
如何避免WebView記憶體洩露
WebView的記憶體洩露主要是因為在頁面銷燬後,WebView的資源無法馬上釋放所導致的。現在主流的是兩種方法:
1)不在xml佈局中新增webview
標籤,採用在程式碼中new出來的方式,並在頁面銷燬的時候去釋放webview
資源
//addview
private WeakReference<BaseWebActivity> webActivityReference = new WeakReference<BaseWebActivity>(this);
mWebView = new BridgeWebView(webActivityReference .get());
webview_container.addView(mWebView);
//銷燬
ViewParent parent = mWebView.getParent();
if (parent != null) {
((ViewGroup) parent).removeView(mWebView);
}
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.clearView();
mWebView.removeAllViews();
mWebView.destroy();
mWebView=null;
2)另起一個程式載入webview,頁面銷燬後幹掉這個程式。但是這個方法的麻煩之處就在於程式間通訊
。
使用方法很簡單,xml檔案中寫出程式名即可,銷燬的時候呼叫System.exit(0)
<activity android:name=".WebActivity"
android:process=":remoteweb"/>
System.exit(0)
webView還有哪些可以優化的地方
-
提前初始化或者使用
全域性WebView
。首次初始化WebView會比第二次初始化慢很多。初始化後,即使WebView已釋放,但一些多WebView共用的全域性服務/資源對想仍未釋放,而第二次初始化不需要生成,因此初始化變快。 -
DNS採用和客戶端API相同的域名,
DNS解析
也是耗時比較多的部分,所以用客戶端API相同的域名因為其DNS會被快取,所以開啟webView的時候就不會再耗時在DNS上了 -
對於JS的優化,儘量不要用
偏重的框架
,比如React。其次是高效能要求頁面還是需要後端渲染。最後就是app中的網頁框架要統一,這樣就可以對js進行快取和複用。
這裡有美團團隊的總結方案,如下:
- WebView初始化慢,可以在
初始化
同時先請求資料,讓後端和網路不要閒著。 - 後端處理慢,可以讓伺服器
分trunk輸出
,在後端計算的同時前端也載入網路靜態資源。 - 指令碼執行慢,就讓
指令碼在最後執行
,不阻塞頁面解析。 - 同時,合理的
預載入、預快取
可以讓載入速度的瓶頸更小。 - WebView初始化慢,就隨時
初始化
好一個WebView待用。 - DNS和連結慢,想辦法複用客戶端使用的
域名和連結
。 - 指令碼執行慢,可以把
框架程式碼拆分
出來,在請求頁面之前就執行好。
Activity、View、Window 之間的關係。
每個 Activity
包含了一個 Window
物件,這個物件是由 PhoneWindow
做的實現。而 PhoneWindow
將 DecorView
作為了一個應用視窗的根 View,這個 DecorView 又把螢幕劃分為了兩個區域:一個是 TitleView
,一個是ContentView
,而我們平時在 Xml 檔案中寫的佈局正好是展示在 ContentView 中的。
說說Android的事件分發機制完整流程,也就是從點選螢幕開始,事件會怎麼傳遞。
我覺得事件分發機制流程可以分為三部分,分別是從外傳裡,從裡傳外,消費之後
。
1)首先,從最外面一層傳到最裡面一層:
如果當前是viewgroup
層級,就會判斷 onInterceptTouchEvent
是否為true,如果為true,則代表事件要消費在這一層級,不再往下傳遞。接著便執行當前 viewgroup 的onTouchEvent方法。如果onInterceptTouchEvent
為false,則代表事件繼續傳遞到下一層級的 dispatchTouchEvent
方法,接著一樣的程式碼邏輯,一直到最裡面一層的view。
虛擬碼解釋:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean isConsume = false;
if (isViewGroup) {
if (onInterceptTouchEvent(event)) {
isConsume = onTouchEvent(event);
} else {
isConsume = child.dispatchTouchEvent(event);
}
} else {
//isView
isConsume = onTouchEvent(event);
}
return isConsume;
}
2)到最裡層的view之後,view本身還是可以選擇消費或者傳到外面。
到最裡面一層就會直接執行onTouchEvent
方法,這時候,view有沒有權利拒絕消費事件呢? 按道理view作為最底層的,應該是沒有發言權才對。但是呢,秉著公平公正原則,view也是可以拒絕的,可以在onTouchEvent
方法返回false,表示他不想消費這個事件。那麼它的父容器的onTouchEvent
又會被呼叫,如果父容器的onTouchEvent又返回false,則又交給上一級。一直到最上層,也就是Activity的onTouchEvent
被呼叫。
虛擬碼解釋:
public void handleTouchEvent(MotionEvent event) {
if (!onTouchEvent(event)) {
getParent.onTouchEvent(event);
}
}
3)消費之後
當某一層viewGroup的onInterceptTouchEvent
為true,則代表當前層級要消費事件。如果它的onTouchListener
被設定了的話,則onTouch會被呼叫,如果onTouch的返回值返回true,則onTouchEvent
不會被呼叫。如果返回false或者沒有設定onTouchListener,則會繼續呼叫onTouchEvent。而onClick方法則是設定了onClickListener
則會被正常呼叫。
虛擬碼解釋:
public void consumeEvent(MotionEvent event) {
if (setOnTouchListener) {
int tag = onTouch();
if (!tag) {
onTouchEvent(event);
}
} else {
onTouchEvent(event);
}
if (setOnClickListener) {
onClick();
}
}
解決滑動衝突的辦法。
解決滑動衝突的根本就是要在適當的位置進行攔截,那麼就有兩種解決辦法:
外部攔截
:從父view端處理,根據情況決定事件是否分發到子view內部攔截
:從子view端處理,根據情況決定是否阻止父view進行攔截,其中的關鍵就是requestDisallowInterceptTouchEvent
方法。
1)外部攔截法,其實就是在onInterceptTouchEvnet
方法裡面進行判斷,是否攔截,見程式碼:
//外部攔截法:父view.java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
//父view攔截條件
boolean parentCanIntercept;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (parentCanIntercept) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
return intercepted;
}
還是比較簡單的,直接判斷攔截條件,然後返回true就代表攔截,false就不攔截,傳到子view。注意的是ACTION_DOWN
狀態不要攔截,如果攔截,那麼後續事件就直接交給父view處理了,也就沒有攔截不攔截的問題了。
- 內部攔截法,就是通過
requestDisallowInterceptTouchEvent
方法讓父view不要攔截。
//父view.java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
//子view.java
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
//父view攔截條件
boolean parentCanIntercept;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (parentCanIntercept) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(event);
}
requestDisallowInterceptTouchEvent(true)
的意思是阻止父view攔截事件,也就是傳入true之後,父view就不會再呼叫onInterceptTouchEvent
。反之,傳入false就代表父view可以攔截,也就是會走到父view的onInterceptTouchEvent
方法。所以需要父view攔截的時候,就傳入flase,需要父view不攔截的時候就傳入true。
Fragment生命週期,當hide,show,replace時候生命週期變化
1)生命週期:
onAttach()
:Fragment和Activity相關聯時呼叫。可以通過該方法獲取Activity引用,還可以通過getArguments()獲取引數。onCreate()
:Fragment被建立時呼叫。onCreateView()
:建立Fragment的佈局。onActivityCreated()
:當Activity完成onCreate()時呼叫。onStart()
:當Fragment可見時呼叫。onResume()
:當Fragment可見且可互動時呼叫。onPause()
:當Fragment不可互動但可見時呼叫。onStop()
:當Fragment不可見時呼叫。onDestroyView()
:當Fragment的UI從檢視結構中移除時呼叫。onDestroy()
:銷燬Fragment時呼叫。onDetach()
:當Fragment和Activity解除關聯時呼叫。
每個呼叫方法對應的生命週期變化:
add()
: onAttach()->…->onResume()。remove()
: onPause()->…->onDetach()。replace()
: 相當於舊Fragment呼叫remove(),新Fragment呼叫add()。remove()+add()的生命週期加起來show()
: 不呼叫任何生命週期方法,呼叫該方法的前提是要顯示的 Fragment已經被新增到容器,只是純粹把Fragment UI的setVisibility為true。hide()
: 不呼叫任何生命週期方法,呼叫該方法的前提是要顯示的Fragment已經被新增到容器,只是純粹把Fragment UI的setVisibility為false。
Activity 與 Fragment,Fragment 與 Fragment之間怎麼互動通訊。
- Activity 與 Fragment通訊
Activity有Fragment的例項,所以可以執行Fragment的方法,或者傳入一個介面。
同樣,Fragment可以通過getActivity()
獲取Activity的例項,也是可以執行方法。
- Fragment 與 Fragment之間通訊
1)直接獲取另一個Fragmetn的例項
getActivity().getSupportFragmentManager().findFragmentByTag("mainFragment");
2)介面回撥
一個Fragment裡面去實現介面,另一個Fragment把介面例項傳進去。
3)Eventbus等框架。
Fragment遇到viewpager遇到過什麼問題嗎。
-
滑動的時候,呼叫setCurrentItem方法,要注意第二個引數
smoothScroll
。傳false,就是直接跳到fragment,傳true,就是平滑過去。一般主頁切換頁面都是用false。 -
禁止預載入的話,呼叫
setOffscreenPageLimit(0)
是無效的,因為方法裡面會判斷是否小於1。需要重寫setUserVisibleHint
方法,判斷fragment是否可見。 -
不要使用
getActivity()
獲取activity例項,容易造成空指標,因為如果fragment已經onDetach()了,那麼就會報空指標。所以要在onAttach
方法裡面,就去獲取activity的上下文。 -
FragmentStatePagerAdapter
對limit外的Fragment銷燬,生命週期為onPause->onStop->onDestoryView->onDestory->onDetach, onAttach->onCreate->onCreateView->onStart->onResume。也就是說切換fragment的時候有可能會多次onCreateView
,所以需要注意處理資料。 -
由於可能多次
onCreateView
,所以我們可以把view儲存起來,如果為空再去初始化資料。見程式碼:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (null == mFragmentView) {
mFragmentView = inflater.inflate(getContentViewLayoutID(), null);
ButterKnife.bind(this, mFragmentView);
isDestory = false;
initViewsAndEvents();
}
return mFragmentView;
}
ARouter的原理
首先,我們瞭解下ARouter
是幹嘛的?ARouter
是阿里巴巴研發的一個用於解決元件間,模組間介面跳轉問題的框架。
所以簡單的說,就是用來跳轉介面的,不同於平時用到的顯式或隱式跳轉,只需要在對應的介面上新增註解
,就可以實現跳轉,看個案例:
@Route(path = "/test/activity")
public class YourActivity extend Activity {
...
}
//跳轉
ARouter.getInstance().build("/test/activity").navigation();
使用很方便,通過一個path
就可以進行跳轉了,那麼原理是什麼呢?
其實仔細思考下,就可以聯想到,既然關鍵跳轉過程是通過path
跳轉到具體的activity
,那麼原理無非就是把path
和Activity
一一對應起來就行了。沒錯,其實就是通過註釋,通過apt
技術,也就是註解處理工具,把path和activity關聯起來了。主要有以下幾個步驟:
- 程式碼里加入的
@Route
註解,會在編譯時期通過apt生成一些儲存path和activity.class對映關係的類檔案 - app程式啟動的時候會載入這些類檔案,把儲存這些對映關係的資料讀到記憶體裡(儲存在map裡)
- 進行路由跳轉的時候,通過
build()
方法傳入要到達頁面的路由地址,ARouter會通過它自己儲存的路由表找到路由地址對應的Activity.class - 然後
new Intent
方法,如果有呼叫ARouter
的withString()
方法,就會呼叫intent.putExtra(String name, String value)
方法新增引數 - 最後呼叫
navigation()
方法,它的內部會呼叫startActivity(intent)進行跳轉
ARouter怎麼實現頁面攔截
先說一個攔截器的案例,用作頁面跳轉時候檢驗是否登入,然後判斷跳轉到登入頁面還是目標頁面:
@Interceptor(name = "login", priority = 6)
public class LoginInterceptorImpl implements IInterceptor {
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
String path = postcard.getPath();
boolean isLogin = SPUtils.getInstance().getBoolean(ConfigConstants.SP_IS_LOGIN, false);
if (isLogin) {
// 如果已經登入不攔截
callback.onContinue(postcard);
} else {
// 如果沒有登入,進行攔截
callback.onInterrupt(postcard);
}
}
@Override
public void init(Context context) {
LogUtils.v("初始化成功");
}
}
//使用
ARouter.getInstance().build(ConfigConstants.SECOND_PATH)
.withString("msg", "123")
.navigation(this,new LoginNavigationCallbackImpl());
// 第二個引數是路由跳轉的回撥
// 攔截的回撥
public class LoginNavigationCallbackImpl implements NavigationCallback{
@Override
public void onFound(Postcard postcard) {
}
@Override
public void onLost(Postcard postcard) {
}
@Override
public void onArrival(Postcard postcard) {
}
@Override
public void onInterrupt(Postcard postcard) {
//攔截並跳轉到登入頁
String path = postcard.getPath();
Bundle bundle = postcard.getExtras();
ARouter.getInstance().build(ConfigConstants.LOGIN_PATH)
.with(bundle)
.withString(ConfigConstants.PATH, path)
.navigation();
}
}
攔截器實現IInterceptor
介面,使用註解@Interceptor
,這個攔截器就會自動被註冊了,同樣是使用APT技術自動生成對映關係類。這裡還有一個優先順序引數priority
,數值越小,就會越先執行。
怎麼應用到元件化中
首先,在公用元件的build.gradle中新增依賴:
dependencies {
api 'com.alibaba:arouter-api:1.4.0'
annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
}
其次,必須在每個業務元件,也就是用到了arouter
的元件中都宣告annotationProcessorOptions
,否則會無法通過apt生成索引檔案,也就無法正常跳轉了:
//業務元件的build.gradle
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
dependencies {
annotationProcessor 'com.alibaba:arouter-compiler:1.2.1'
implementation '公用元件'
}
這個arguments
是用來設定給編譯處理器的一些引數,這裡就把[AROUTER_MODULE_NAME: project.getName()]
鍵值對傳了過去,方便Arouter使用apt的時候進行資料處理,也是Arouter庫所規定的配置。
然後就可以正常使用了。
說說你對協程的理解
在我看來,協程和執行緒一樣都是用來解決併發任務(非同步任務)
的方案。
所以協程和執行緒是屬於一個層級的概念,但是對於kotlin
中的協程,又與廣義的協程有所不同。
kotlin中的協程其實是對執行緒的一種封裝
,或者說是一種執行緒框架,為了讓非同步任務更好更方便使用。
說下協程具體的使用
比如在一個非同步任務需要回撥到主執行緒的情況,普通執行緒需要通過handler
切換執行緒然後進行UI更新等,一旦多個任務需要順序呼叫
,那更是很不方便,比如以下情況:
//客戶端順序進行三次網路非同步請求,並用最終結果更新UI
thread{
iotask1(parameter) { value1 ->
iotask1(value1) { value2 ->
iotask1(value2) { value3 ->
runOnUiThread{
updateUI(value3)
}
}
}
}
}
簡直是魔鬼呼叫
,如果不止3次,而是5次,6次,那還得了。。
而用協程就能很好解決這個問題:
//併發請求
GlobalScope.launch(Dispatchers.Main) {
//三次請求併發進行
val value1 = async { request1(parameter1) }
val value2 = async { request2(parameter2) }
val value3 = async { request3(parameter3) }
//所有結果全部返回後更新UI
updateUI(value1.await(), value2.await(), value3.await())
}
//切換到io執行緒
suspend fun request1(parameter : Parameter){withContext(Dispatcher.IO){}}
suspend fun request2(parameter : Parameter){withContext(Dispatcher.IO){}}
suspend fun request3(parameter : Parameter){withContext(Dispatcher.IO){}}
就像是同一個執行緒中順序執行的效果一樣,再比如我要按順序執行一次非同步任務,然後完成後更新UI,一共三個非同步任務。
如果正常寫應該怎麼寫?
thread{
iotask1() { value1 ->
runOnUiThread{
updateUI1(value1)
iotask2() { value2 ->
runOnUiThread{
updateUI2(value2)
iotask3() { value3 ->
runOnUiThread{
updateUI3(value3)
}
}
}
}
}
}
}
暈了暈了,不就是一次非同步任務,一次UI更新嗎。怎麼這麼麻煩,來,用協程看看怎麼寫:
GlobalScope.launch (Dispatchers.Main) {
ioTask1()
ioTask1()
ioTask1()
updateUI1()
updateUI2()
updateUI3()
}
suspend fun ioTask1(){
withContext(Dispatchers.IO){}
}
suspend fun ioTask2(){
withContext(Dispatchers.IO){}
}
suspend fun ioTask3(){
withContext(Dispatchers.IO){}
}
fun updateUI1(){
}
fun updateUI2(){
}
fun updateUI3(){
}
協程怎麼取消
- 取消
協程作用域
將取消它的所有子協程。
// 協程作用域 scope
val job1 = scope.launch { … }
val job2 = scope.launch { … }
scope.cancel()
- 取消
子協程
// 協程作用域 scope
val job1 = scope.launch { … }
val job2 = scope.launch { … }
job1.cancel()
但是呼叫了cancel
並不代表協程內的工作會馬上停止,他並不會組織程式碼執行。
比如上述的job1
,正常情況處於active
狀態,呼叫了cancel
方法後,協程會變成Cancelling
狀態,工作完成之後會變成Cancelled
狀態,所以可以通過判斷協程的狀態來停止工作。
Jetpack 中定義的協程作用域(viewModelScope 和 lifecycleScope)
可以幫助你自動取消任務,下次再詳細說明,其他情況就需要自行進行繫結和取消了。
之前大家應該看過我寫的啟動流程分析了吧,那篇文章裡我說過分析原始碼的目的一直都不是為了學知識而學,而是理解了這些基礎,我們才能更好的解決問題。所以今天就來看看通過分析app啟動流程,我們該怎麼具體進行啟動優化。
- App啟動流程中我們能進行優化的地方有哪些?
- 具體有哪些優化方法?
- 分析啟動耗時的方法
具體有哪些啟動優化方法?
- 障眼法之閃屏頁
為了消除啟動時的白屏/黑屏,可以通過設定android:windowBackground,讓人感覺一點選icon就啟動完畢了的感覺。
<activity android:name=".ui.activity.啟動activity"
android:theme="@style/MyAppTheme"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<style name="MyAppTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@drawable/logo</item>
</style>
- 預建立Activity
物件第一次建立的時候,java虛擬機器首先檢查類對應的Class 物件是否已經載入。如果沒有載入,jvm會根據類名查詢.class檔案,將其Class物件載入。同一個類第二次new的時候就不需要載入類物件,而是直接例項化,建立時間就縮短了。
- 第三方庫懶載入
很多第三方開源庫都說在Application中進行初始化,所以可以把一些不是需要啟動就初始化的三方庫的初始化放到後面,按需初始化,這樣就能讓Application變得更輕。
- WebView啟動優化
webview第一次啟動會非常耗時,具體優化方法可以看我之前的文章,關於webview的優化。
- 執行緒優化
執行緒是程式執行的基本單位,執行緒的頻繁建立是耗效能的,所以大家應該都會用執行緒池。單個cpu情況下,即使是開多個執行緒,同時也只有一個執行緒可以工作,所以執行緒池的大小要根據cpu個數來確定。
- MultiDex 優化
由於65536方法限制,所以一般class檔案要生成多個dex檔案,Android5.0以下,ClassLoader載入類的時候只會從class.dex(主dex)里載入,所以要執行MultiDex.install(context)方法才能正常讀取dex類。
而這個install方法就是耗時大戶,會解壓apk,遍歷dex檔案,壓縮dex、將dex檔案通過反射轉換成DexFile物件、反射替換陣列。
這裡需要的方案就是今日頭條方案:
1、在Application的attachBaseContext方法裡,啟動另一個程式的LoadDexActivity去非同步執行MultiDex邏輯,顯示Loading。
2、然後主程式Application進入while迴圈,不斷檢測MultiDex操作是否完成
3、MultiDex執行完之後主程式Application繼續走,ContentProvider初始化和Application onCreate方法,也就是執行主程式正常的邏輯。
所以重點就是單開程式去執行MultiDex邏輯,這樣就不影響APP的啟動了。
分析啟動耗時的方法
- Systrace + 函式插樁
也就是通過在方法的入口和出口加入統計程式碼,從而統計方法耗時
class Trace{
public static void i(String tag){
android.os.Trace.beginSection(tag);
}
public static void o(){
android.os.Trace.endSection();
}
}
void test(){
Trace.i("test");
System.out.println("doSomething");
Trace.o();
}
- BlockCanary
BlockCanary 可以監聽主執行緒耗時的方法,就是在主執行緒訊息迴圈打出日誌的地入手, 當一個訊息操作時間超過閥值後, 記錄系統各種資源的狀態, 並展示出來。所以我們將閾值設定低一點,這樣的話如果一個方法執行時間超過200毫秒,獲取堆疊資訊。
而記錄時間的方法我們之前也說過,就是通過looper()方法中迴圈去從MessageQueue中去取msg的時候,在dispatchMessage方法前後會有logging日誌列印,所以只需要自定義一個Printer,重寫println(String x)方法即可實現耗時統計了。
Activity、View、Window三者如何關聯?
Activity包含了一個PhoneWindow
,而PhoneWindow
就是繼承於Window的,Activity通過setContentView
將View設定到了PhoneWindow
上,而View通過WindowManager的addView()、removeView()、updateViewLayout()
對View進行管理。Window的新增過程以及Activity的啟動流程都是一次IPC的過程。Activity的啟動需要通過AMS完成;Window的新增過程需要通過WindowSession
完成。
onCreate,onResume,onStart裡面,什麼地方可以獲得寬高
如果在onCreate、onStart、onResume
中直接呼叫View的getWidth/getHeight
方法,是無法得到View寬高的正確資訊,因為view的measure過程與Activity的生命週期是不同步的,所以無法保證在這些生命週期裡view
的measure已經完成。所以很有可能獲取的寬高為0。
所以主要有以下三個方法來獲取view的寬高:
- view.post()方法
在該方法裡的runnable
物件,能保證view已經繪製完成,也就是執行完measure、layout和draw
方法了。
view.post(new Runnable() {
@Override
public void run() {
int width = view.getWidth();
int hight = view.getHeight();
}
});
- onWindowFocusChanged方法
Activity中可以重寫onWindowFocusChanged
方法,該方法表示Activity的視窗得到焦點或者失去焦點的時候,所以Activitiy獲取焦點時,view肯定繪製完成了,這時候獲取寬高也是沒問題的:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getWidth();
int hight = view.getHeight();
}
}
- ViewTreeObserver註冊OnGlobalLayoutListener介面
ViewTreeObserver
是一個觀察者,主要是用來觀察檢視樹的各種變化。OnGlobalLayoutListener
的作用是當View樹的狀態發生改變或者View樹中某view的可見性發生改變時,OnGlobalLayoutListener
的onGlobalLayout方法將會被回撥。因此,此時獲取view的寬高也是可以的。
ViewTreeObserver observer = title_name.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int width = view.getWidth();
int hight = view.getHeight();
}
});
為什麼view.post可以獲得寬高,有看過view.post的原始碼嗎?
能獲取寬高的原因肯定就是因為在此之前view 繪製已經完成,所以View.post()
新增的任務能夠保證在所有 View 繪製流程結束之後才被執行。
看看post的原始碼:
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Assume that post will succeed later
ViewRootImpl.getRunQueue().post(action);
return true;
}
//RunQueue .class
void post(Runnable action) {
postDelayed(action, 0);
}
void postDelayed(Runnable action, long delayMillis) {
HandlerAction handlerAction = new HandlerAction();
handlerAction.action = action;
handlerAction.delay = delayMillis;
synchronized (mActions) {
mActions.add(handlerAction);
}
}
void executeActions(Handler handler) {
synchronized (mActions) {
final ArrayList<HandlerAction> actions = mActions;
final int count = actions.size();
for (int i = 0; i < count; i++) {
final HandlerAction handlerAction = actions.get(i);
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
actions.clear();
}
}
所以在執行View.post()
的方法時,那些Runnable並沒有馬上被執行,而是儲存到RunQueue裡面,然後通過executeActions
方法執行,也就是通過handler,post了一個延時任務Runnable。而executeActions
方法什麼時候會執行呢?
private void performTraversals() {
getRunQueue().executeActions(attachInfo.mHandler);
...
performMeasure();
...
performLayout();
...
performDraw();
}
可以看到在performTraversals
方法中執行了,但是在view繪製之前,這是因為在繪製之前就把需要執行的runnable
封裝成Message傳送到MessageQueue
裡排隊了,但是Looper不會馬上去取這個訊息,因為Looper
會按順序取訊息,主執行緒還有什麼訊息沒執行完呢?其實就是當前的這個performTraversals
所在的任務,所以要等下面的·performMeasure,performLayout,performDraw·都執行完,也就是view繪製完畢了,才會去執行之前我們post的那個runnable,也就是我們能在view.post
方法裡的runnable
能獲取寬高的主要原因了。
SharedPreferences是如何保證執行緒安全的,其內部的實現用到了哪些鎖
SharedPreferences的本質是用鍵值對的方式儲存資料到xml檔案,然後對檔案進行讀寫操作。
- 對於讀操作,加一把鎖就夠了:
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
- 對於寫操作,由於是兩步操作,一個是editor.put,一個是commit或者apply所以其實是需要兩把鎖的:
//第一把鎖,操作Editor類的map物件
public final class EditorImpl implements Editor {
@Override
public Editor putString(String key, String value) {
synchronized (mEditorLock) {
mEditorMap.put(key, value);
return this;
}
}
}
//第二把鎖,操作檔案的寫入
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
是程式安全的嗎?如果是不安全的話我們作為開發人員該怎麼辦?
1) SharedPreferences是程式不安全的,因為沒有使用跨程式的鎖。既然是程式不安全,那麼久有可能在多程式操作的時候發生資料異常。
2) 我們有兩個辦法能保證程式安全:
- 使用跨程式元件,也就是ContentProvider,這也是官方推薦的做法。通過ContentProvider對多程式進行了處理,使得不同程式都是通過ContentProvider訪問SharedPreferences。
- 加檔案鎖,由於SharedPreferences的本質是讀寫檔案,所以我們對檔案加鎖,就能保證程式安全了。
SharedPreferences 操作有檔案備份嗎?是怎麼完成備份的?
- SharedPreferences 的寫入操作,首先是將原始檔備份:
if (!backupFileExists) {
!mFile.renameTo(mBackupFile);
}
- 再寫入所有資料,只有寫入成功,並且通過 sync 完成落盤後,才會將 Backup(.bak) 檔案刪除。
- 如果寫入過程中程式被殺,或者關機等非正常情況發生。程式再次啟動後如果發現該 SharedPreferences 存在 Backup 檔案,就將 Backup 檔案重名為原始檔,原本未完成寫入的檔案就直接丟棄,這樣就能保證之前資料的正確。
為什麼需要外掛化
我覺得最主要的原因是可以動態擴充套件功能。
把一些不常用的功能或者模組做成外掛
,就能減少原本的安裝包大小,讓一些功能以外掛的形式在被需要的時候被載入,也就是實現了動態載入
。
比如動態換膚、節日促銷、見不得人
的一些功能,就可以在需要的時候去下載相應模式的apk,然後再動態載入功能。所以一般這個功能適用於一些平臺類的專案,比如大眾點評美團這種,功能很多,使用者很大概率只會用其中的一些功能,而且這些模組單獨拿出來都可以作為一個app執行。
但是現在用的卻很少了,具體情況見第三點。
外掛化的原理
要實現外掛化,也就是實現從apk讀取所有資料,要考慮三個問題:
讀取外掛程式碼
,完成外掛中程式碼的載入和與主工程的互相呼叫讀取外掛資源
,完成外掛中資源的載入和與主工程的互相訪問四大元件管理
1)讀取外掛程式碼,其實也就是進行外掛中的類載入。所以用到類載入器就可以了。
Android中常用的有兩種類載入器,DexClassLoader
和PathClassLoader
,它們都繼承於BaseDexClassLoader
。區別在於DexClassLoader多傳了一個optimizedDirectory
引數,表示快取我們需要載入的dex檔案的,並建立一個DexFile
物件,而且這個路徑必須為內部儲存路徑。而PathClassLoader
這個引數為null,意思就是不會快取到內部儲存空間了,而是直接用原來的檔案路徑載入。所以DexClassLoader
功能更為強大,可以載入外部的dex檔案。
同時由於雙親委派機制,在構造外掛的ClassLoader
時會傳入主工程的ClassLoader
作為父載入器,所以外掛是可以直接可以通過類名引用主工程的類。
而主工程呼叫外掛則需要通過DexClassLoader
去載入類,然後反射呼叫方法。
2)讀取外掛資源,主要是通過AssetManager
進行訪問。具體程式碼如下:
/**
* 載入外掛的資源:通過AssetManager新增外掛的APK資源路徑
*/
protected void loadPluginResources() {
//反射載入資源
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
}
通過addAssetPath方法把外掛的路徑穿進去,就可以訪問到外掛的資源了。
3)四大元件管理
為什麼單獨說下四大元件呢?因為四大元件不僅要把他們的類載入出來,還要去管理他們的生命週期,在AndroidManifest.xml
中註冊。這也是外掛化中比較重要的一部分。這裡重點說下Activity。
主要實現方法是通過Hook技術,主要的方案就是先用一個在AndroidManifest.xml
中註冊的Activity來進行佔坑,用來通過AMS的校驗,接著在合適的時機用外掛Activity
替換佔坑的Activity
。
Hook 技術又叫做鉤子函式,在系統沒有呼叫該函式之前,鉤子程式就先捕獲該訊息,鉤子函式先得到控制權,這時鉤子函式既可以加工處理(改變)該函式的執行行為,還可以強制結束訊息的傳遞。簡單來說,就是把系統的程式拉出來變成我們自己執行程式碼片段。
這裡的hook其實就是我們常說的下鉤子,可以改變函式的內部行為。
這裡載入外掛Activity用到hook技術,有兩個可以hook的點,分別是:
- Hook IActivityManager
上面說了,首先會在AndroidManifest.xml中註冊的Activity來進行佔坑,然後合適的時機來替換我們要載入的Activity。所以我們主要需要兩步操作:
第一步
:使用佔坑的這個Activity完成AMS驗證。
也就是讓AMS知道我們要啟動的Activity是在xml裡面註冊過的哦。具體程式碼如下:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".contains(method.getName())) {
//換掉
Intent intent = null;
int index = 0;
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if (arg instanceof Intent) {
//說明找到了startActivity的Intent引數
intent = (Intent) args[i];
//這個意圖是不能被啟動的,因為Acitivity沒有在清單檔案中註冊
index = i;
}
}
//偽造一個代理的Intent,代理Intent啟動的是proxyActivity
Intent proxyIntent = new Intent();
ComponentName componentName = new ComponentName(context, proxyActivity);
proxyIntent.setComponent(componentName);
proxyIntent.putExtra("oldIntent", intent);
args[index] = proxyIntent;
}
return method.invoke(iActivityManagerObject, args);
}
第二步
:替換回我們的Activity。
上面一步是把我們實際要啟動的Activity換成了我們xml裡面註冊的activity來躲過驗證,那麼後續我們就需要把Activity換回來。
Activity啟動的最後一步其實是通過H(一個handler)中重寫的handleMessage方法會對LAUNCH_ACTIVITY
型別的訊息進行處理,最終會呼叫Activity的onCreate方法。最後會呼叫到Handler的dispatchMessage
方法用於處理訊息,如果Handler的Callback型別的mCallback
不為null,就會執行mCallback的handleMessage
方法。 所以我們能hook的點就是這個mCallback
。
public static void hookHandler() throws Exception {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Object currentActivityThread= FieldUtil.getField(activityThreadClass ,null,"sCurrentActivityThread");//1
Field mHField = FieldUtil.getField(activityThread,"mH");//2
Handler mH = (Handler) mHField.get(currentActivityThread);//3
FieldUtil.setField(Handler.class,mH,"mCallback",new HCallback(mH));
}
public class HCallback implements Handler.Callback{
//...
@Override
public boolean handleMessage(Message msg) {
if (msg.what == LAUNCH_ACTIVITY) {
Object r = msg.obj;
try {
//得到訊息中的Intent(啟動SubActivity的Intent)
Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
//得到此前儲存起來的Intent(啟動TargetActivity的Intent)
Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
//將啟動SubActivity的Intent替換為啟動TargetActivity的Intent
intent.setComponent(target.getComponent());
} catch (Exception e) {
e.printStackTrace();
}
}
mHandler.handleMessage(msg);
return true;
}
}
用自定義的HCallback來替換mH中的mCallback
即可完成Activity的替換了。
- Hook Instrumentation
這個方法是由於startActivityForResult
方法中呼叫了Instrumentation的execStartActivity
方法來啟用Activity的生命週期,所以可以通過替換Instrumentation
來完成,然後在Instrumentation
的execStartActivity
方法中用佔坑SubActivity
來通過AMS的驗證,在Instrumentation
的newActivity
方法中還原TargetActivity。
public class InstrumentationProxy extends Instrumentation {
private Instrumentation mInstrumentation;
private PackageManager mPackageManager;
public InstrumentationProxy(Instrumentation instrumentation, PackageManager packageManager) {
mInstrumentation = instrumentation;
mPackageManager = packageManager;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
if (infos == null || infos.size() == 0) {
intent.putExtra(HookHelper.TARGET_INTENsT_NAME, intent.getComponent().getClassName());//1
intent.setClassName(who, "com.example.liuwangshu.pluginactivity.StubActivity");//2
}
try {
Method execMethod = Instrumentation.class.getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
return (ActivityResult) execMethod.invoke(mInstrumentation, who, contextThread, token,
target, intent, requestCode, options);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
IllegalAccessException, ClassNotFoundException {
String intentName = intent.getStringExtra(HookHelper.TARGET_INTENT_NAME);
if (!TextUtils.isEmpty(intentName)) {
return super.newActivity(cl, intentName, intent);
}
return super.newActivity(cl, className, intent);
}
}
public static void hookInstrumentation(Context context) throws Exception {
Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
Field mMainThreadField =FieldUtil.getField(contextImplClass,"mMainThread");//1
Object activityThread = mMainThreadField.get(context);//2
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Field mInstrumentationField=FieldUtil.getField(activityThreadClass,"mInstrumentation");//3
FieldUtil.setField(activityThreadClass,activityThread,"mInstrumentation",new InstrumentationProxy((Instrumentation) mInstrumentationField.get(activityThread),
context.getPackageManager()));
}
市面上的一些外掛化方案以及你的想法
前幾年外掛化還是很火的,比如Dynamic-Load-Apk(任玉剛),DroidPlugin,RePlugin(360),VirtualApk(滴滴)
,但是現在機會都沒怎麼在運營了,好多框架都最多隻支援到Android9。
這是為什麼呢?我覺得一個是維護成本太高難以相容,每更新一次原始碼,就要重新維護一次。二就是確實外掛化技術現在用的不多了,以前用外掛化框架幹嘛?主要是比如增加新的功能,讓功能模組之間解耦。現在有RN可以進行外掛化功能,有元件化可以進行專案解耦。所以用的人就不多咯。
雖然外掛化用的不多了,但是我覺得技術還是可以瞭解的,而且熱更新主要用的也是這些技術。方案可以被淘汰,但是技術不會。
參考
多執行緒
記憶體洩露
啟動優化
view.post
view.post
SharedPreferences
總結
希望給大家一點幫助吧,當然文章我也會繼續寫的,感覺大家之前給我點的贊,嘿嘿。
大家一起加油吧!共勉!愛你們!
有一起學習的小夥伴可以關注下❤️。