android點三

全局廣播和本地廣播的區(qū)別及原理

區(qū)別及原理

美團(tuán)技術(shù)博客-鎖相關(guān)
美團(tuán)的這篇文章很詳細(xì)宛徊,需要仔細(xì)看晨抡。

Okhttp源碼解析

精品解析Okhttp
Okhttp幾個問題
前言:
Okhttp有幾個核心類,我們需要對其功能有個大致的了解:
①OkHttpClient:整個OkHttp的核心管理類。
②Request:發(fā)送請求封裝類,內(nèi)部有 url 、header苍苞、method、body等常見參數(shù)
③Response:請求的結(jié)果狼纬,包含code羹呵、message、header、body
④RealCall:負(fù)責(zé)請求的調(diào)度(同步走當(dāng)前線程發(fā)送請求,異步使用Okhttp內(nèi)部線程池進(jìn)行)划煮;同時負(fù)責(zé)構(gòu)造內(nèi)部邏輯【責(zé)任鏈】欧募。雖然OkHttpClient是整個Okhttp的核心管理類,但真正發(fā)送請求并組織邏輯的是RealCall類舒憾,它同時肩負(fù)了調(diào)度和責(zé)任鏈組織的兩大重任。有兩個最重要的方法execute()和enqueue(),前者處理同步請求香浩,后者處理異步請求。而這個異步請求种吸,就是通過異步線程和callback做了一個異步調(diào)用的封裝弃衍,最終還是和exucute()方法一樣調(diào)用getResponseWithInterceptorChain()獲得請求結(jié)果。
接下來我們進(jìn)行具體的分析坚俗,這里用的是Okhttp 4.9.1版本镜盯。也是截止到2021/05/16最新的版本岸裙。

OkhttpClient

image.png

我們構(gòu)造OkhttpClient的時候,都是用OkhttpClient.Builder()來構(gòu)造實例的速缆,可以看到這里用的第一個設(shè)計模式就是建造者模式降允。
我們可以使用通過Builder來指定用戶自定義的攔截器、超時時間艺糜,連接池剧董、事件監(jiān)聽器等諸多屬性。最后調(diào)用build()方法來創(chuàng)建OkhttpClient破停。
在其init方法中會創(chuàng)建TrustManager翅楼、sslSockFacotry等(跟https有關(guān))、


image.png

RealCall

我們在發(fā)送請求的時真慢,會調(diào)用OkHttpClient的newCall方法發(fā)送同步或異步請求毅臊。


image.png

此方法實際上就是new了一個RealCall對象。


image.png

在RealCall中有幾個重要的屬性:
image.png

①RealConnectionPool:連接池黑界,負(fù)責(zé)管理和復(fù)用連接
②eventListener:請求回調(diào)管嬉,比如連接請求開始、取消朗鸠、失敗蚯撩、結(jié)束等
③executed:原子類。連接是否已經(jīng)執(zhí)行烛占,每次execute前都會檢查此字段胎挎,避免請求重復(fù)執(zhí)行
④connection:RealConnection類型,代表真實的連接忆家。
其他字段暫不分析呀癣。

具體請求【execute()/enqueue()執(zhí)行流程】

因為execute()和enqueue()最終都是調(diào)用getReponseWithInterceptorsChain()方法。所以兩個方法我們只挑出enqueue進(jìn)行分析弦赖。

image.png

首先就是一個CAS操作项栏,判斷executed字段,也就是請求是否執(zhí)行過蹬竖。然后調(diào)用eventListener通知各個監(jiān)聽者請求開始沼沈。
image.png

接著就是構(gòu)造一個AsyncCall對象塞進(jìn)OkHttpClient的Dispatcher中
image.png

Dispatcher中創(chuàng)建了請求線程池executorService,而AsyncCall實現(xiàn)了Runnable接口币厕,實際上就是將請求放到線程池中處理列另。
image.png

可以看到這里的阻塞隊列用的是SynchronousQueue,一個不存儲元素的阻塞隊列旦装,容量為0页衙,默認(rèn)非公平。
Java常見阻塞隊列詳解
所以具體邏輯還是在AsyncCall中的#run方法中,AsyncCall是RealCall的一個內(nèi)部類店乐,在其構(gòu)造方法中傳入了responseCallback參數(shù)艰躺。
image.png

image.png

可以看到在run方法中通過getResponseWithInterceptorChain()方法拿到請求結(jié)果后,會通過reponseCallback將請求結(jié)果(失敗眨八、成功結(jié)果)通知給上層腺兴。
下面就是OkHttpClient最核心的getResponseWithInterceptorChain()方法的分析了,此方法承載了整個請求的核心邏輯:
image.png

image.png

首先生成了一個Interceptors廉侧,攔截器的List列表页响,按順序依次將:

client.Interceptors
RetryAndFollowUpInterceptor,
BridgeInterceptor
CacheInterceptor
ConnectInterceptor
client.networkInterceptors
CallServerInterceptor

這些攔截器添加到List中,然后創(chuàng)建了一個RealInterceptroChain的類段誊,最后的請求結(jié)果Response通過chain.proceed獲取到闰蚕。
OkHttp將整個請求的復(fù)雜邏輯切成了一個一個的獨立模塊并命名為攔截器(Interceptor),通過責(zé)任鏈的設(shè)計模式串聯(lián)到了一起连舍,最終完成請求獲取響應(yīng)結(jié)果陪腌。 這也是我們看到的第二個設(shè)計模式:責(zé)任鏈模式。
proceed方法:

image.png

其處理流程就是按照責(zé)任鏈添加進(jìn)來的順序依次執(zhí)行各個責(zé)任鏈:
image.png

**每個攔截器在執(zhí)行之前烟瞧,會將剩余尚未執(zhí)行的攔截器組成新的RealInterceptorChain。
攔截器的邏輯被新的責(zé)任鏈調(diào)用next.preceed()切分為start染簇、next.preceed参滴、end這三個部分執(zhí)行。
next.proceed()所代表的其實就是剩余所有攔截器的執(zhí)行邏輯锻弓。
所有攔截器最終會形成一個層層嵌套的嵌套結(jié)構(gòu)砾赔。(N個RealInterceptorChain)
**
拋開用戶自定義的攔截器client.interceptors和client.networkInterceptors,總共添加了五個攔截器:
**
RetryAndFollowUpInterceptor : 失敗和重定向攔截器
BridgeInterceptor : 封裝request和response攔截器
CacheInterceptor : 緩存相關(guān)的過濾器青灼,負(fù)責(zé)讀取緩存直接返回暴心、更新緩存
ConnectInterceptor : 連接服務(wù),負(fù)責(zé)和服務(wù)器建立連接杂拨,真正網(wǎng)絡(luò)請求開始的地方
CallServerInterceptor :執(zhí)行流操作(寫出請求體专普、獲得相應(yīng)數(shù)據(jù))負(fù)責(zé)向服務(wù)器發(fā)送請求數(shù)據(jù)、從服務(wù)器讀取相應(yīng)數(shù)據(jù)弹沽,進(jìn)行http請求報文的封裝和請求報文的解析檀夹。
**
下面我們挨個對每一個攔截器進(jìn)行分析。

1.RetryAndFollowUpInterceptor

image.png

image.png

RetryAndFollowUpInterceptor的#intercept開啟了一while(true)死循環(huán)策橘,并在循環(huán)內(nèi)部完成兩個重要的判定:
①當(dāng)請求內(nèi)部拋出異常時炸渡,判斷是否需要重試
②當(dāng)響應(yīng)結(jié)果是3xx重定向時,構(gòu)建新的請求并發(fā)送請求丽已。

是否重試的邏輯在RetryAndFollowUpInterceptor的#recover方法中:
image.png

判斷邏輯如上圖所示:
①client的retryOnConnectionFailure參數(shù)設(shè)置為false蚌堵,不進(jìn)行重試
②請求的body已經(jīng)發(fā)出,不進(jìn)行重試
③特殊的異常類型不進(jìn)行重試,如ProtocolException吼畏、SSLHandshakeException等
④沒有更多的route督赤,不進(jìn)行重試
前面四條規(guī)則都不符合的請求下,才會重試宫仗。
重定向調(diào)用的是#followUpRequest方法:
image.png

image.png

image.png

image.png

就是根據(jù)各種錯誤碼够挂,重構(gòu)Request進(jìn)行請求,或者直接返回null.

Interceptors和NetworkInterceptors的區(qū)別

image.png

getResponseWithInterceptorChain中之前說過的兩種用戶自義定的攔截器:
interceptors和networkInterceptors從RetryAndFollowUpInterceptor分析后就可以看出區(qū)別了:
從前面添加攔截器的順序可以知道** Interceptors 和 networkInterceptors 剛好一個在 RetryAndFollowUpInterceptor 的前面藕夫,一個在后面孽糖。**

