「 iOS知识小集 」2018 · 第 35 期

2,154 阅读11分钟

原文链接

上周公众号发布的以下文章:

本期知识小集的主要内容包括:

  • 为 Fabric MOD 一个卡顿检测功能
  • 关于定位的一个小知识点
  • iOS 获取设备型号最新总结
  • Storyboard 中的约束优先级
  • UIWindow 的显示特性与常见操作方法小结

为 Fabric MOD 一个卡顿检测功能

作者:hite和落雁

卡顿检测系统,用于检测 App 的主线程运行情况。在追求 N 个 9 奔溃之外,卡顿也是我们极其重要运行指标。

很遗憾,世界上最好的免费 APM 平台 Fabric 却没有。而国内的 bugly、网易云捕等,都提供了类似的功能。如下图是云捕的卡顿功能。

说起来卡顿检测,技术原理很简单,下面是来自 bugly 的 QA 里的描述

iOS 卡顿检查的依据是监控主线程 Runloop 的执行,观察执行耗时是否超过预定阀值(默认阀值为3000ms) 在监控到卡顿时会立即记录线程堆栈到本地,在App从后台切换到前台时,执行上报。

卡顿检测系统,这个大任务,可以分解为两部分:卡顿的检测 + 卡顿的展示和管理。

卡顿的收集

卡顿的收集,有现成的代码,核心代码可查看,gist gist.github.com/hite/1a7ee4…

检测到之后,需要获取当前时刻的堆栈,所有线程的堆栈(其实只需要主线程就够了)。

OK,拿来主义,一个很有名 PLCrashReporter。我自己集成测试,在我的老古董机器,iPhone 6 跑,卡顿是能检测到,但是整个软件基本不可用,整个界面全卡住了。PLCrashReporter 生成日志代码如下图所示。

性能非常差,完全不可用。

拿不到堆栈信息,无法展示,所以只能采用造轮子的方式。根据戴铭 blog 里的例子,我改造了下,如 gist gist.github.com/hite/1a7ee4… 所示。我们收集到了所有堆栈的栈顶地址;接下来我们需要将这些栈信息符号化。

很容易想到的方案是传到自己的服务器上,用 Mac 环境处理堆栈的符号化,转换为可读的堆栈数据——代价太大,而且还不经济。

在浏览了 fabric 的各项 API 后,我发现有一个很讨好的接口,recordCustomExceptionName

符号化堆栈,卡顿管理

fabric 提供了 recordCustomExceptionName 接口,接口签名如下图所示。

我们利用这个接口,将第一步收集的堆栈数据传给 fabric,让 fabric 给我们符号化,而且 fabric 卡顿日志还能够聚合、分类、分组、跟踪。crash 日志的那一套都可以用上, fabric 用户对此是熟悉的。核心代码如下图所示。

至此,我们用很少的代价就做好了一个卡顿检测的系统,并且和奔溃功能一起使用,集中处理 APM 各项指标。

这个方案在月活几十万的 App 应用了快半年了。用户一点都没有感受到有卡顿系统,对现有系统影响很小。如下图所示。

所有的数据都在 crashlytics 里,选择 non-fatal,即可,从统计下来的数据来看,卡顿主要体验在;

  1. 读写文件
  2. 读写数据库
  3. 处理图片
  4. 动画渲染
  5. 在非主线程读一些主线程才能用的属性(奇怪吧?)

基本上和我们猜想是一致的,接下来就需要跟踪和处理这些卡顿。

目前这个系统有两个缺陷:

  1. 检查卡顿本身的 runloop 也被认为是卡顿
  2. 因为 recordCustomExceptionName 接口的限制,所有线程的栈都被合并到一个栈,但不影响核心卡顿代码的阅读。

关于定位的一个小知识点

作者: 高老师很忙

今天分享一个轻松的小知识点~~~

搜索了网上关于 iOS 定位的文章,很多在 locationManager(_:didUpdateLocations:) 收到回调就执行了 stopUpdatingLocation(),如下图:

然而在一些情况之下,这样写是有隐患的(如下图),

在某次运行的时候(并不是每次出现),在 21 点 16 分返回了一个 21 点 09 分的点,这是因为 CoreLocation 可能会返回一个缓存的值给我们,所以我们使用的时候应该判断一下时间戳(如下图),这样可以减少定位偏差。

参考链接:O网页链接

iOS 获取设备型号最新总结

作者: KANGZUBIN

在开发中,我们经常需要获取设备的型号(如 iPhone X,iPhone 8 Plus 等)以进行数据统计,或者做不同的适配。但苹果并没有提供相应的系统 API 让我们直接取得当前设备的型号。

