① android-Ble藍牙開發Demo示例–掃描,連接,發送和接收數據,分包解包(附源碼)
萬物互聯的物聯網時代的已經來臨,ble藍牙開發在其中扮演著舉重若輕的角色。最近剛好閑一點,抽時間梳理下這塊的知識點。
涉及ble藍牙通訊的客戶端(開啟、掃描、連接、發送和接收數據、分包解包)和服務端(初始化廣播數據、開始廣播、配置Services、Server回調操作)整個環節以及一些常見的問題即踩過的一些坑。
比如
1、在Android不同版本或不同手機的適配問題,掃描不到藍牙設備
2、如何避免ble藍牙連接出現133錯誤?
3、單次寫的數據大小有20位元組限制,如何發送長數據
藍牙有傳統(經典)藍牙和低功耗藍牙BLE(Bluetooth Low Energy)之分,兩者的開發的API不一樣,本文主講Ble藍牙開發,傳統藍牙不展開,有需要的可以自行了解。
相對傳統藍牙,BLE低功耗藍牙,主要特點是快速搜索,快速連接,超低功耗保持連接和數據傳輸。
客戶端
服務端
Android4.3(API Level 18)開始引入BLE的核心功能並提供了相應的 API。應用程序通過這些 API 掃描藍牙設備、查詢 services、讀寫設備的 characteristics(屬性特徵)等操作。
BLE藍牙協議是GATT協議, BLE相關類不多, 全都位於android.bluetooth包和android.bluetooth.le包的幾個類:
android.bluetooth.
.BluetoothGattService 包含多個Characteristic(屬性特徵值), 含有唯一的UUID作為標識
.BluetoothGattCharacteristic 包含單個值和多個Descriptor, 含有唯一的UUID作為標識
.BluetoothGattDescriptor 對Characteristic進行描述, 含有唯一的UUID作為標識
.BluetoothGatt 客戶端相關
.BluetoothGattCallback 客戶端連接回調
.BluetoothGattServer 服務端相關
.BluetoothGattServerCallback 服務端連接回調
android.bluetooth.le.
.AdvertiseCallback 服務端的廣播回調
.AdvertiseData 服務端的廣播數據
.AdvertiseSettings 服務端的廣播設置
.BluetoothLeAdvertiser 服務端的廣播
.BluetoothLeScanner 客戶端掃描相關(Android5.0新增)
.ScanCallback 客戶端掃描回調
.ScanFilter 客戶端掃描過濾
.ScanRecord 客戶端掃描結果的廣播數據
.ScanResult 客戶端掃描結果
.ScanSettings 客戶端掃描設置
BLE設備分為兩種設備: 客戶端(也叫主機/中心設備/Central), 服務端(也叫從機/外圍設備/peripheral)
客戶端的核心類是 BluetoothGatt
服務端的核心類是 BluetoothGattServer 和 BluetoothLeAdvertiser
BLE數據的核心類是 BluetoothGattCharacteristic 和 BluetoothGattDescriptor
下面詳細講解下客戶端和服務端的開發步驟流程
安卓手機涉及藍牙許可權問題,藍牙開發需要在AndroidManifest.xml文件中添加許可權聲明:
在搜索設備之前需要詢問打開手機藍牙:
注意: BLE設備地址是動態變化(每隔一段時間都會變化),而經典藍牙設備是出廠就固定不變了!
通過掃描BLE設備,根據設備名稱區分出目標設備targetDevice,下一步實現與目標設備的連接,在連接設備之前要停止搜索藍牙;停止搜索一般需要一定的時間來完成,最好調用停止搜索函數之後加以100ms的延時,保證系統能夠完全停止搜索藍牙設備。停止搜索之後啟動連接過程;
BLE藍牙的連接方法相對簡單只需調用connectGatt方法;
參數說明
與設備建立連接之後與設備通信,整個通信過程都是在BluetoothGattCallback的非同步回調函數中完成;
BluetoothGattCallback中主要回調函數如下:
上述幾個回調函數是BLE開發中不可缺少的;
當調用targetdDevice.connectGatt(context, false, gattCallback)後系統會主動發起與BLE藍牙設備的連接,若成功連接到設備將回調onConnectionStateChange方法,其處理過程如下:
判斷newState == BluetoothGatt.STATE_CONNECTED表明此時已經成功連接到設備;
mBluetoothGatt.discoverServices();
掃描BLE設備服務是安卓系統中關於BLE藍牙開發的重要一步,一般在設備連接成功後調用,掃描到設備服務後回調onServicesDiscovered()函數,函數原型如下:
BLE藍牙開發主要有負責通信的BluetoothGattService完成的。當且稱為通信服務。通信服務通過硬體工程師提供的UUID獲取。獲取方式如下:
具體操作方式如下:
開啟監聽,即建立與設備的通信的首發數據通道,BLE開發中只有當客戶端成功開啟監聽後才能與服務端收發數據。開啟監聽的方式如下:
BLE單次寫的數據量大小是有限制的, 通常是20位元組 ,可以嘗試通過requestMTU增大,但不保證能成功。分包寫是一種解決方案,需要定義分包協議,假設每個包大小20位元組,分兩種包,數據包和非數據包。對於數據包,頭兩個位元組表示包的序號,剩下的都填充數據。對於非數據包,主要是發送一些控制信息。
監聽成功後通過向 writeCharacteristic寫入數據實現與服務端的通信。寫入方式如下:
其中:value一般為Hex格式指令,其內容由設備通信的藍牙通信協議規定;
若寫入指令成功則回調BluetoothGattCallback中的onCharacteristicWrite()方法,說明將數據已經發送給下位機;
若發送的數據符合通信協議,則服務端會向客戶端回復相應的數據。發送的數據通過回調onCharacteristicChanged()方法獲取,其處理方式如下:
通過向服務端發送指令獲取服務端的回復數據,即可完成與設備的通信過程;
當與設備完成通信之後之後一定要斷開與設備的連接。調用以下方法斷開與設備的連接:
源碼上傳在CSDN上了,有需要的可以借鑒。
=====> Android藍牙Ble通訊Demo示例源碼–掃描,連接,發送和接收數據,分包解包
BLE單次寫的數據量大小是有限制的,通常是20位元組,可以嘗試通過requestMTU增大,但不保證能成功。分包寫是一種解決方案,需要定義分包協議,假設每個包大小20位元組,分兩種包,數據包和非數據包。對於數據包,頭兩個位元組表示包的序號,剩下的都填充數據。對於非數據包,主要是發送一些控制信息。
總體流程如下:
1、定義通訊協議,如下(這里只是個舉例,可以根據項目需求擴展)
2、封裝通用發送數據介面(拆包)
該介面根據會發送數據內容按最大位元組數拆分(一般20位元組)放入隊列,拆分完後,依次從隊列里取出發送
3、封裝通用接收數據介面(組包)
該介面根據從接收的數據按協議里的定義解析數據長度判讀是否完整包,不是的話把每條消息累加起來
4、解析完整的數據包,進行業務邏輯處理
5、協議還可以引入加密解密,需要注意的選演算法參數的時候,加密後的長度最好跟原數據長度一致,這樣不會影響拆包組包
一般都是Android版本適配以及不同ROM機型(小米/紅米、華為/榮耀等)(EMUI、MIUI、ColorOS等)的許可權問題
藍牙開發中有很多問題,要靜下心分析問題,肯定可以解決的,一起加油;
② [Android源碼分析] - 非同步通信Handler機制
一、問題:在Android啟動後會在新進程里創建一個主線程,也叫UI線程( 非線程安全 )這個線程主要負責監聽屏幕點擊事件與界面繪制。當Application需要進行耗時操作如網路請求等,如直接在主線程進行容易發生ANR錯誤。所以會創建子線程來執行耗時任務,當子線程執行完畢需要通知UI線程並修改界面時,不可以直接在子線程修改UI,怎麼辦?
解決方法:Message Queue機制可以實現子線程與UI線程的通信。
該機制包括Handler、Message Queue、Looper。Handler可以把消息/ Runnable對象 發給Looper,由它把消息放入所屬線程的消息隊列中,然後Looper又會自動把消息隊列里的消息/Runnable對象 廣播 到所屬線程里的Handler,由Handler處理接收到的消息或Runnable對象。
1、Handler
每次創建Handler對象時,它會自動綁定到創建它的線程上。如果是主線程則默認包含一個Message Queue,否則需要自己創建一個消息隊列來存儲。
Handler是多個線程通信的信使。比如在線程A中創建AHandler,給它綁定一個ALooper,同時創建屬於A的消息隊列AMessageQueue。然後在線程B中使用AHandler發送消息給ALooper,ALooper會把消息存入到AMessageQueue,然後再把AMessageQueue廣播給A線程里的AHandler,它接收到消息會進行處理。從而實現通信。
2、Message Queue
在主線程里默認包含了一個消息隊列不需要手動創建。在子線程里,使用Looper.prepare()方法後,會先檢查子線程是否已有一個looper對象,如果有則無法創建,因為每個線程只能擁有一個消息隊列。沒有的話就為子線程創建一個消息隊列。
Handler類包含Looper指針和MessageQueue指針,而Looper里包含實際MessageQueue與當前線程指針。
下面分別就UI線程和worker線程講解handler創建過程:
首先,創建handler時,會自動檢查當前線程是否包含looper對象,如果包含,則將handler內的消息隊列指向looper內部的消息隊列,否則,拋出異常請求執行looper.prepare()方法。
- 在 UI線程 中,系統自動創建了Looper 對象,所以,直接new一個handler即可使用該機制;
- 在 worker線程 中,如果直接創建handler會拋出運行時異常-即通過查『線程-value』映射表發現當前線程無looper對象。所以需要先調用Looper.prepare()方法。在prepare方法里,利用ThreadLocal<Looper>對象為當前線程創建一個Looper(利用了一個Values類,即一個Map映射表,專為thread存儲value,此處為當前thread存儲一個looper對象)。然後繼續創建handler, 讓handler內部的消息隊列指向該looper的消息隊列(這個很重要,讓handler指向looper里的消息隊列,即二者共享同一個消息隊列,然後handler向這個消息隊列發送消息,looper從這個消息隊列獲取消息) 。然後looper循環消息隊列即可。當獲取到message消息,會找出message對象里的target,即原始發送handler,從而回調handler的handleMessage() 方法進行處理。
- handler與looper共享消息隊列 ,所以handler發送消息只要入列,looper直接取消息即可。
- 線程與looper映射表 :一個線程最多可以映射一個looper對象。通過查表可知當前線程是否包含looper,如果已經包含則不再創建新looper。
5、基於這樣的機制是怎樣實現線程隔離的,即在線程中通信呢。
核心在於 每一個線程擁有自己的handler、message queue、looper體系 。而 每個線程的Handler是公開 的。B線程可以調用A線程的handler發送消息到A的共享消息隊列去,然後A的looper會自動從共享消息隊列取出消息進行處理。反之一樣。
二、上面是基於子線程中利用主線程提供的Handler發送消息出去,然後主線程的Looper從消息隊列中獲取並處理。那麼還有另外兩種情況:
1、主線程發送消息到子線程中;
採用的方法和前面類似。要在子線程中實例化AHandler並設定處理消息的方法,同時由於子線程沒有消息隊列和Looper的輪詢,所以要加上Looper.prepare(),Looper.loop()分別創建消息隊列和開啟輪詢。然後在主線程中使用該AHandler去發送消息即可。
2、子線程A與子線程B之間的通信。
1、 Handler為什麼能夠實現不同線程的通信?核心點在哪?
不同線程之間,每個線程擁有自己的Handler、消息隊列和Looper。Handler是公共的,線程可以通過使用目標線程的Handler對象來發送消息,這個消息會自動發送到所屬線程的消息隊列中去,線程自帶的Looper對象會不斷循環從裡面取出消息並把消息發送給Handler,回調自身Handler的handlerMessage方法,從而實現了消息的線程間傳遞。
2、 Handler的核心是一種事件激活式(類似傳遞一個中斷)的還是主要是用於傳遞大量數據的?重點在Message的內容,偏向於數據傳輸還是事件傳輸。
目前的理解,它所依賴的是消息隊列,發送的自然是消息,即類似事件中斷。
0、 Android消息處理機制(Handler、Looper、MessageQueue與Message)
1、 Handler、Looper源碼閱讀
2、 Android非同步消息處理機制完全解析,帶你從源碼的角度徹底理解
謝謝!
wingjay
![](https://avatars0.githubusercontent.com/u/9619875?v=3&s=460)
③ Android介面回調總結,以及運用到彈窗PopWindow的Demo實現
最近項目中接觸到介面回調,以及Android彈窗PopWindow組件的使用,現在利用學到的知識自己寫了一個簡單的Demo,練習下在Android下如何運用介面回調,來實現彈窗PopWindow的功能。
1. 定義一個介面:OnSelectItemListener。定義一個方法 void selectItem(String name, int type),作為點擊彈窗的每個Item的回調介面。
2. 自定義彈窗類:MyPopupWindow,其布局文件為popup_window.xml。當在MainActivity調用其構造函數創建對象時,同時執行initPopupWindow()函數,給每個Item設置監聽器,監聽點擊Item時,回調介面函數selectItem("Pop Window A", POP_WINDOW_ITEM_1),該函數在MainActivity中實現。
3. 主Activity: MainActivity。其布局文件為一個Button和一個TextView。監聽Button,每當點擊則彈出PopWindow,呈現三個Item。調用MyPopupWindow類中的方法setOnSelectItemListener(OnSelectItemListener listener),傳入OnSelectItemListener 對象作為參數,同時實現回調介面OnSelectItemListener的方法void selectItem(String name, int type)。
主Activity: MainActivity. java
[java] view plain
packagecom.lambdroid.callbacktest2;
importandroid.app.Activity;
importandroid.content.Context;
importandroid.os.Bundle;
importandroid.view.View;
importandroid.widget.Button;
importandroid.widget.TextView;
importandroid.widget.Toast;
//聯系介面的回調以及PopWindow彈窗的簡單使用
{
privateMyPopupWindow myPopupWindow;
privateButton btn_pop_window;
privateTextView tv_display;
protectedContext context;
@Override
protectedvoidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
context =this;
btn_pop_window = (Button) findViewById(R.id.btn_pop_window);
tv_display = (TextView) findViewById(R.id.tv_display);
//給Button設置事件監聽:彈出彈窗
btn_pop_window.setOnClickListener(newView.OnClickListener() {
@Override
publicvoidonClick(View v) {
myPopupWindow.show(btn_pop_window);
}
});
myPopupWindow =newMyPopupWindow(context);
//實現OnSelectItemListener介面的selectItem方法:對於彈窗三個Item的事件監聽
myPopupWindow.setOnSelectItemListener(newOnSelectItemListener() {
@Override
publicvoidselectItem(String name,inttype) {
//點擊電站列表,彈出彈框
if(myPopupWindow !=null&& myPopupWindow.isShowing()) {
myPopupWindow.dismiss();
}
tv_display.setText(name);
switch(type){
caseMyPopupWindow.POP_WINDOW_ITEM_1:
Toast.makeText(context,"我是彈窗A, 我的英文名是"+ name, Toast.LENGTH_SHORT).show();
break;
caseMyPopupWindow.POP_WINDOW_ITEM_2:
Toast.makeText(context,"我是彈窗B, 我的英文名是"+ name, Toast.LENGTH_SHORT).show();
break;
caseMyPopupWindow.POP_WINDOW_ITEM_3:
Toast.makeText(context,"我是彈窗C, 我的英文名是"+ name, Toast.LENGTH_SHORT).show();
break;
default:
break;
}
}
});
}
}
activity_main.xml
[html] view plain
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:id="@+id/btn_pop_window"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="20dp"
android:padding="20dp"
android:text="Pop Window"
android:textSize="20sp"/>
android:id="@+id/tv_display"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="Hello World!"
android:textSize="30sp"/>
自定義彈窗類:MyPopupWindow.java
[java] view plain
packagecom.lambdroid.callbacktest2;
importandroid.app.ActionBar;
importandroid.content.Context;
importandroid.graphics.drawable.ColorDrawable;
importandroid.view.LayoutInflater;
importandroid.view.View;
importandroid.widget.LinearLayout;
importandroid.widget.PopupWindow;
.OnClickListener{
privatePopupWindow mPopWindow;
privateContext mContext;
privateLinearLayout llPop1;
privateLinearLayout llPop2;
privateLinearLayout llPop3;
privateintpw_height;
publicstaticfinalintPOP_WINDOW_ITEM_1 =1;
publicstaticfinalintPOP_WINDOW_ITEM_2 =2;
publicstaticfinalintPOP_WINDOW_ITEM_3 =3;
privateOnSelectItemListener listener;
(OnSelectItemListener listener){
this.listener = listener;
}
publicMyPopupWindow(Context context){
mContext = context;
initPopupWindow();//初始化彈窗
}
publicvoidinitPopupWindow(){
View view = LayoutInflater.from(mContext).inflate(R.layout.popup_window,null);
mPopWindow =newPopupWindow(view, ActionBar.LayoutParams.WRAP_CONTENT, ActionBar.LayoutParams.WRAP_CONTENT,true);
mPopWindow.setOutsideTouchable(true);
/** 為其設置背景,使得其內外焦點都可以獲得 */
mPopWindow.setBackgroundDrawable(newColorDrawable());
mPopWindow.setFocusable(true);
pw_height = view.getHeight();
llPop1 = (LinearLayout) view.findViewById(R.id.ll_pop_1);
llPop1.setOnClickListener(this);
llPop2 = (LinearLayout) view.findViewById(R.id.ll_pop_2);
llPop2.setOnClickListener(this);
llPop3 = (LinearLayout) view.findViewById(R.id.ll_pop_3);
llPop3.setOnClickListener(this);
}
//監聽三個彈窗的點擊事件
@Override
publicvoidonClick(View v) {
switch(v.getId()){
caseR.id.ll_pop_1:
if(listener !=null) {
listener.selectItem("Pop Window A", POP_WINDOW_ITEM_1);//回調介面
}
break;
caseR.id.ll_pop_2:
if(listener !=null) {
listener.selectItem("Pop Window B", POP_WINDOW_ITEM_2);
}
break;
caseR.id.ll_pop_3:
if(listener !=null) {
listener.selectItem("Pop Window C", POP_WINDOW_ITEM_1);
}
break;
default:
break;
}
}
//顯示彈窗,並設置彈窗基於標題欄的顯示位置
publicvoidshow(View view) {
//popupwindow相對view位置x軸偏移量
View viewTemp = mPopWindow.getContentView();
viewTemp.measure(0,0);
intwidth = viewTemp.getMeasuredWidth();
intxOffset = (view.getWidth() - width) /2;
mPopWindow.showAsDropDown(view, xOffset,0);
}
/**
* 退出popupwindow
*/
publicvoiddismiss() {
if(mPopWindow !=null&& mPopWindow.isShowing()) {
mPopWindow.dismiss();
}
}
/**
* popupwindow是否正在顯示
*/
publicbooleanisShowing() {
if(mPopWindow !=null) {
returnmPopWindow.isShowing();
}
returnfalse;
}
}
popup_window.xml
[html] view plain
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
android:id="@+id/ll_alarm_type"
android:layout_width="120dp"
android:layout_height="130dp"
android:orientation="vertical"
android:background="@drawable/popupwindow"
android:paddingBottom="16dp"
android:paddingTop="16dp">
android:id="@+id/ll_pop_1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="5dp"
android:layout_gravity="center_horizontal"
android:orientation="vertical">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="窗口 A"
android:textSize="15sp"
android:textColor="#ffffff"/>
android:id="@+id/ll_pop_2"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_gravity="center_horizontal"
android:orientation="vertical">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="窗口 B"
android:textSize="15sp"
android:textColor="#ffffff"/>
android:id="@+id/ll_pop_3"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_gravity="center_horizontal"
android:orientation="vertical">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="窗口 C"
android:textSize="15sp"
android:textColor="#FFFFFF"/>
回調介面:OnSelectItemListener
[java] view plain
packagecom.lambdroid.callbacktest2;
{
voidselectItem(String name,inttype);
}
點擊Button,彈出彈窗,顯示三個Item
點擊第二個Item,通過回調函數,來實現TextView內容的修改,以及彈出Toast
總結
Java回調情形涉及很多,本文屬於介面的非同步回調:當不知道何時會執行介面的回調函數,(通過介面回調來對獲取到的資源的操作)。除此還有線程間的非同步回調(子線程進行耗時操作,操作完畢通知主線程或將數據傳給主線程處理),以及利用介面回調來實現線程間的數據通信等等(Android可以利用Handler來實現)。等下次再舉例說明Java回調函數的其它情形。
④ Android網路請求庫【OkHttp4.9.3】基本用法與原理分析
OkHttp是一套處理 HTTP 網路請求的依賴庫,由 Square 公司設計研發並開源,目前可以在 Java 和 Kotlin 中使用。對於 Android App 來說,OkHttp 現在幾乎已經占據了所有的網路請求操作,Retrofit + OkHttp實現網路請求似乎成了一種標配。因此它也是每一個 Android 開發工程師的必備技能,了解其內部實現原理可以更好地進行功能擴展、封裝以及優化。
OkHttp的高效性體現在:
第一步:創建OkHttpClient,創建OkHttpClient有兩種方式:
OkHttpClient提供了豐富的配置方法,例如添加攔截器、指定連接池、設置請求超時等等。
第二步:創建請求
使用Request.Builder() 構建Request實例
第三步:發起網路請求
OkHttp支持同步和非同步兩種請求方式
OkHttp的使用方法非常簡單,三步操作就可以發起一個簡單的同步或非同步請求。我們也可以很輕松地對網路請求進行配置,例如添加請求頭、設置請求方式、設置請求超時等等,這些配置參數會在源碼分析過程中詳細介紹。
現在我們已經學會了三步操作發起網路請求,接下來以這三個步驟為切入點,深入到源碼中學習OkHttp的實現原理,廢話少說馬上開車。
OkHttpClient創建方式有兩種,我們看看兩種方式有什麼區別。
第一種直接使用默認構造函數,內部依然是使用建造者模式
第二種使用建造者模式
兩種方式最終都是調用構造函數OkHttpClient(builder:Builder),由參數builder負責所有的參數配置工作。
當您創建單個OkHttpClient實例並將其用於所有 HTTP 調用時,OkHttp 性能最佳。 這是因為每個OkHttpClient都擁有自己的連接池和線程池,重用連接和線程可減少延遲並節省內存。 相反,為每個請求創建一個客戶端會浪費空閑池上的資源。
Request同樣使用建造者模式來創建,這里貼上部分重要源碼,很簡單就不細說了。
OkHttp發起網路請求分為同步請求和非同步請求兩種方式,我們只分析非同步請求流程,因為只要理解了非同步請求過程,基本上也就明白同步請求是怎麼一回事了。
RealCall是連接應用層與網路層的橋梁,負責處理連接、請求、響應和數據流。
Dispatcher維護著一套非同步任務執行策略,分析策略之前先介紹幾個重要概念:
client.dispatcher.enqueue(AsyncCall(responseCallback)) 執行步驟為:
AsyncCall實現了Runnable介面,因此一旦被線程池中的線程處理就會調用它的run()方法:
話休絮煩,我們開始分析攔截器責任鏈:
責任鏈執行流程:首先獲取當前攔截器interceptor,並且調用interceptor.intercept(next)執行攔截器操作。這里的next表示的是index+1後的責任鏈對象,攔截器的intercept()方法內部會調用next.proceed(request)方法再次進入到責任鏈,由於此時index已經加1,所以處理的是下一個攔截器。
如此循環往復,直到處理完責任鏈上最後一個攔截器為止。
注意除最後一個攔截器CallServerInterceptor不會調用chain.proceed(request)方法之外,其他攔截器都應該至少調用一次chain.proceed(request)方法。
為了驗證上面的結論,我們進入到RetryAndFollowUpInterceptor的intercept()方法一探究竟:
可以看到注釋1處重新進入責任鏈處理下一個攔截器。
有興趣可以自行查看最後一個攔截器CallServerInterceptor源碼,此處只給出本人閱讀源碼後得出的結論:
以上就是攔截器責任鏈的工作流程,我們再通過流程圖仔細感受一下。
分析完攔截器責任鏈,我們繼續分析AsyncCall#run()方法:
我們看到,如果()方法成功獲得服務端返回的數據,則調用responseCallback.onResponse(this@RealCall, response)方法完成非同步回調;如果服務端數據獲取失敗(請求異常),則調用responseCallback.onFailure(this@RealCall, canceledException)方法完成非同步回調
需要注意的是,responseCallback回調是在子線程中完成的,所以如果想把數據顯示到UI上,需要切換回主線程進行UI操作。
OkHttp發起網路請求全過程:
【知識點】OkHttp 原理 8 連問