結(jié)合前面的責(zé)任鏈調(diào)用圖可以分析出來,假如一個請求在 RetryAndFollowUpInterceptor 這個攔截器內(nèi)部重試或者重定向了 N 次毅贮,那么其內(nèi)部嵌套的所有攔截器也會被調(diào)用N次办悟,同樣 networkInterceptors 自定義的攔截器也會被調(diào)用 N 次。而相對的 Interceptors 則一個請求只會調(diào)用一次滩褥,所以在OkHttp的內(nèi)部也將其稱之為 Application Interceptor病蛉。

2.BridgeInterceptor

image.png

主要功能如下:
①負(fù)責(zé)把用戶構(gòu)造的請求轉(zhuǎn)換轉(zhuǎn)換為發(fā)送到服務(wù)器的請求、把服務(wù)器返回的響應(yīng)轉(zhuǎn)換為用戶友好的響應(yīng)瑰煎,是從應(yīng)用程序代碼到網(wǎng)絡(luò)代碼的橋梁铺然。(第三個模式:橋接模式)
但實際上看源碼,只看到設(shè)置一些請求頭酒甸,沒看到其他操作魄健。
②設(shè)置內(nèi)容長度,內(nèi)容編碼
③設(shè)置gzip壓縮插勤,并在接收到內(nèi)容后進(jìn)行解壓沽瘦。
④添加cookie
⑤ 設(shè)置其他報文頭,如Keep-Alive,Host等农尖。其中Keep-Alive是實現(xiàn)連接復(fù)用的必要步驟析恋。

image.png

3.CacheInterceptor

具體邏輯如下:

image.png

① 通過Request嘗試到Cache拿緩存。前提是OkHttpClient中配置了緩存盛卡,默認(rèn)是不支持的助隧。Cache中用的是DiskLruCache,也就是磁盤緩存滑沧。


image.png

②根據(jù)當(dāng)前時間now喇颁、request、response創(chuàng)建一個緩存策略嚎货,用戶判斷這樣使用緩存橘霎。這里的緩存工廠:CacheStrategy.Factory用到的是工廠模式(第四個設(shè)計模式)

③如果緩存策略中設(shè)置禁止使用網(wǎng)絡(luò),并且緩存為空殖属,則構(gòu)建一個Response直接返回姐叁,返回碼為504.(網(wǎng)關(guān)超時)

④緩存策略中 不使用網(wǎng)絡(luò),但是有緩存,直接返回緩存


image.png

⑤接著走后續(xù)過濾器的流程外潜,chain.proceed(networkRequest)
⑥當(dāng)緩存存在的時候规婆,如果網(wǎng)絡(luò)返回的Response為304酿炸,則使用緩存的Response


image.png

⑦根據(jù)返回的networkReponse構(gòu)造網(wǎng)絡(luò)網(wǎng)絡(luò)請求的Response


image.png

⑧當(dāng)在OkHttpClient中配置了緩存粹懒,則將這個Response緩存起來增热。
⑨緩存起來的步驟是先緩存header,再緩存body滔驾。
⑩返回Response谒麦。
接下來的兩個Interceptor就是OKHttp中最重要的兩個攔截器了:

4.ConnectInterceptor

image.png

只有簡單的兩句:

    val exchange = realChain.call.initExchange(chain)
    val connectedChain = realChain.copy(exchange = exchange)

調(diào)用RealCall的initExchange初始化Exchange。Exchange中有很多邏輯哆致,涉及了整個網(wǎng)絡(luò)連接建議的過程绕德,包括dns過程和socket連接的過程。(注意摊阀,在javva實現(xiàn)的時候耻蛇,試試從Transmitter獲得了一個新的Exchange對象,之前Exchange)

image.png

** 里面直接new了一個Exchange對象胞此,并傳入了RealCall臣咖、eventListener、exchangeFinder漱牵、codec對象夺蛇。
codec是ExchangeCodec類型的,這是一個接口布疙,有兩個實現(xiàn):
①Http1ExchangeCodec
②Http2ExchangeCodec
分別對應(yīng)Http1協(xié)議和Http2協(xié)議。**
image.png

image.png

Exchange里包含了一個ExchangeCodec對象愿卸,這個對象里面又包含了一個RealConnection對象灵临,RealConnection的屬性成員有socket、handShake趴荸、protocol儒溉、http2Connection等,實際上RealConnection就是一個Socket連接的包裝類发钝。而ExchangeCode對象就是對RealConnection操作(writeRequestHeader顿涣、readResponseHeader)的封裝。
image.png

ConnectInterceptor內(nèi)部完成了socket連接酝豪,其連接對應(yīng)的方法就是ExchangeFinder里的#findHealthyConnection
image.png

image.png

image.png

在findHealthyConnection中會開啟一個死循環(huán)涛碑,調(diào)用findConnection方法獲取一個正確的連接,直到獲取一個正確的連接才會退出孵淘。接下來我們就看socket連接到底是在哪里建立的:
在findConnection中會先從連接池中嘗試獲取連接蒲障,如果能獲取到就返回連接;如果獲取不到,則通過RouteSelector選擇合適的Route(保存請求地址揉阎、代理服務(wù)器等信息)庄撮,然后調(diào)用RealConnection的connect方法。
image.png

其內(nèi)部或調(diào)用connectSocket方法:
image.png

內(nèi)部又調(diào)用:

      Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)

Platform的connectonSocket中會調(diào)用socket.connect進(jìn)行真正的socket連接毙籽。

image.png

在socket連接之前洞斯,還有一個dns的過程,也是隱含在findHealthConnection的內(nèi)部邏輯坑赡。后面再說烙如。

執(zhí)行完ConnectInterceptor之后,其實添加了自定義的網(wǎng)絡(luò)攔截器networkInterceptors垮衷,按照順序執(zhí)行的規(guī)定厅翔,所有的networkInterceptor執(zhí)行執(zhí)行,socket連接其實已經(jīng)建立了搀突,可以通過realChain拿到socket做一些事情了刀闷,這也就是為什么稱之為network Interceptor的原因。

5.CallServerInterceptor

最后一個攔截器仰迁,前面的攔截器已經(jīng)完成了socket連接和TLS連接甸昏,這一步就是傳輸http的頭部和body數(shù)據(jù)。
由以下步驟組成:
①向服務(wù)器發(fā)送request header
②如果有request body徐许,就向服務(wù)器發(fā)送
③讀取response header施蜜,先構(gòu)造一個Response對象
④讀取有reponse body,就在③的基礎(chǔ)上加上body構(gòu)造一個新的Response對象雌隅。

一些其他重要概念分析

1.Connection連接復(fù)用
image.png

在ExchangeFinder的#findConnection方法中翻默,通過RealConnection的connect方法建立socket連接后,RealConnection中持有的socket更新為連接的socket恰起,之后回在臨界區(qū)中將這個newConnection也就是RealConnetion放到connectionPool中修械。
在RealConnectionPool中有一個ConcurrentLinkedQueue類型的connections用倆存儲復(fù)用的RealConnection對象。


image.png
2.DNS過程

DNS過程隱藏在RouteSelector檢查中检盼,我們需要搞明白RouteSelector肯污、RouterSelection、Route三個類的關(guān)系吨枉。


image.png

image.png

①RouteSelector在調(diào)用next遍歷在不同proxy情況下獲得下一個Selection封裝類蹦渣,Selection持有一個Route列表,也就是每個proxy都對應(yīng)有Route列表
②Selection其實就是針對List<Route>封裝的一個迭代器貌亭,通過next()方法獲得下一個Route柬唯,Route持有proxy、address和inetAddress圃庭,可以理解為Route就是針對IP和Proxy配對的一個封裝
③RouteSelector的next()方法內(nèi)部調(diào)用了nextProxy()权逗,nextProxy()內(nèi)部又調(diào)用resetNextInetSocketAddress()方法美尸。


image.png

image.png

④resetNextInetSocketAddress通過address.dns.lookup獲取InetSocketAddress,也就是IP地址斟薇。


image.png

通過上面一系列流程知道师坎,IP地址最終是通過address的dns獲取到的,而這個dns又是怎么構(gòu)建的呢堪滨?
image.png

反向追蹤代碼可以看到就是構(gòu)建OkhttpClient的時候傳遞進(jìn)來的胯陋。默認(rèn)值是
image.png

內(nèi)部的lookup是通過InetAddress.getAllByName方法獲取到對應(yīng)域名的IP,也就是默認(rèn)的DNS實現(xiàn)袱箱。
image.png

涉及到的設(shè)計模式簡單總結(jié)

