前言
在上篇 「Sentry 在百瓶的落地實踐」中,筆者主要從方案選型 & 落地實踐兩個大的方面進行了闡述,本篇文章我們主要對 Sentry 在百瓶的落地實踐中遇到的問題進行分析。本文中主要分析的問題主要包括以下幾大類(Flutter SDK 版本為 1.22.6,Dart SDK 版本為 2.10.5):
- NoSuchMethodError
- Flutter 官方 bug (已經修復)
- StateError
- NetworkError(DNS)
NoSuchMethodError
問題一
問題描述:
在進行 List 、String 等型別資料判空處理時,直接使用 xxx.isNotEmpty,沒有進行判斷是否為 null,導致 NoSuchMethodError:The getter isNotEmpty was called null。
問題截圖:
解決方案:
// 問題程式碼
if(timeEndList.isNotEmpty){
...
}
// 解決方案
static bool isNotNullOrEmpty<E>(Iterable<E> iterable) => iterable != null && iterable.isNotEmpty;
if (IterableUtils.isNotNullOrEmpty(timeEndList)){
...
}
在進行判空處理時,需要首先判斷是否為 null,然後再使用 isNotEmpty 進行判斷,避免這種型別錯誤,考慮到我們在專案中會使用大量類似判斷,所以我們可以對同一型別的資料判斷方法進行封裝,避免每處使用都要再去寫一遍。
問題二
問題描述:
這裡是使用了 Future.wait 併發請求多個 API,並且在第二個 API 設定超時,由於第二個 API 請求超時,在後續處理響應時,沒有處理空異常判斷導致獲取不到 code。
問題截圖:
解決方案:
// 問題程式碼
if (res[1].code == HttpCode.ok) {
...
}
// 解決方案
if (res[1]?.code == HttpCode.ok) {
...
}
在使用了 Future.wait 併發請求多個 API ,如果有設定超時處理,要考慮到 API 請求超時失敗的問題儘量避免這種問題發生。
問題三
問題描述:
當我們需要獲取到 與 Widget 上下文相關聯的 RenderBox 尺寸或者位置時,發生錯誤。
問題截圖:
解決方案:
// 問題程式碼
if (IterableUtils.isNotNullOrEmpty(ctx.state.details) == true) {
final RenderBox renderBox = ctx.state.detailsKey.currentContext.findRenderObject();
final Offset postion = renderBox.localToGlobal(Offset.zero);
ctx.dispatch(MallGoodsDetailActionCreator.setDetailsOffsetYAction(postion.dy));
}
// 解決方案
if (IterableUtils.isNotNullOrEmpty(ctx.state.details) == true) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final RenderBox renderBox = ctx.state.detailsKey.currentContext.findRenderObject();
final Offset postion = renderBox.localToGlobal(Offset.zero);
ctx.dispatch(MallGoodsDetailActionCreator.setDetailsOffsetYAction(postion.dy));
});
}
發生以上問題的原因是,上下文並沒有與我們的 state 進行關聯,如果要避免這種情況發生,我們可以在 Widget 渲染完畢後再進行獲取 RenderBox 尺寸或者位置。
Flutter 官方 bug (已經修復)
問題描述:
在使用 NestedScrollView 元件時,由於 position.minScrollExtent 可以為空 ,在生產環境中執行會偶現 NoSuchMethodError nested_scroll_view.dart in _NestedScrollCoordinator.hasScrolledBody NoSuchMethodError: The method '>' was called on null. Receiver: null Tried calling: >() 這個問題,目前官方已經解決並且合併到 master 分支。
問題截圖:
那麼這個問題是如何發生的呢?用官方的原文來解釋就是:
- scheduleAttachRootWidget 將呼叫 _firstBuild 並新建一個具有空畫素的 _NestedScrollPosition;
- FocusManager 將安排一個微任務;
- 完成 firstBuild 然後重新整理 microTask,NestedScrollView 又 dirty 了;
- scheduleWarmUpFrame 將重建 dirty 節點並觸發異常(_NestedScrollPosition 僅在佈局後可用)。
解決方案:
// 問題程式碼
bool get hasScrolledBody {
for (final _NestedScrollPosition position in _innerPositions) {
assert(position.minScrollExtent != null && position.pixels != null);
if (position.pixels > position.minScrollExtent) {
return true;
}
}
return false;
}
// 解決方案
bool get hasScrolledBody {
for (final _NestedScrollPosition position in _innerPositions) {
if (!position.hasContentDimensions || !position.hasPixels) {
continue;
} else if (position.pixels > position.minScrollExtent) {
return true;
}
}
return false;
}
StateError
問題描述:
當我們在使用 list.firstWhere 的時候,通常會引發 Bad State: No element 這類問題。
問題截圖:
解決方案:
// 問題程式碼
Map<String, String> getInitialSkuById(String skuId, List<Map<String, dynamic>> skuList) {
final Map<String, String> selectedKeyValue = <String, String>{};
final Map<String, dynamic> selectedSku =
skuList.firstWhere((Map<String, dynamic> skuItem) => skuItem['id'] == skuId);
if (selectedSku['stockNum'] > 0) {
selectedSku.forEach((String k, dynamic v) {
if (k.contains('keyStr')) {
selectedKeyValue[k] = v;
}
});
}
return selectedKeyValue;
}
// 解決方案
Map<String, String> getInitialSkuById(String skuId, List<Map<String, dynamic>> skuList) {
final Map<String, String> selectedKeyValue = <String, String>{};
final Map<String, dynamic> selectedSku = skuList.firstWhere(
(Map<String, dynamic> skuItem) => skuItem['id'] == skuId,
orElse: null,
);
if (selectedSku != null && selectedSku['stockNum'] > 0) {
selectedSku.forEach((String k, dynamic v) {
if (k.contains('keyStr')) {
selectedKeyValue[k] = v;
}
});
}
return selectedKeyValue;
}
在我們使用 list.firstWhere 的時候,通常有匹配不到條件的時候,這個時候就非常有必要使用 orElse 來進行處理這種情況。
下面的程式碼根據條件篩選為 'green' 的結果值,如果沒有的話就返回 'No matching color found',結果輸出為:No matching color found。
final List<String> list = <String>['red', 'yellow', 'pink', 'blue'];
final String item = list.firstWhere(
(String element) => element == 'green',
orElse: () => 'No matching color found',
);
print(item); // // No matching color found
如果沒有寫 orElse 的情況下會丟擲異常: Unhandled exception: Bad state: No element。當然如果在 Null safety 版本下,可以直接使用 firstWhereOrNull 方法來進行處理。
下面我們來對比一下 firstWhere 和 firstWhereOrNull 的原始碼:
E firstWhere(bool test(E element), {E orElse()?}) {
for (E element in this) {
if (test(element)) return element;
}
if (orElse != null) return orElse();
throw IterableElementError.noElement();
}
T? firstWhereOrNull(bool Function(T element) test) {
for (var element in this) {
if (test(element)) return element;
}
return null;
}
firstWhere 會首先進行匹配符合條件的結果,如果沒有匹配到,再進行處理 orElse ,如果沒有 orElse ,就會丟擲異常;firstWhereOrNull 就簡單的多了,如果沒有匹配到符合條件的值,就會直接返回 null。
NetworkError(DNS)
網路錯誤是導致網路請求失敗的錯誤條件,每個網路錯誤都有一個型別,它是一個字串,每個網路錯誤都有一個階段,它描述了錯誤發生在哪個階段:
- dns:DNS 解析過程中發生的錯誤;
- connection:安全連線建立期間發生的錯誤;
- application:請求和響應傳輸過程中發生的錯誤;
問題描述:
在客戶端向服務單發起網路請求時,都會經過 DNS 解析的過程,一般情況下都是基於 DNS 協議向運營商 Local DNS 發起解析請求的傳統方式,但是這種情況下可能會出現域名劫持和跨網訪問的問題,造成域名解析異常。
解決方案:
那麼,如果我們的 App 在發起網路請求的時候,發現 DNS 解析失敗,我們應該怎麼辦?當然我們可以接入阿里云云解析 DNS 服務或者騰訊移動解析 HTTP DNS 等服務來更加有效的保障 App、小程式正常訪問。
下面我們來一起回顧一下 DNS 相關的知識:
- 什麼是 DNS
- 域名分層結構
- DNS 分層結構
- DNS 解析過程
DNS
DNS 是域名系統 (Domain Name System) 的縮寫,是因特網的一項核心服務,它作為可以將域名和 IP 地址互相對映的一個分散式資料庫,能夠使人更方便的去訪問網際網路,而不用去記住能夠被機器讀取的 IP 數串。
域名分層結構
由於因特網的使用者數量過多,所有因特網在命名時採用的是層次樹狀結構的命名方法。
任何一個連線在因特網上的主機或路由器,都有一個唯一的層次結構(域名)。
域名可以劃分為各個子域,子域還可以繼續劃分為子域的子域,這樣就形成了頂級域名、主域名、子域名等。
- ".com" 是頂級域名;
- "baiping.com" 是主域名(也可稱託管一級域名),主要指企頁名;
- "example.baiping.com" 是子域名(也可稱為託管二級域名);
- "www.example.baiping.com" 是子域名的子域(也可稱為託管三級域名)。
DNS 分層結構
域名是分層結構,域名 DNS 伺服器也是對應的層級結構。有了域名結構,還需要有域名 DNS 伺服器去解析域名,且是需要由遍及全世界的域名 DNS 伺服器去解析,域名 DNS 伺服器實際上就是裝有域名系統的主機。
DNS 解析過程
DNS 查詢的結果通常會在本地域名伺服器中進行快取,如果本地域名伺服器中有快取的情況下,則會跳過如下 DNS 查詢步驟,很快返回解析結果。本地域名伺服器沒有快取的情況下,DNS 查詢所需的 8 個步驟:
- 使用者在 Web 瀏覽器中輸入 "example.com",則由本地域名伺服器開始進行遞迴查詢。
- 本地域名伺服器採用迭代查詢的方法,向根域名伺服器進行查詢;
- 根域名伺服器告訴本地域名伺服器,下一步應該查詢的頂級域名伺服器 .com TLD(頂級域名伺服器)的 IP 地址;
- 本地域名伺服器向頂級域名伺服器 .com TLD 進行查詢;
- .com TLD 伺服器告訴本地域名伺服器,下一步查詢 example.com 權威域名伺服器的 IP 地址;
- 本地域名伺服器向 example.com 權威域名伺服器傳送查詢;
- example.com 權威域名伺服器告訴本地域名伺服器所查詢的主機 IP 地址;
- 本地域名伺服器最後把查詢的IP地址響應給 Web 瀏覽器。一旦 DNS 查詢的 8 個步驟返回了 example.com 的 IP 地址,瀏覽器就能夠發出對網頁的請求;
- 瀏覽器向 IP 地址發出 HTTP 請求;
- 該 IP 處的 Web 伺服器返回要在瀏覽器中呈現的網頁。
名詞解釋:
- DNS Resolve: 指本地域名伺服器,它是 DNS 查詢中的第一站,是負責處理髮出初始請求的 DNS 伺服器。運營商 ISP 分配的 DNS、谷歌 8.8.8.8 等都屬於 DNS Resolver;
- Root Server:指根域名伺服器,當本地域名伺服器在本地查詢不到解析結果時,則第一步會向它進行查詢,並獲取頂級域名伺服器的 IP 地址;
- 遞迴查詢:是指 DNS 伺服器在收到使用者發起的請求時,必須向使用者返回一個準確的查詢結果。如果 DNS 伺服器本地沒有儲存與之對應的資訊,則該伺服器需要詢問其他伺服器,並將返回的查詢結構提交給使用者;
- 迭代查詢:是指 DNS 伺服器在收到使用者發起的請求時,並不直接回複查詢結果,而是告訴另一臺 DNS 伺服器的地址,使用者再向這臺 DNS 伺服器提交請求,這樣依次反覆,直到返回查詢結果。
總結
以上四種異常是我們在編寫程式碼初期經常遇到的問題,通過對以上四種異常的分析,我們可以得到一些經驗總結,在後續的開發中,我們可以根據這些總結,進行改進,以便更好的解決問題。