Flutter 常见异常分析

公众号名片 作者名片

前言

在上篇 「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。

问题截图:

sentry_no_such_method_error_1 sentry_no_such_method_error_2

解决方案:

// 问题代码
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。

问题截图:

sentry_no_such_method_error_3 sentry_no_such_method_error_4

解决方案:

// 问题代码
if (res[1].code == HttpCode.ok) {
  ...
}

// 解决方案
if (res[1]?.code == HttpCode.ok) {
  ...
}
复制代码

在使用了 Future.wait 并发请求多个 API ,如果有设置超时处理,要考虑到 API 请求超时失败的问题尽量避免这种问题发生。

问题三

问题描述:

当我们需要获取到 与 Widget 上下文相关联的 RenderBox 尺寸或者位置时,发生错误。

问题截图:

sentry_no_such_method_error_5 sentry_no_such_method_error_6

解决方案:

// 问题代码
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 分支。

问题截图:

sentry_nested_scroll_view_error

那么这个问题是如何发生的呢?用官方的原文来解释就是:

  1. scheduleAttachRootWidget 将调用 _firstBuild 并新建一个具有空像素的 _NestedScrollPosition;
  2. FocusManager 将安排一个微任务;
  3. 完成 firstBuild 然后刷新 microTask,NestedScrollView 又 dirty 了;
  4. 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 这类问题。

问题截图:

sentry_state_error_1 sentry_state_error_2

解决方案:

// 问题代码
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)

网络错误是导致网络请求失败的错误条件,每个网络错误都有一个类型,它是一个字符串,每个网络错误都有一个阶段,它描述了错误发生在哪个阶段:

  1. dns:DNS 解析过程中发生的错误;
  2. connection:安全连接建立期间发生的错误;
  3. application:请求和响应传输过程中发生的错误;

问题描述:

在客户端向服务单发起网络请求时,都会经过 DNS 解析的过程,一般情况下都是基于 DNS 协议向运营商 Local DNS 发起解析请求的传统方式,但是这种情况下可能会出现域名劫持和跨网访问的问题,造成域名解析异常。

sentry_network_error_1

解决方案:

那么,如果我们的 App 在发起网络请求的时候,发现 DNS 解析失败,我们应该怎么办?当然我们可以接入阿里云云解析 DNS 服务或者腾讯移动解析 HTTP DNS 等服务来更加有效的保障 App、小程序正常访问。

下面我们来一起回顾一下 DNS 相关的知识:

  • 什么是 DNS
  • 域名分层结构
  • DNS 分层结构
  • DNS 解析过程

DNS

DNS 是域名系统 (Domain Name System) 的缩写,是因特网的一项核心服务,它作为可以将域名和 IP 地址互相映射的一个分布式数据库,能够使人更方便的去访问互联网,而不用去记住能够被机器读取的 IP 数串。

域名分层结构

由于因特网的用户数量过多,所有因特网在命名时采用的是层次树状结构的命名方法。 任何一个连接在因特网上的主机或路由器,都有一个唯一的层次结构(域名)。 域名可以划分为各个子域,子域还可以继续划分为子域的子域,这样就形成了顶级域名、主域名、子域名等。

  1. ".com" 是顶级域名;
  2. "baiping.com" 是主域名(也可称托管一级域名),主要指企页名;
  3. "example.baiping.com" 是子域名(也可称为托管二级域名);
  4. "www.example.baiping.com" 是子域名的子域(也可称为托管三级域名)。

sentry_network_error_2

DNS 分层结构

域名是分层结构,域名 DNS 服务器也是对应的层级结构。有了域名结构,还需要有域名 DNS 服务器去解析域名,且是需要由遍及全世界的域名 DNS 服务器去解析,域名 DNS 服务器实际上就是装有域名系统的主机。

sentry_network_error_3

DNS 解析过程

DNS 查询的结果通常会在本地域名服务器中进行缓存,如果本地域名服务器中有缓存的情况下,则会跳过如下 DNS 查询步骤,很快返回解析结果。本地域名服务器没有缓存的情况下,DNS 查询所需的 8 个步骤:

  1. 用户在 Web 浏览器中输入 "example.com",则由本地域名服务器开始进行递归查询。
  2. 本地域名服务器采用迭代查询的方法,向根域名服务器进行查询;
  3. 根域名服务器告诉本地域名服务器,下一步应该查询的顶级域名服务器 .com TLD(顶级域名服务器)的 IP 地址;
  4. 本地域名服务器向顶级域名服务器 .com TLD 进行查询;
  5. .com TLD 服务器告诉本地域名服务器,下一步查询 example.com 权威域名服务器的 IP 地址;
  6. 本地域名服务器向 example.com 权威域名服务器发送查询;
  7. example.com 权威域名服务器告诉本地域名服务器所查询的主机 IP 地址;
  8. 本地域名服务器最后把查询的IP地址响应给 Web 浏览器。一旦 DNS 查询的 8 个步骤返回了 example.com 的 IP 地址,浏览器就能够发出对网页的请求;
  9. 浏览器向 IP 地址发出 HTTP 请求;
  10. 该 IP 处的 Web 服务器返回要在浏览器中呈现的网页。

名词解释:

  1. DNS Resolve: 指本地域名服务器,它是 DNS 查找中的第一站,是负责处理发出初始请求的 DNS 服务器。运营商 ISP 分配的 DNS、谷歌 8.8.8.8 等都属于 DNS Resolver;
  2. Root Server:指根域名服务器,当本地域名服务器在本地查询不到解析结果时,则第一步会向它进行查询,并获取顶级域名服务器的 IP 地址;
  3. 递归查询:是指 DNS 服务器在收到用户发起的请求时,必须向用户返回一个准确的查询结果。如果 DNS 服务器本地没有存储与之对应的信息,则该服务器需要询问其他服务器,并将返回的查询结构提交给用户;
  4. 迭代查询:是指 DNS 服务器在收到用户发起的请求时,并不直接回复查询结果,而是告诉另一台 DNS 服务器的地址,用户再向这台 DNS 服务器提交请求,这样依次反复,直到返回查询结果。

sentry_network_error_4

总结

以上四种异常是我们在编写代码初期经常遇到的问题,通过对以上四种异常的分析,我们可以得到一些经验总结,在后续的开发中,我们可以根据这些总结,进行改进,以便更好的解决问题。

更多精彩请关注我们的公众号「百瓶技术」,有不定期福利呦!

猜你喜欢

转载自juejin.im/post/7111258964976910367