①責(zé)任鏈遏乔,這個無需多言
②建造者模式:OkHttpClient的構(gòu)造過程
③工廠模式:CacheInterceptor中緩存工廠:CacheStrategy.Factory
④觀察者模式:eventListener中的各種監(jiān)聽回調(diào)
⑤單例模式:Platform中的get()方法,一個懶漢发笔。(其中的connectSocket方法是socket真正connect的地方)

其他

是否使用Http2是由Address中的protocols決定的盟萨,這個List中記錄了客戶端支持的協(xié)議。protocols又取自connectionSpecs字段了讨。由OkHttpClient.Builder中指定connectionSpecs捻激。具體使用的協(xié)議由RealConnection中的Protocol記錄。


image.png

image.png

image.png

image.png

默認(rèn)支持的協(xié)議是TLS_1_3和TLS_1_2


image.png

image.png

Android大圖加載

Android加載大圖前计、多圖策略
我們編寫的應(yīng)用程序都是有一定內(nèi)存限制的胞谭,程序占用了過高的內(nèi)存就容易出現(xiàn)OOM異常,我們可以通過以下代碼查看每個應(yīng)用程序最高可用內(nèi)存是多少

    public static String getDeviceMaxMemory() {
        long maxMemory = Runtime.getRuntime().maxMemory();
        return maxMemory / (8 * 1024 * 1024) + "MB";
    }

因此在展示高分辨率圖片的時候男杈,最好先將圖片進(jìn)行壓縮丈屹。壓縮后的圖片大小應(yīng)該和用來展示它的空間大小相近。
BitmapFactory這個類提供了多個解析方法(decodeByteArray, decodeFile, decodeResource等)用于創(chuàng)建Bitmap對象伶棒,我們應(yīng)該根據(jù)圖片的來源選擇合適的方法旺垒。

image.png

①decodeFile:可以從本地File文件中加載Bitmap
②decodeResource:加載資源文件中的圖片
③decodeByteArray:加載字節(jié)流形式的圖片
④decodeStream:加載網(wǎng)絡(luò)圖片
這些方法均會為已經(jīng)構(gòu)建的bitmap分配內(nèi)存,這是就很容易會導(dǎo)致OOM.
為此每一種解析方法都提供了一種可選的BitmapFactory.Options參數(shù)肤无,將這個參數(shù)的inJustDecodeBounds屬性設(shè)置為true就可以讓解析方法禁止為bitmap分配內(nèi)存先蒋,返回值也不再是一個Bitmap對象,而是null舅锄。雖然是null鞭达,但是BitamapFactory.Options的outWidth司忱、outHeight和outMineType屬性都會被賦值皇忿,這個技巧就可以讓我們在加載圖片之前就獲得圖片的長寬值和MIME類型,從而根據(jù)情況對圖片進(jìn)行壓縮坦仍。
image.png

當(dāng)我們知道圖片大小后鳍烁,就可以決定是把整張圖加載到內(nèi)存中還是加載一個壓縮版的圖片,下面這些因素就是我們接下來要考慮的:
①預(yù)估一下加載整張圖片所需占用的內(nèi)存
②為了加載這一張圖片我們愿意提供多少內(nèi)存
③用于展示這張圖片的控件的實際大小
③當(dāng)前設(shè)備的屏幕尺寸和分辨率

如果將一張1080×720的圖像放到840×480的屏幕并不會得到更好的顯示效果(和840×480的圖像顯示效果是一致的),反而會浪費更多的內(nèi)存繁扎。
那么這時候怎么壓縮呢幔荒?
inSampleSize參數(shù)
通過設(shè)置BitmapFactory.Options中的inSampleSize的值就可以實現(xiàn).inSampleSize即采樣率糊闽,采樣率為1時是原始大小,為2時爹梁,寬高均為原來的二分之一右犹,像素數(shù)和占用內(nèi)存數(shù)就變?yōu)樵瓉淼乃姆种唬蓸勇室话闶?的指數(shù)姚垃,即1.2.4.8...
為甚是2的指數(shù)呢念链,因為源碼里注釋里標(biāo)了:
image.png

inPreferredConfig
這個值是設(shè)置色彩模式,默認(rèn)值是ARGB_8888,在這個模式下积糯,一個像素點占用4個字節(jié)掂墓,如果不對透明度有要求的話,一般采用RGB_565模式看成,這個模式下一個像素點占用2個字節(jié)君编,只解析RGB通道顏色值。
ALPHA_8:每個像素點僅表示alpha的值川慌,不會存儲任何顏色信息吃嘿,占1個字節(jié)。
ARGB_4444:和8888類似窘游,2個字節(jié)唠椭,但圖片分辨率較低,已經(jīng)不推薦使用了忍饰。
并不是每次通過設(shè)置inPreferredConfig都能減少我們Bitmap所占的內(nèi)存贪嫂。
當(dāng)我們圖片是png的時候,我們設(shè)置成什么都沒用艾蓝。
當(dāng)我們圖片是jpg8位力崇,24位,32位時赢织,我們通過設(shè)置inPreferredConfig位RGB_565時砰左,可以減少一半的內(nèi)存占用唆迁。當(dāng)我們解析的圖片是jpg8位時,通過設(shè)置inPreferredConfig位ALPHA_8時可以減少四分之三的內(nèi)存占用。
當(dāng)我們不指定inPreferredConfig的值時笛谦,我們默認(rèn)使用RGB_8888來解碼,當(dāng)指定了的inPreferredConfig值不滿足時绢涡,例如png圖片使用RGB_565來解碼時焙蚓,系統(tǒng)默認(rèn)會選擇RGB_8888來解碼。
接下來我們就可以進(jìn)行具體操作了:

    public static Bitmap ratio(String path, int pixelHeight, int pixelWidth, InputStream inputStream) {
        Log.i(TAG, "ratio: 壓縮圖片...");
        BitmapFactory.Options options = new BitmapFactory.Options();
        //只加載圖片寬高话速,不加載內(nèi)容
        options.inJustDecodeBounds = true;
        //位深度最低
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        if (path != null) {
            File file = new File(path);
            if (file.exists()) {
                //預(yù)加載讶踪,之后就可以獲取到圖片的寬和高
                BitmapFactory.decodeFile(path, options);
            }
        } else if (inputStream != null) {
            Log.i(TAG, "ratio: ");
            BitmapFactory.decodeStream(inputStream, null, options);
        } else {
            Log.i(TAG, "ratio: Failed");
            return null;
        }
        int originalH = options.outHeight;
        int originalW = options.outWidth;
        options.inSampleSize = getSampleSize(originalH, originalW, pixelHeight,
                pixelWidth);
        //一定要記得返回內(nèi)容,不然加個雞兒啊
        options.inJustDecodeBounds = false;
        if (path != null) {
            return BitmapFactory.decodeFile(path, options);
        } else {
            Log.i(TAG, "ratio: decodeStream...");
            return BitmapFactory.decodeStream(inputStream, null, options);
        }
    }
    private static int getSampleSize(int imgHeight, int imgWidth, int viewHeight, int viewWidth) {
        int inSampleSize = 1;
        if (imgHeight > viewHeight || imgWidth > viewWidth) {
            int heightRatio = Math.round((float) imgHeight / (float) viewHeight);
            int  widthRatio = Math.round((float) imgWidth/ (float) viewWidth);
            inSampleSize = Math.min(widthRatio, heightRatio);
        }
        return inSampleSize;
    }

①首先構(gòu)造BitmapFactory.Options泊交,指定inJustDecodeBounds為true乳讥,只加載圖片寬高柱查,不加載到內(nèi)存中。
②接著指定位深度云石,也就是inPreferredConfig為Bitmap.Config.RGB_565.
③然后就可以通過#decodeFile或者#decodeStream等方法預(yù)加載圖片的原始寬高唉工。
拿到圖片原始寬高后,再根據(jù)傳進(jìn)來的控件的寬高計算采樣率汹忠,選擇寬和高中最小的比率作為采樣率值酵紫,這樣能保證圖片最后的寬高一定都會大于等于目標(biāo)的寬、高错维。
④指定好采樣率后奖地,將inJustDecodeBounds置為false,調(diào)用deoceFile等將期望尺寸的圖片加載到內(nèi)存中返回赋焕。

圖片三級緩存

    /**
     * 用戶加載網(wǎng)絡(luò)圖片
     *
     * @param view
     * @param url
     */
    public void displayImage(ImageView view, String url) {
        Bitmap bitmap;
        bitmap = getBitmapFromCache(url);
        Log.i(TAG, "displayImage: bitmap:" + (bitmap == null));
        if (bitmap != null) {
            Log.i(TAG, "displayImage: getBitmap From Cache");
            view.setImageBitmap(bitmap);
            return;
        }
        bitmap = getBitmapFromLocal(url);
        if (bitmap != null) {
            Log.i(TAG, "displayImage: getBitmap From Local");
            view.setImageBitmap(bitmap);
            //從本地取得也要放到緩存中
            putBitmapToCache(url, bitmap);
            return;
        }
        getBitmapFromNet(view, url);
    }