其中,UIDevice 有一个属性 model 只是用于获取 iOS 设备的类型,如 iPhone,iPod touch,iPad 等;而其另一个属性 name 表示当前设备的名称,由用户在设置》通用》关于》名称中设定,如 My iPhone,xxx 的 iPhone 等。然而,我们无法根据这两个值获得具体的型号。

不过,每一种 iOS 设备型号都有对应的一个或多个硬件编码/标识符,称为 device model 或者叫 machine name,之前的小集介绍过,我们可以通过如下图中的代码来获取。

所以,通常的做法是,先获取设备的 device model 值,再手动映射为具体的设备型号(或者直接把device model 值传给后端,让后端去做映射,这样的好处是可以随时兼容新设备)。

例如:去年发布的第一代 iPhone X 对应的 device mode 为 iPhone10,3 和 iPhone10,6,而今年最新发布 iPhone XS 对应 iPhone11,2,iPhone XS Max 对应 iPhone11,4 和 iPhone11,6,iPhone XR 对应 iPhone11,8,完整的 device mode 数据参考 Wiki:www.theiphonewiki.com/wiki/Models

综上,我们可以先获取 device model 值,记为 platform,然后进行对比判断,转换成具体的设备型号,实现代码如下图所示。

备注:图中代码只给了对 iPhone 设备型号的判断,而完整的包括 iPad 和 iPod touch 型号我已经放在 GitHub Gist 上,大家可以参考,详见这里:gist.github.com/kangzubin/5…

参考链接:

Storyboard 中的约束优先级

**作者:**这个汤圆没有馅

在 Masonary 中也可以设置约束的优先级,如make.left.equalTo(weakSelf.view.mas_left).offset(20).priority(250) 中的 priority。

在 Storyboard 中也可以,举个🌰:父视图上有 imgView 和两个 label,现要求两个 label 的宽度随内容且不超出,另必须保证红色 label 中的内容显示完整。如下图。

storyboard 拖控件就不说了,直接从约束开始。

imgView: left、right、top、height、width 绿色label:left、center-y、right、height 红色label:left、center-y、right、height

这个时候 storyboard 会报错,因为两个 label 的宽度无法定位。如下图。

提示说,降低红色 label 的水平方向压缩阻力(即容易被压缩)以确保在其他视图之前可以被裁剪。点击 Change Priority,改变约束优先级。

如上图,我们可以看 Size Inspector 中,红色 label 水平方向压缩阻力由750降为了749,说明在水平方向上,绿色 label 展示的优先级要高于红色 label。当然这和我们一开始的需求反了,待会儿再改。我们先看看 Size Inspector 中的说明。

  • Content Hugging Priority:拉伸阻力,即抗拉伸。值越大,越不容易被拉伸。
  • Content Compression Resistance Priority:压缩阻力,即抗压缩。值越大,越不容易被压缩。
  • Intrinsic Size:控件未设置宽高约束时用的。
  • Ambiguity:解决冲突时是否需要验证。

Priority 的值默认分为三个等级 Required(1000)、High(750)、Low(250),其实可以输入任意其他数字。

好,回到需求,只要把红色 label 的水平方向压缩阻力优先级的值改成任意大于绿色 label压缩阻力的值即可。如果红色 label 的内容太多,那就会把绿色 label 给挤没掉。如下图。

UIWindow 的显示特性与常见操作方法小结

**作者:**陈满iOS

UIWindow 的显示特性

1、相同 windowLevel 下,调整 UIWindow 显示层的基本方法

  1. 显示相关属性:hidden
  • 如果仅仅想显示一个UIWindow
customWindow.hidden = NO;

虽然设置自己的 hidden 即可显示出来,但上述方法并不会"自动"影响之前显示的 UIWindow 对象的 hidden 属性。如果,之前 UIWindow 的 hidden = NO,设置新 UIWindow 的 hidden 将旧 UIWindow 覆盖后,旧 UIWindow 的 hidden 属性依旧为 NO。

  • 如果仅仅想隐藏一个 UIWindow
customWindow.hidden = YES;

如果你没有专门设置过 hidden 属性,系统默认为 YES。上述代码会将 UIWindow 绝对隐藏,不管有没其他 UIWindow 覆盖。当也没有其它非隐藏的 UIWindow 的时候,APP 屏幕完全黑屏。

  • 如果想显示一个UIWindow,同时设置为keyWindow,并将其显示在同一windowLevel的其它任何UIWindow之上
- (void)makeKeyAndVisible

上述方法真的会将其显示在同一windowLevel的其它任何UIWindow之上!显示最上层的UIWindow以最后执行过该代码的UIWindow为准。

  1. 显示相关方法:makeKeyAndVisible 的作用
