Android面試題收錄及解答10月刊
- 2020 年 10 月 26 日
- 筆記
前言
嗨,大家好,好久不見。這裡跟大家侃侃
這中間發生了什麼。
一個月前呢,想準備面試,就網上隨便找找面試題
什麼的,發現要麼就是賣課的,要麼就是不給詳細回答的或者回答不夠深的(也許是我沒找到😢)。反正稍微有點苦惱,因為我畢竟是個懶人,就想看看面試題,然後自己思考下,順便看看一些參考回答,看看自己回答的全不全面
等等。
於是,我就想乾脆我自己做這個事
吧,就算沒人看,也當我自己每天複習下了。於是,我就建了一個小小公眾號
(小到確實沒人看,哈哈哈),每天去找一些大廠的面試真題
,然後解答下
,然後自己確實也在這個過程中能複習到不少以前沒有重視的問題,今天就總結下之前一個多月總結的面試題,難度不大,大佬可以直接路過,當然發發善心點個贊
也是可以的❤️。
進入正題,下面為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
總結
希望給大家一點幫助吧,當然文章我也會繼續寫的,感覺大家之前給我點的贊,嘿嘿。
大家一起加油吧!共勉!愛你們!
有一起學習的小夥伴可以關注下❤️。