整體思路就是先從LruCache緩存中獲取圖片参歹,如果緩存中直接返回。如果沒有隆判,從本地文件中獲取犬庇,如果本地文件中有,將其加到緩存中并返回侨嘀;如果本地文件中也沒有臭挽,就網(wǎng)絡(luò)請求,并加載到本地文件和緩存中返回咬腕。

Handler源碼解析

Handler源碼分析
Handler 27問

1.創(chuàng)建流程

再創(chuàng)建Handler的時候欢峰,會創(chuàng)建Looper對象,并獲取Looper的MessageQueue消息隊列:


image.png

image.png

其中Looper是從Looper.myLooper()中獲取的:


image.png

image.png

而Looper對象從上面可以看出涨共,是在Looper的#prepare方法中纽帖,new了一個Looper存放到ThreadLocal中,這就保證了每一個線程只有一個唯一的Looper举反。
在Looper的構(gòu)造方法中實例化了兩個重要的參數(shù):

①mQueue:MessageQueue懊直,也就是消息隊列
②mThread:當(dāng)前線程。

image.png

Looper的創(chuàng)建主要有兩種方法:
①Looper.prepare()
②Looper.prepareMainLooper():主線程Looper火鼻,在ActivityThread的mai方法中調(diào)用并創(chuàng)建主線程Looper

2.Looper消息循環(huán)

生成Looper&MessageQueue之后室囊,會自動進(jìn)入消息循環(huán)Looper.loop(),一個隱式操作。(沒找到手動調(diào)用的地方)


image.png

image.png

image.png

在#loop方法中魁索,會開始一個for死循環(huán)融撞,調(diào)用MessageQueue的next方法一直從消息隊列中取出消息,調(diào)用Handler的dispatchMessage方法蛾默,將消息發(fā)送到對應(yīng)的Handler懦铺。最后調(diào)用Message的#recycleUnChecked方法釋放消息占據(jù)的資源捉貌。

特別注意:在進(jìn)行消息分發(fā)時(dispatchMessage(msg))支鸡,會進(jìn)行1次發(fā)送方式的判斷(Activity插件化的重點):
若msg.callback屬性不為空冬念,則代表使用了post(Runnable r)發(fā)送消息,則直接回調(diào)Runnable對象里復(fù)寫的run()
若msg.callback屬性為空牧挣,則代表使用了sendMessage(Message msg)發(fā)送消息急前,則回調(diào)復(fù)寫的handleMessage(msg)


image.png

3.創(chuàng)建消息對象

一般創(chuàng)建消息調(diào)用的是Message.obtain()方法:


image.png

image.png

Message內(nèi)部為了1個Message池,用于Message消息對象的復(fù)用瀑构。使用obtain就是從池中獲取裆针。(說是池,但是這個sPool就是一個靜態(tài)的Message對象)

4.發(fā)送消息

發(fā)送消息調(diào)用的sendMessage方法


image.png

image.png

image.png

最終會拿到當(dāng)前Handler的消息隊列和Message調(diào)用Handler的#enqueueMessage方法
在此方法中會將msg.target復(fù)制為this寺晌,之前說的Looper的#loop方法會通過MessageQueue的next方法后調(diào)用msg.target.dispatchMessage(msg)去處理消息世吨,實際上就是將該消息發(fā)送對應(yīng),也就是當(dāng)前的Handler實例
接著就是調(diào)用消息隊列的enqueueMessage方法:


image.png

image.png

將Message根據(jù)時間放入到消息隊列中呻征。MessageQueue采用單鏈表實現(xiàn)耘婚,提高插入消息、刪除消息的效率陆赋。會判斷消息隊列中有無消息沐祷,若無,則將當(dāng)前插入的消息作為隊頭攒岛,若有消息赖临,則根據(jù)消息創(chuàng)建的事件插入到隊列中。(單鏈表的實現(xiàn)就是Message中持有一個Message類型的mMessages對象灾锯,通過指定此對象的next對象來實現(xiàn)單鏈表)

5.Handler#post(runnable)

image.png

image.png

在#post方法中會調(diào)用getPostMessage方法兢榨,其中會通過Message.obtain方法獲取Message,并指定msg的callback為傳入的runnable對象顺饮。
在Looper#loop方法取出消息調(diào)用Handler的#dispatchMessage將消息發(fā)送給當(dāng)前Handler:


image.png

此方法中會判斷msg.callback是否為空色乾,如果不為空調(diào)用handleCallback方法,否則調(diào)用handleMessage方法领突。

在#handleCallback中就是調(diào)用callback的run方法暖璧,執(zhí)行傳入的Runnable#run里的邏輯


image.png

子線程直接創(chuàng)建Handler會拋出異常,原因就是:
主線程默認(rèn)執(zhí)行了looper.prepare方法君旦,此時使用Handler就可以往相信隊列中不斷進(jìn)行發(fā)送消息和取出消息處理(#loop)澎办,反之沒有執(zhí)行l(wèi)ooper.prepare方法,就會拋出異常金砍,這個在源碼中有所體現(xiàn)局蚀。

6.Looper中的死循環(huán)為什么沒有阻塞主線程?

解析的很清楚
主線程的死循環(huán)一直運行是不是特別消耗CPU資源呢恕稠? 其實不然琅绅,這里就涉及到Linux pipe/epoll機(jī)制,簡單說就是在主線程的MessageQueue沒有消息時鹅巍,便阻塞在loop的queue.next()中的nativePollOnce()方法里千扶,詳情見Android消息機(jī)制1-Handler(Java層)料祠,此時主線程會釋放CPU資源進(jìn)入休眠狀態(tài),直到下個消息到達(dá)或者有事務(wù)發(fā)生澎羞,通過往pipe管道寫端寫入數(shù)據(jù)來喚醒主線程工作髓绽。這里采用的epoll機(jī)制,是一種IO多路復(fù)用機(jī)制妆绞,可以同時監(jiān)控多個描述符顺呕,當(dāng)某個描述符就緒(讀或?qū)懢途w),則立刻通知相應(yīng)程序進(jìn)行讀或?qū)懖僮骼ㄈ模举|(zhì)同步I/O株茶,即讀寫是阻塞的。 所以說图焰,主線程大多數(shù)時候都是處于休眠狀態(tài)忌卤,并不會消耗大量CPU資源。

7.【Handler延遲消息】

image.png

image.png

最終會調(diào)用Handler#enqueueMessage將消息存入消息隊列中楞泼。這個延遲時間驰徊,也就是消息真正要發(fā)送的時間會作為消息的屬性存入到Message的when字段中。
在Looper#loop方法中循環(huán)取數(shù)據(jù)的時候堕阔,會調(diào)用MessageQueue#next方法獲取下一條消息,next中也是一個死循環(huán)獲取消息:
image.png

image.png

判斷當(dāng)前時間是否到達(dá)消息的執(zhí)行時間棍厂,如果沒有到,則調(diào)用nativePollOnce這個native方法進(jìn)行阻塞超陆。直到時間到了消息的執(zhí)行時間牺弹,才將消息返回。
而喚醒則是在消息真正執(zhí)行的地方时呀,也就是MessageQueue#enqueueMessage方法中調(diào)用nativeWake喚醒张漂。沒有消息的時候就阻塞。
image.png

8.【IdleHandler】

IdleHandler介紹
IdleHandler谨娜,它可以在主線程空閑時執(zhí)行任務(wù)航攒,而不影響其他任務(wù)的執(zhí)行。
使用起來很簡單:

       Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle() {
                //此處添加處理任務(wù)
                return false;
            }
        });
使用場景

尋找其合適的使用場景可以從如下幾點出發(fā):
在主線程中干的事趴梢、與消息隊列有關(guān)漠畜、根據(jù)返回值不同做不同的處理
①Activity啟動優(yōu)化:
onCreate/onStart/onResume中耗時較短但非必要的代碼可以放到IdleHandler中執(zhí)行,減少啟動時間
②可以替換View.post()
③發(fā)生一個返回true的IdleHandler坞靶,在里面讓某個View不停閃爍憔狞,這樣當(dāng)用戶發(fā)呆時就可以誘導(dǎo)用戶點擊這個View(實際上就是需要在主線程閑置時做些處理的場景都可以用這個來做)

IdleHandler源碼解析
image.png

可以看到IdleHandler是聲明在MessageQueue的一個靜態(tài)接口,只有一個方法:

queueIdle()彰阴,返回值為true的時候瘾敢,表示IdleHandler存活;返回值為false的時候,則會直接直接移除掉IdleHandler簇抵,不需要我們手動移除庆杜。

image.png