[self.window makeKeyAndVisible];

其执行效果包括 但不限于 执行了如下代码(因为还会覆盖同 level 的所有 window):

[self.window makeKeyWindow];
self.window.hidden = NO;

讲真,makeKeyAndVisible 真的会自动改变 hidden 属性值为 NO。

  1. UIWindow 对象的 hidden 属性默认值
  • 默认值:true

如果你仅仅创建一个 UIWindow,而又不专门设置 hidden 属性(或者makeKeyAndVisible),系统默认分配的默认值为true。

  1. 误区:关于 keyWindow 的混淆易错点

设置 keyWindow 与否并不影响视图层级显示,仅来接收键盘及其它非触摸事件。如果没有专门设置过 keyWindow 的 hiden 为 NO,而且也没有其它非隐藏的 UIWindow,那么APP会黑屏。

  • 如果仅仅设置为keyWindow
- (void)makeKeyWindow
  • 如果仅仅解除为keyWindow
- (void)resignKeyWindow

app 的 keyWindow 与是否在最上层显示没有任何关系。比如,你如果想通过 [[UIApplication sharedApplication] keyWindow] 获取正在显示的 UIWindow 是极其不准确 的。有时候通过这个代码获取的如果真的是正在显示的 UIWindow,仅仅是因为碰巧而已。

  1. 警惕点:有多个 hidden 属性 =NOUIWindow,该显示谁?

如上所见,makeKeyAndVisible 与 hidden 的 setter 方法均可以改变 hidden 的值,但有个问题,经过多次调整,可能有多个 UIWindow 的 hidden 都为 NO,那么应该显示谁?

  • 对于 hidden 的 setter 方法,最终显示的以最后执行过 .hidden=NO 的 UIWindow 为准,且执行 .hidden=NO 之前 hidden 的值为 YES。(hidden如果是从NO改为NO的不 算 最后 改变UIWindow的显示状态)
  • 对于 makeKeyAndVisible 方法,最终显示的以最后 执行过 makeKeyAndVisible 的 UIWindow 为准。
  • 对于先后分别用 makeKeyAndVisible 方法和 hidden 的 setter 方法,还是先后分别用 hidden 的 setter 方法和 makeKeyAndVisible 方法,结局同样以最后改变显示状态的 UIWindow 为准。

2、基于 windowLevel,调整 UIWindow 显示层的拓展方法

先去 UIWindow.h 里面看看 UIWindowLevel 的定义:

例如,在手势相关类中调整自定义的 UIWindow 层级

[self.window makeKeyAndVisible]; 
_window.windowLevel = UIWindowLevelAlert;
  • 打印代表 UIWindowLevelAlert 层级的数据值
(lldb) po self.window.windowLevel
2000
  • 同理,打印代表UIWindowLevelStatusBar层级的数据值
(lldb) po self.window.windowLevel
1000
  • 同理,打印代表UIWindowLevelNormal层级的数据值
(lldb) po self.window.windowLevel
0

小结:

  1. windowLevel 数值越大的显示在窗口栈的越上面
  2. 显示层的优先级 为: UIWindowLevelAlert > UIWindowLevelStatusBar > UIWindowLevelNormal
  3. 系统给 UIWindow 默认的 windowLevel 为UIWindowLevelNormal

UIWindow常见操作方法总结

1、获取 App 所有 window 的 windows 数组

[[UIApplication sharedApplication] windows]

例如,第三方加载动画框架 KVNProcess 中 KVNProgress.m 文件会有一段这样的代码,如下图1所示

2、keyWindow

[[UIApplication sharedApplication] keyWindow]

例如,第三方下拉菜单框架 FFDropDownMenu 的 FFDropDownMenuView.m 文件中有这样一段代码:

UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
[keyWindow addSubview:self];

这段代码的目的是添加到最上层 UIWindow,但实际操作是把自己的视图添加到 keyWindow 上。其实,如果我们在编写代码时严谨地保证 keyWindow 是显示在最上层的 UIWindow,这样写没有问题。但如果:自己或者其它第三方框架曾经调高过其它 UIWindow 属性 windowLevel,或者有同级 windowLevel 的其它 UIWindow 后来改变过显示状态(如 .hidden=NO,makeKeyAndVisible 等),可能会导致下拉菜单的弹出视图无法显示(被覆盖)。

3、获取 AppDelegate 单例的 window 属性 专门获取 AppDelegate.m 文件中的 window 属性,不包含其它其定义的 window

[[[UIApplication sharedApplication] delegate] window]

关注我们

欢迎关注我们的公众号:iOS-Tips,也欢迎加入我们的群组讨论问题。可以公众号留言 iosflutter 等关键词获取入群方式。

推荐阅读