其調(diào)用處理就在MessageQueue#next方法中,當(dāng)沒有消息的時候正压,會遍歷mPendingIdleHandlers列表,調(diào)用IdleHandler的#queueIdle方法责球,同時執(zhí)行完獲取#queueIdle的返回值焦履,如果為false,則從mIdleHandlers移除掉雏逾,這也就是為什么我們不需要手動移除掉IdleHandler嘉裤,只需要在#queueIdle中返回false即可。


image.png

這里的mIdleHandlers和mPendingIdleHandlers栖博,實際上是同一個屑宠,只是中間將這個列表轉(zhuǎn)成數(shù)組處理了。


image.png

9.MessageQueue沒有消息時候會怎樣仇让?阻塞之后怎么喚醒呢典奉?說說pipe/epoll機(jī)制?

當(dāng)消息不可用或者沒有消息的時候就會阻塞在next方法丧叽,而阻塞的辦法是通過pipe/epoll機(jī)制
epoll機(jī)制是一種IO多路復(fù)用的機(jī)制卫玖,具體邏輯就是一個進(jìn)程可以監(jiān)視多個描述符,當(dāng)某個描述符就緒(一般是讀就緒或者寫就緒)踊淳,能夠通知程序進(jìn)行相應(yīng)的讀寫操作假瞬,這個讀寫操作是阻塞的钓瞭。在Android中窜觉,會創(chuàng)建一個Linux管道(Pipe)來處理阻塞和喚醒。

當(dāng)消息隊列為空壕鹉,管道的讀端等待管道中有新內(nèi)容可讀垄开,就會通過epoll機(jī)制進(jìn)入阻塞狀態(tài)琴许。
當(dāng)有消息要處理,就會通過管道的寫端寫入內(nèi)容溉躲,喚醒主線程虚吟。

10.同步屏障和異步消息是怎么實現(xiàn)的?

其實在Handler機(jī)制中签财,有三種消息類型:

①同步消息串慰。也就是普通的消息。
②異步消息唱蒸。通過setAsynchronous(true)設(shè)置的消息邦鲫。
③同步屏障消息。通過postSyncBarrier方法添加的消息,特點是target為空庆捺,也就是沒有對應(yīng)的handler古今。
這三者之間的關(guān)系如何呢?

正常情況下滔以,同步消息和異步消息都是正常被處理捉腥,也就是根據(jù)時間when來取消息,處理消息你画。
當(dāng)遇到同步屏障消息的時候抵碟,就開始從消息隊列里面去找異步消息,找到了再根據(jù)時間決定阻塞還是返回消息坏匪。

也就是說同步屏障消息不會被返回拟逮,他只是一個標(biāo)志,一個工具适滓,遇到它就代表要去先行處理異步消息了敦迄。

所以同步屏障和異步消息的存在的意義就在于有些消息需要“加急處理”

11.同步屏障使用場景

View刷新流程里,ViewRootImpl的scheduleTraversals里的postSyncBarriar

12.【Message是如何復(fù)用的凭迹?】

在Looper#loop方法中取出消息后罚屋,調(diào)用了Message#recycleUnchecked

image.png

在recycleUnchecked方法中,釋放了所有資源嗅绸,然后將當(dāng)前的空消息插入到sPool表頭沿后。
這里的sPool就是一個消息對象池,它也是一個鏈表結(jié)構(gòu)的消息朽砰,最大長度為50尖滚。
然后在Message#obtain方法中,復(fù)用消息對象池里的第一個消息對象瞧柔。

直接復(fù)用消息池sPool中的第一條消息漆弄,然后sPool指向下一個節(jié)點,消息池數(shù)量減一造锅。
image.png

13.可以多次創(chuàng)建Looper嗎撼唾?

當(dāng)然不可以,在Looper#prepare方法中加了判斷哥蔚,如果當(dāng)前線程已經(jīng)創(chuàng)建了looper直接拋出異常:


image.png

14.Looper中的quitAllowed字段是啥倒谷?有什么用?

是否退出消息循環(huán)
Looper#quit方法用到了糙箍,如果這個字段為false渤愁,代表不允許退出,就會報錯深夯。
那么這個quit方法一般是什么時候使用呢抖格?

主線程中诺苹,一般情況下肯定不能退出,因為退出后主線程就停止了雹拄。所以是當(dāng)APP需要退出的時候收奔,就會調(diào)用quit方法,涉及到的消息是EXIT_APPLICATION滓玖,大家可以搜索下坪哄。
子線程中,如果消息都處理完了势篡,就需要調(diào)用quit方法停止消息循環(huán)翩肌。

15.Message是怎么找到它所屬的Handler然后進(jìn)行分發(fā)的?

在loop方法中殊霞,找到要處理的Message摧阅,然后調(diào)用了這么一句代碼處理消息:


image.png

在使用Hanlder發(fā)送消息的時候汰蓉,會設(shè)置msg.target = this绷蹲,所以target就是當(dāng)初把消息加到消息隊列的那個Handler。

16.Handler 的 post(Runnable) 與 sendMessage 有什么區(qū)別

Hanlder中主要的發(fā)送消息可以分為兩種:
post(Runnable)
sendMessage
其實post和sendMessage的區(qū)別就在于:
post方法給Message設(shè)置了一個callback
在dispatchMessage的時候:

image.png

如果msg.callback不為空顾孽,也就是通過post方法發(fā)送消息的時候祝钢,會把消息交給這個msg.callback進(jìn)行處理,然后就沒有后續(xù)了若厚。
如果msg.callback為空拦英,也就是通過sendMessage發(fā)送消息的時候,會判斷Handler當(dāng)前的mCallback是否為空测秸,如果不為空就交給Handler.Callback.handleMessage處理疤估。
如果mCallback.handleMessage返回true,則無后續(xù)了霎冯。
如果mCallback.handleMessage返回false铃拇,則調(diào)用handler類重寫的handleMessage方法。
所以post(Runnable) 與 sendMessage的區(qū)別就在于后續(xù)消息的處理方式沈撞,是交給msg.callback還是 Handler.Callback或者Handler.handleMessage

17.Handler.Callback.handleMessage 和 Handler.handleMessage 有什么不一樣慷荔?為什么這么設(shè)計?

根本在于Handler的兩種創(chuàng)建方式:


image.png

常用的方法就是第1種缠俺,派生一個Handler的子類并重寫handleMessage方法显晶。而第2種就是系統(tǒng)給我們提供了一種不需要派生子類的使用方法,只需要傳入一個Callback即可壹士。

View繪制流程

TODO

Android UI刷新機(jī)制

github解答
參考文章

刷新本質(zhì)流程

通過ViewRootImpl的scheduleTraversals()進(jìn)行界面的三大流程磷雇。
調(diào)用到scheduleTraversals()時不會立即執(zhí)行,而是將該操作保存到待執(zhí)行隊列中躏救。并注冊監(jiān)聽底層的刷新信號倦春。
當(dāng)VSYNC信號到來時,會從待執(zhí)行隊列中取出對應(yīng)的scheduleTraversals()操作,并將其加入到主線程的消息隊列中睁本。

主線程從消息隊列中取出并執(zhí)行三大流程: onMeasure()-onLayout()-onDraw()

同步屏障的作用

同步屏障用于阻塞住所有的同步消息(底層VSYNC的回調(diào)onVsync方法提交的消息是異步消息)
用于保證界面刷新功能的performTraversals()的優(yōu)先執(zhí)行尿庐。

同步屏障的原理?

主線程的Looper會一直循環(huán)調(diào)用MessageQueue的next方法并且取出隊列頭部的Message執(zhí)行呢堰,遇到同步屏障(一種特殊消息)后會去尋找異步消息執(zhí)行抄瑟。如果沒有找到異步消息就會一直阻塞下去,除非將同步屏障取出枉疼,否則永遠(yuǎn)不會執(zhí)行同步消息皮假。
界面刷新操作是異步消息,具有最高優(yōu)先級
我們發(fā)送的消息是同步消息骂维,再多耗時操作也不會影響UI的刷新操作

流程詳解

在Android端惹资,是誰在控制 Vsync 的產(chǎn)生?又是誰來通知我們應(yīng)用進(jìn)行刷新的呢航闺? 在Android中褪测, Vysnc 信號的產(chǎn)生是由底層 HWComposer 負(fù)責(zé)的,SurfaceFlinger 服務(wù)把 VSync 信號通知到應(yīng)用進(jìn)行刷新潦刃,刷新處理是在Java層的 Choreographer 侮措,Android整個屏幕刷新的核心就在于這個 Choreographer
【具體流程】
當(dāng)我們進(jìn)行UI重繪的時候,都會調(diào)用ViewRootImpl#requestLayout

image.png

requestLayout中又會調(diào)用scheduleTraversals()
image.png

可以看到這里這里并沒有立即進(jìn)行重繪乖杠,而是做了兩件事:
①往消息隊列中插入了一條SyncBarrier(同步屏障)
②通過Cherographer post了一個callback

這里的SyncBarrier分扎,也就是同步屏障作用:
①阻止同步消息的執(zhí)行
②優(yōu)先執(zhí)行異步消息
為什么設(shè)計這個同步屏障呢?
主要原因在于提高消息的優(yōu)先級胧洒,保證消息能高優(yōu)先級執(zhí)行畏吓。
之所以沒有在Message中加一個優(yōu)先級的變量,鏈接中也指出卫漫,可能是因為在Android中MessageQueue是一個單鏈表菲饼,整個鏈表的排序是根據(jù)時間進(jìn)行排序的。如果再加入一個優(yōu)先級的排序規(guī)則汛兜,一方面會是排序規(guī)則更為復(fù)雜巴粪,另一方面也會使消息不可控。
小問題:如果在一個方法中連續(xù)調(diào)用了requestLayout多次粥谬,系統(tǒng)會插入多條內(nèi)存屏障或者post多個callback嗎肛根?
答案是不會,因為在執(zhí)行scheduleTraversals前會判斷mTraversalScheduled是否為true漏策。

看一下Choreographer:
主要作用就是進(jìn)行系統(tǒng)協(xié)調(diào)派哲,通過ThreadLocal存儲,每個線程都有一個Choreographer:

image.png

image.png

Choreographer post的callback會放入CallbackQueue里面掺喻,這個CallbackQueue也是一個單鏈表芭届。
Choreographer的postCallback方法會調(diào)用scheduleFrameLocked方法:
image.png

里面會調(diào)用scheduleVsyncLocked()方法:
image.png

內(nèi)部會調(diào)用注冊VSYNC信號監(jiān)聽:每次調(diào)用requestLayout都會主動調(diào)用DisplayEventReceiver的scheduleVsync方法储矩,DisplayEventReceiver的作用就是注冊Vsync信號的監(jiān)聽,當(dāng)下個Vsync信號到來的時候就會通知DisplayEventReceiver褂乍。
接收VSYNC信號的地方在FrameDisplayEventReceiver中的onVsync方法持隧,這個類繼承自DisplayEventReceiver實現(xiàn)了Runnable接口,接收消息后會執(zhí)行run方法里的#doFrame()方法
image.png

image.png

doFrame中主要進(jìn)行了兩部分處理:
①判斷處理一幀的時長逃片,打勇挪Α:
The application may be doing too much work on its main thread
從CallbackQueue中取出到執(zhí)行時間的callback進(jìn)行處理**
這個callback實際上就是TraversalRunnable,也就是我們ViewRootImpl#scheduleTraversals中postCallback傳入的mTraversalRunnable
image.png

TraversalRunnable中會調(diào)用doTraversal
image.png

doTraversal中會移除同步屏障褥实,執(zhí)行#performTraversals
image.png

image.png

performTraversals中處理了大量的邏輯后(是我看源碼中最長的方法呀狼,800多行),會調(diào)用#performDraw開始進(jìn)行真正的界面繪制损离。performDraw中會調(diào)用draw方法哥艇,而draw方法繪制完畢會調(diào)用View的mTreeObserver.dispatchOnDraw回調(diào)給View樹中的onDraw方法。

image.png

image.png

與UI刷新相關(guān)的問題

①我們都知道Android的刷新頻率是60幀/秒僻澎,這是不是意味著每隔16ms就會調(diào)用一次onDraw方法貌踏?
這里60幀/秒是屏幕刷新頻率,但是是否會調(diào)用onDraw()方法要看應(yīng)用是否調(diào)用requestLayout()進(jìn)行注冊監(jiān)聽
②如果界面不需要重繪怎棱,那么16ms到后還會刷新屏幕嗎哩俭?
如果不需要重繪绷跑,那么應(yīng)用就不會收到Vsync信號拳恋,但是還是會進(jìn)行刷新,只不過繪制的數(shù)據(jù)不變而已砸捏;
③我們調(diào)用invalidate()之后會馬上進(jìn)行屏幕刷新嗎谬运?
不會,到等到下一個Vsync信號到來
④我們說丟幀是因為主線程做了耗時操作垦藏,為什么主線程做了耗時操作就會引起丟幀梆暖?
原因是,如果在主線程做了耗時操作掂骏,就會影響下一幀的繪制轰驳,導(dǎo)致界面無法在這個Vsync時間進(jìn)行刷新,導(dǎo)致丟幀了弟灼。
⑤如果在屏幕快要刷新的時候才去OnDraw()繪制级解,會丟幀嗎?
這個沒有太大關(guān)系酷鸦,因為Vsync信號是周期的罐孝,我們什么時候發(fā)起onDraw()不會影響界面刷新屡律;

requestLayout和invalidate的區(qū)別:

requestLayout會直接遞歸調(diào)用父窗口的requestLayout,直到ViewRootImpl,然后觸發(fā)peformTraversals芒划,由于mLayoutRequested為true冬竟,會導(dǎo)致onMeasure和onLayout被調(diào)用,不一定會觸發(fā)OnDraw民逼。requestLayout觸發(fā)onDraw可能是因為在在layout過程中發(fā)現(xiàn)l,t,r,b和以前不一樣泵殴,那就會觸發(fā)一次invalidate,所以觸發(fā)了onDraw拼苍,也可能是因為別的原因?qū)е耺Dirty非空(比如在跑動畫)

view的invalidate不會導(dǎo)致ViewRootImpl的invalidate被調(diào)用袋狞,而是遞歸調(diào)用父view的invalidateChildInParent,直到ViewRootImpl的invalidateChildInParent映屋,然后觸發(fā)peformTraversals苟鸯,會導(dǎo)致當(dāng)前view被重繪,由于mLayoutRequested為false,不會導(dǎo)致onMeasure和onLayout被調(diào)用棚点,而OnDraw會被調(diào)用

BlockCanary

BlockCanary原理解析
BlockCanary源碼解析
我們先看一下原理早处,BlockCanary的原理很簡單:
在Android中,應(yīng)用的卡頓主要是主線程阻塞導(dǎo)致的瘫析。Looper是主線程的消息調(diào)度點砌梆。

image.png

image.png

image.png

就是在Looper#loop方法中,在msg.target.dispatchMessage方法前后會調(diào)用Printer輸出日志贬循,判斷dispatchMessage方法的耗時咸包。我們可以傳入自定義的Printer,置定一個閾值杖虾,如果超過這個時間就可以判斷為主線程阻塞了烂瘫。
這個Printer可以通過Looper的#setMessagLoggging傳入。MainLooper里默認(rèn)是沒有Printer的奇适》乇龋可以在ActivityThread的#main方法中看到:
image.png

image.png

參考第一篇博客,我們就可以自己定義一個Printer嚷往,通過Looper.getMainLooper().setMessageLogging(printer)方法傳入我們的printer葛账,判斷每個方法的耗時情況。
BlockCanary原理圖解:

image.png

源碼分析:

BlockCanary初始化代碼就一句:
BlockCanary.install(context, new AppBlockCanaryContext()).start();
傳入Application的Context以及BlockCanaryContext皮仁,其中BlockCanaryContext可以配置BlockCanary的各種自定義配置參數(shù)籍琳,比如耗時閾值、日志保存目錄贷祈、是否展示通知等趋急。

install流程

image.png

image.png

通過DCL創(chuàng)建BlockCanary單例。


image.png

在BlockCanary的構(gòu)造方法中付燥,創(chuàng)建了核心處理類:BlockCanaryInternals宣谈,其創(chuàng)建也是一個DCL單例。創(chuàng)建了BlockCanaryInternals后键科,會將BlockCanaryContext加到其責(zé)任鏈中闻丑。如何開啟展示前臺通知的開關(guān)漩怎,DisplayService也會被加到責(zé)任鏈中。

看一下BlockCanaryInternals:


image.png

在BlockCanaryInternals中有條責(zé)任鏈mInterceptors負(fù)責(zé)處理相關(guān)流程嗦嗡。
三個參數(shù):
StackSampler:表示線程堆棧采樣
CpuSampler:表示Cpu相關(guān)數(shù)據(jù)采樣
LooperMonitor:其中的#onBlockEvent函數(shù)會在發(fā)生Block的時候勋锤,輸出相關(guān)log

在BlockCanaryInternals的構(gòu)造方法中,會調(diào)用setMonitor方法,監(jiān)聽#onBlcokEven回調(diào)侥祭,監(jiān)聽到block事件后叁执,構(gòu)造BlockInfo也就是阻塞信息,然后交給責(zé)任鏈處理矮冬。實際上這個責(zé)任鏈上只有BlockCanaryContext和DisplayService....ORZ:


image.png

image.png

start流程

image.png

start流程就是調(diào)用Looper.getMainLooper().setMessageLogging()方法傳入自定義的日志輸出類LooperMonitor谈宛。
image.png

image.png

重寫了Printer的println方法:
①在dispatchMessage前執(zhí)行一次println方法,記錄開始時間并調(diào)用startDump方法
②在dispatchMessage后再執(zhí)行一次println方法胎署,并對比執(zhí)行時間
對比的邏輯十分簡單,結(jié)束時間減去開始時間大于預(yù)先設(shè)置的閥值,即可理解發(fā)生block,這時調(diào)用notifyBlockEvent,將發(fā)生block的時間信息回傳給BlockCanaryInternals吆录。就回到上面的邏輯,構(gòu)造BlockInfo交給責(zé)任鏈處理琼牧。

簡要概括:
1.自定義一個Looper的MessageLogging設(shè)置給主線程的Looper
2.在Looper.loop的dispatchMessage方法前打印線程和CPU的堆棧信息
3.在Looper.loop的dispatchMessage方法后判斷是否發(fā)生block
4.發(fā)生block時調(diào)用DisplayService創(chuàng)建NotificationManager消息通過
5.點擊NotificationManager窗口跳轉(zhuǎn)到DisplayActivity,并展示發(fā)生block時的線程堆棧以及CPU堆棧

Rxjava消息訂閱和線程切換

Rxjava消息訂閱和線程切換

Rxjava消息訂閱

1.簡單使用:
①創(chuàng)建被觀察者Observable恢筝,定義要發(fā)送的事件
②創(chuàng)建觀察者Observer,接收事件并作出響應(yīng)操作
③觀察者通過訂閱(subscribe)被觀察者把它們連接到一起
demo:

    private void create() {
        //上游
        Observable.create((ObservableOnSubscribe<String>) emitter -> {
            emitter.onNext("1---秦時明月");
            emitter.onNext("2---空山鳥語");
            emitter.onNext("3---天行九歌");
            emitter.onComplete();
            stringBuffer.append("發(fā)送數(shù)據(jù):" + "\n"
                    + "1---秦時明月" + "\n" + "2---空山鳥語" + "\n" + "3---天行九歌" + "\n"
            );
            Log.i(TAG, "subscribe: .....");
        }).subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<String>() {//通過subscribe連接下游
                    private Disposable mDisposable;
                    private int i = 0;//計數(shù)器

                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.i(TAG, "onSubscribe: ");
                        mDisposable = d;
                    }

                    @Override
                    public void onNext(String s) {
                        Log.i(TAG, "onNext: ");
                        if (i == 0) {
                            stringBuffer.append("接收到的數(shù)據(jù):" + "\n");
                        }
                        stringBuffer.append(s + "\n");
                        i++;//第幾個事件
                        if (i == 2) {
                            setResult();
                            mDisposable.dispose();//阻斷,dls上線巨坊!
                        }
                    }

                    @Override
                    public void onError(Throwable e) {

                    }

                    @Override
                    public void onComplete() {
                        //走不到撬槽,被上面的攔截了
                        Log.i(TAG, "onComplete: " + stringBuffer);

                    }
                });
    }
消息訂閱源碼分析:①創(chuàng)建被觀察者 ②訂閱過程

①創(chuàng)建被觀察者Observable

image.png

創(chuàng)建了一個ObservableCreate并將我們自定義的ObservableOnSubscribe作為參數(shù)傳到ObservableCreate中去。然后調(diào)用RxJavaPlugins.onAssembly方法趾撵。
image.png

ObservableCreate繼承自O(shè)bservalbe侄柔,并且會將我們的ObservalbeOnSubscribe作為source變量存儲起來。
image.png

onAssembly會將我們創(chuàng)建的ObservableCreate給返回鼓寺。

創(chuàng)建過程簡單總結(jié)
Observable.create()中就是把我們自定義的ObservableOnSubscribe對象重新包裝到ObservableCreate對象勋拟,然后返回這個ObservableCreate對象勋磕。

②訂閱流程
訂閱流程也就是Observable.subscribe()

image.png

其核心就在于這兩句:

    observer = RxJavaPlugins.onSubscribe(this, observer);
    subscribeActual(observer);

RxJavaPlugins.onSubscribe將observer返回妈候。
訂閱流程的實現(xiàn)就在#subscribeActual方法中:

image.png

subscribeActual是一個抽象方法,其實現(xiàn)就在我們剛才創(chuàng)建的ObservableCreate中:
image.png

subscribeActual方法中會創(chuàng)建一個CreateEmitter對象挂滓,將我們自己定義的Observer作為參數(shù)傳遞進(jìn)來苦银,實際上就是CreateEmitter會包裝一下observer
image.png

這個CreateEmitter是ObservableCreate的一個靜態(tài)內(nèi)部類,其主要作用就是對事件進(jìn)行攔截赶站、observer的事件回調(diào)等幔虏。
接著回到subscribeActual中,會調(diào)用observer的#onSubscribe方法:
也就是通知觀察者已經(jīng)成功訂閱了被觀察者

接著subscribeActual方法中會繼續(xù)調(diào)用source.subscribe方法贝椿,這里的source就是我們自定義的ObservableOnSubscribe對象想括。
image.png

在我們自定義的ObservableOnSubscribe的#subscribe方法中,我們會通過ObservableEmitter一次調(diào)用onNext和onComplete方法烙博,這里的ObservalbeEmitter接口具體實現(xiàn)為CreateEmitter:
image.png

image.png

在CreateEmitter的onNext瑟蜈、onComplete方法中會調(diào)用我們subscribe傳入的自定義的Observer的onNext烟逊、onComplete方法。這樣一個完整的消息訂閱流程就完成了铺根。在發(fā)送事件前都會調(diào)用isDisposed來判斷是否攔截事件宪躯,這個攔截機(jī)制我們下面分析。

訂閱流程簡單總結(jié)
Observable(被觀察者)和Observer(觀察者)建立連接(訂閱)之后位迂,會創(chuàng)建出一個發(fā)射器CreateEmitter访雪,發(fā)射器會把被觀察者中產(chǎn)生的事件發(fā)送到觀察者中去,觀察者對發(fā)射器中發(fā)出的事件做出響應(yīng)處理掂林〕甲海可以看到,是訂閱之后泻帮,Observable(被觀察者)才會開始發(fā)送事件

切斷流程分析

image.png

切換上層實現(xiàn)就是調(diào)用onSubsribe中傳遞進(jìn)來的Disposable的dispose方法肝陪,攔截掉事件。
image.png

Disposable是一個接口刑顺,只有兩個方法dispose和isDisposed氯窍。具體實現(xiàn)是在CreateEmitter中(CreateEmitter中實現(xiàn)了Disposable接口)
image.png

就是調(diào)用DisposableHelper.dispose(this)。
image.png

DisposableHelper是一個枚舉類蹲堂,并且只有一個值DISPOSED,dispose()方法中會將這個原子引用field設(shè)為DISPOSED狼讨,即標(biāo)記為中斷狀態(tài),后面就可以通過isDisposed方法判斷連接器是否被中斷柒竞,也就是是否會中斷事件分發(fā)政供。
CreateEmitter類中的onNext、onComplete前都會調(diào)用isDisposed來決定是否中斷事件朽基。
如果沒有dispose布隔,observer.onNext()才會被調(diào)用到。
onError()和onComplete()互斥稼虎,只能其中一個被調(diào)用到衅檀,因為調(diào)用了他們的任意一個之后都會調(diào)用dispose()。
先onError()后onComplete()霎俩,onComplete()不會被調(diào)用到哀军。反過來,則會崩潰打却,因為onError()中拋出了異常:RxJavaPlugins.onError(t)杉适。實際上是dispose后繼續(xù)調(diào)用onError()都會炸。

【Rxjava的線程切換】

線程切換操作就兩步:
subscribeOn:指定上游線程
observerOn:指定下游線程

image.png

①Observer(觀察者)的onSubscribe()方法運行在當(dāng)前線程中柳击。
②Observable(被觀察者)中的subscribe()運行在subscribeOn()指定的線程中猿推。
③Observer(觀察者)的onNext()和onComplete()等方法運行在observeOn()指定的線程中。

線程切換的源碼分析也就分為兩部分:subsribeOn()和observerOn()

subscribeOn()

一般使用:
subscribeOn(Schedulers.newThread())
subscribeOn需要傳入一個Scheduler類對象作為參數(shù)捌肴,Scheduler是一個調(diào)度類蹬叭,能延時或者周期性地去執(zhí)行一個任務(wù)毯侦。
Scheduler類型

image.png

Scheduler是一個抽象類,有10個具體實現(xiàn)具垫,其中常用的有:
image.png

IoScheduler侈离、NewThreadScheduler、HandlerScheduler筝蚕;
Schueduler這些子類的基本實現(xiàn)基本差不多卦碾,我們根據(jù)參考博文分析一下Schedulers.io():
image.png

image.png

在Schedulers的靜態(tài)代碼塊中初始了IoScheduler,初始化的時候會創(chuàng)建一個IOTask起宽。
image.png

image.png

可以看到Schedulers.io()使用了靜態(tài)內(nèi)部類的方式來出創(chuàng)建了一個單例IoScheduler對象洲胖。
下面就可以看一下subscribeOn方法了:
image.png

調(diào)用RxJavaPlugins.onAssembly
首先會將當(dāng)前的Observable具體實現(xiàn)為ObservableCreate包裝成一個新的ObservableSubcribeOn對象,Observable中傳入的是ObservalbeOnSubsribe:
image.png

RxJavaPlugins.onAssembly也是將ObservableSubscribeOn對象原樣返回坯沪,看一下ObservableSubscribeOn的構(gòu)造方法:
image.png

就是把source也就是我們的被觀察者Observable以及scheduler保存一下绿映。
subscribeOn就是將我們的Observable以及Scheduler重新包裝成ObservableSubscribeOn,然后返回腐晾。
【ObservableSubscribeOn#subscribeActual】
在之前的subscribe訂閱流程匯總叉弦,具體實現(xiàn)是ObservableCreate類,但在調(diào)用subscribeOn之后藻糖,ObservableCreate對象被封裝成了一個新的ObservableSubscribeOn對象了淹冰,subscribeActual的具體實現(xiàn)也就是在ObservableSubscribeOn中了:
image.png

subscribeActual同樣將我們自定義的Observer包裝成一個新的SubscribeOnObserver對象:
image.png

然后調(diào)用Observer的onSubscribe方法,目前還沒有任何線程切換巨柒,所以O(shè)bserver的onSubscribe方法還是運行在當(dāng)前線程中的樱拴。
最重要的還是subscribeActual最后一行代碼:
parent.setDisposable(scheduler.scheduleDirect(new SubscribeTask(parent)));
首先創(chuàng)建一個SubscribeTask對象,然后調(diào)用scheduler.scheduleDirect().
image.png

此類是ObservableSubscribeOn的內(nèi)部類洋满,實現(xiàn)了Runnable接口晶乔,然后run中調(diào)用了Observer.subscribe

【Scheduler類的scheduleDirect:線程切換的真正邏輯所在】

image.png

image.png

#scheduleDirect中就是在當(dāng)前線程調(diào)用createWorker創(chuàng)建了一個Worker,Worker中可以執(zhí)行Runnable牺勾;然后將Runnable和Workder包裝成一個DisposeTask,然后通過Worker#schedule執(zhí)行這個task正罢。

image.png

image.png

在IoScheduler的createWorker方法中就是new一個EventLoopWorker,并且傳一個Worker緩存池進(jìn)去禽最。不同的Scheduler的createWork中會創(chuàng)建不同類型的Worker腺怯,這點需要注意。
在EventLoopWorker的構(gòu)造方法中會從緩存Worker池中取一個Worker出來川无,然后將Runnable交給這個threadWorkder的#scheduleActual去執(zhí)行。
image.png

Worker的緩沖池對象是CachedWorkPool虑乖,其內(nèi)部的ConcurrentLinkedQueue列表緩存了ThreadWorker懦趋。其#get方法中會判斷如果緩沖池不為空,則從緩沖池中取threadWorker疹味,如果緩沖池為空仅叫,就創(chuàng)建一個ThreadWorker并返回帜篇。
接下來就看一下ThreadWorker的#scheduleActual()里的具體實現(xiàn)了:
image.png

image.png

image.png

image.png

在NewThreadWorker的構(gòu)造方法中會創(chuàng)建一個ScheduledExecutorService對象,通過ScheduledExecutorService來使用線程池诫咱。
在其#sceduleActual中笙隙,會將runnable對象包裝成一個新對象ScheduledRunnable
由是否延遲決定是調(diào)用sumbit還是schedule去執(zhí)行runnable,最終SubscribeTask的run方法就會在線程池中執(zhí)行坎缭,即Observable的subscribe方法會在IO線程執(zhí)行竟痰。

簡單總結(jié):
Observer(觀察者)的onSubscribe()方法運行在當(dāng)前線程中,因為在這之前都沒涉及到線程切換掏呼。
如果設(shè)置了subscribeOn(指定線程)坏快,那么Observable(被觀察者)中subscribe()方法將會運行在這個指定線程中去。

多次設(shè)置subscribeOn的問題

如果多次設(shè)置subscribeOn憎夷,只有第一次有作用莽鸿,其原因如下:
每調(diào)用一次subscribeOn就會被舊的被觀察者也就是ObservableCreate包裝成一個新的被觀察者也就是ObservableSubscribeOn,如果調(diào)用三次拾给,包裝就會變成如下樣式:


image.png

由底層向上層一層一層通知祥得,到了第一層ObservableSubscribeOn的時候(也就是第一次調(diào)用subscribeOn)都會把線程切到IO線程中執(zhí)行,后面也就不會在切其他線程了蒋得。

observeOn()
image.png

image.png

將Observer新包裝成一個ObservableObserveOn對象啃沪,這里被包裝的舊的被觀察者是ObservableSubscribeOn對象(也就是subscribeOn時的包裝)
此時包裝邏輯如下:


image.png

和subscribeOn邏輯類似,線程切換的邏輯主要在ObservableObserverOn(構(gòu)造方式中只有一些賦值操作)當(dāng)中的subscribeActual中
image.png

subscribeActual方法中首先會判斷是否是當(dāng)前新城窄锅,如果是當(dāng)前線程的話创千,直接調(diào)用里面一層的subscribe方法;如果不是當(dāng)前線程入偷,就調(diào)用Scheduler的createWorker方法創(chuàng)建一個Worker追驴,然后將Worker和ObservableObserverOn包裝成ObserverOnObserver,調(diào)用source也就是上游的IO線程中的ObservableSubscribeOn的subscribe方法將事件逐一發(fā)送疏之,此時包裝為:


image.png

ObserverOnObserver是ObservableObserverOn的一個靜態(tài)內(nèi)部類:
image.png

ObserverOnObserver的onNext()殿雪、onComplete處理邏輯差不多,我們主要看一下onNext:
image.png

onNext中首先通過CAS操作保持并發(fā)的安全性锋爪,然后調(diào)用workder的#schedule方法丙曙,因為我們上面用的是Schedulers.mainThread(),其實現(xiàn)就是HandlerThread
image.png

HandlerScheduler的scedule中就是通過Handler來發(fā)送Message來講消息從IO線程發(fā)送到主線程中。最終還是在主線程中調(diào)用ObserverOnObserver的run方法
image.png

image.png

會調(diào)用drainNormal方法(當(dāng)前已經(jīng)運行在主線程中了)其骄,方法內(nèi)部會開啟一個死循環(huán)亏镰,從隊列中也就是SimpleQueue中一直取消息,然后調(diào)用Observer的onNext方法拯爽,也就是在observerOn中指定的線程中調(diào)用了索抓,這里就是主線程。

【 Activity、Window逼肯、View三者關(guān)注】

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末耸黑,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子篮幢,更是在濱河造成了極大的恐慌大刊,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件三椿,死亡現(xiàn)場離奇詭異缺菌,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帜讲,“玉大人尸昧,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長租冠。 經(jīng)常有香客問我,道長薯嗤,這世上最難降的妖魔是什么顽爹? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮骆姐,結(jié)果婚禮上镜粤,老公的妹妹穿的比我還像新娘。我一直安慰自己玻褪,他們只是感情好肉渴,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著带射,像睡著了一般同规。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上窟社,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天券勺,我揣著相機(jī)與錄音,去河邊找鬼灿里。 笑死关炼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的钠四。 我是一名探鬼主播盗扒,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼跪楞,長吁一口氣:“原來是場噩夢啊……” “哼缀去!你這毒婦竟也來了侣灶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤缕碎,失蹤者是張志新(化名)和其女友劉穎褥影,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咏雌,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡凡怎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了赊抖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片统倒。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖氛雪,靈堂內(nèi)的尸體忽然破棺而出房匆,到底是詐尸還是另有隱情,我是刑警寧澤报亩,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布浴鸿,位于F島的核電站,受9級特大地震影響弦追,放射性物質(zhì)發(fā)生泄漏岳链。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一劲件、第九天 我趴在偏房一處隱蔽的房頂上張望掸哑。 院中可真熱鬧,春花似錦零远、人聲如沸苗分。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俭嘁。三九已至,卻和暖如春服猪,著一層夾襖步出監(jiān)牢的瞬間供填,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工罢猪, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留近她,地道東北人。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓膳帕,卻偏偏與公主長得像粘捎,于是被迫代替她去往敵國和親薇缅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容