阅读 1238

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

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

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

  • 再谈数组、集合、字典与 hash、isEqual 方法的关联
  • 使用 Keychain 存储登录态需要注意的一个坑
  • 给 UIView 添加阴影
  • 比较三种网络框架上传图片过程中的不同点?
  • 通过 runtime 控制导航栏的 hidden 属性
  • 关于 IAP 丢单的处理

再谈数组、集合、字典与 hash、isEqual 方法的关联

作者: halohily

我们或多或少了解,Objective-C 中的 NSArray、NSSet、NSDictionary 与 NSObject 及其子类对象的 hash、isEqual 方法有许多联系,这篇小集讲一下其中的一些细节。

NSArray 允许添加重复元素,添加元素时不查重,所以不调用上述两个方法。在移除元素时,会对当前数组内的元素进行遍历,每个元素的 isEqual 方法都会被调用(使用 remove 方法传入的元素作为参数),所有返回真值的元素都被移除。在字典中,不涉及 hash 方法。

NSSet 不允许添加重复元素,所以添加新元素时,该元素的 hash 方法会被调用。若集合中不存在与此元素 hash 值相同的元素,则它直接被加入集合,不调用 isEqual 方法;若存在,则调用集合内的对应元素的 isEqual 方法,返回真值则判等,不加入,处理结束。若返回 false,则判定集合内不存在该元素,将其加入。

从集合中移除元素时,首先调用它的 hash 方法。若集合中存在与其 hash 值相等的元素,则调用该元素的 isEqual 方法,若真值则判等,进行移除;若不存在,则会依次调用集合中每个元素的 isEqual 方法,只要找到一个返回真值的元素,就进行移除,并结束整个过程。(所以这样会有其他满足 isEqual 方法但却被漏掉未被移除的元素)。调用 contains 方法时,过程类似。

因此,若某自定义对象会被加入到集合或作为字典的 key 时,需要同时重写 isEqual 方法和 hash 方法。这样,若集合中某元素存在,则调用它的 contains 和 remove 方法时,可以在 O(1) 完成查询。否则,查询它的时间复杂度提升为 O(n)。

值得注意的是,NSDictionary 的键和值都是对象类型即可。但是被设为键的对象需要遵守 NSCopying 协议。

使用 Keychain 存储登录态需要注意的一个坑

作者: KANGZUBIN

今天要讨论的这个问题你可能永远都不会遇到,而且绝大部分情况下你很难在开发中事先预料到它未来可能会发生,但是一旦不幸发生了,可能就是一个很严重的线上问题,惨痛教训。

我们通常会在 Keychain(钥匙串)中存储一些密码、用户登录态等敏感数据,一是可以提高保存数据的安全性;二是当用户卸载 App 后重新安装,可以自动登录保留上次的登录态;三是同一开发者账号下的不同 App,如果是采用同一套账户体系,就可以通过 Keychain Groups 共享登录态。

我们的 App 之前都是只把用户的登录态保存在 Keychain 中,并在 App 启动时去读取它,这一直也都没什么问题。前一段时间我们的 App 由于业务合规的原因审核被拒,按照苹果的要求不得不把 App 从公司的 A 开发者账号转让到 B 开发者账号下(公司旗下有很多不同主体的开发者账号),转让过程很顺利,但发版后短时间内收到大面积的用户反馈说,更新新版本后提示“登录失效,需要重新登录”。

原因很容易就可以猜到,App 从 A 转让到 B,就无法读取保存在 A 账号下的 Keychain 数据了,用户更新版本覆盖安装后,打开 App 也就无法获取之前的登录态了。

而且对于这种已经发生的问题,我们似乎也没有什么有效的补救措施,临时加急再发一版似乎也解决不了问题,因为之前的 Keychain 数据就是读取不到了,总不能再把 App 转让回去吧,😂

那么如何未雨绸缪预防以后再发生这种因为转让 App 导致存储在 Keychain 中的登录态丢失读取不到呢?(虽然出现转让 App 的概率非常低)

我们在新版本中采用了一种兼容的方法:把用户的登录态同时加密存储在本地缓存(Sandbox)和 Keychain 中,在 App 启动时,优先从 Keychain 中读取,如果 Keychain 中取不到,就从本地缓存中取(然后再把本地缓存的同步到 Keychain 中,因为即使 App 转让了,用户更新版本覆盖安装后 Sandbox 中的数据是不会变的),如果两处都取不到,就认为未登录。

你有没有更好的解决方案?欢迎留言讨论。

另外,有很多人通过 Keychain 来存储设备唯一标示符,也需要注意这个问题。

关于 Keychain 如何使用,可以参考苹果官方文档:GenericKeychain,而关于 Keychain 滥用问题的讨论,可以看 V2EX 的这个帖子

给 UIView 添加阴影

作者: Lefe_x

给 UIView 添加阴影看似简单,如果操作不当也可能会浪费你一些时间。有时候明明添加了阴影可是在 UI 上却没显示出来,尤其涉及到 cell 复用的情况。这里总结几条阴影不显示的原因:

  • 是否设置了 masksToBounds 为 YES,设置为 masksToBounds=YES,阴影不显示;
  • 设置阴影时 view 的 frame 是否为 CGRectZero,如果是,即使设置阴影后修改 frame 不为 CGRectZero 时,也不会显示阴影;
  • 使用自动布局时往往会遇到 frame 为 CGRectZero 时设置阴影无效,这时可以使用 layoutIfNeeded 方法;

通过 layer 设置阴影

// 阴影的颜色
self.imageView.layer.shadowColor = [UIColor blackColor].CGColor;
self.imageView.layer.shadowOpacity = 0.8;
// 阴影的圆角
self.imageView.layer.shadowRadius = 1;
// 阴影偏离的位置 (100, 50) x 方向偏离 100,y 偏离 50 正向,如果是负数正好为相反的方向
self.imageView.layer.shadowOffset = CGSizeMake(3, 4);
复制代码

通过 shadowPath 设置阴影

通过这种方式设置的阴影可以自定义阴影的形状,它会使用在 layer 上设置的属性,比如 shadowRadius。

UIEdgeInsets edges = UIEdgeInsetsMake(15, 10, 15, 10);
UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(-edges.left, -edges.top, CGRectGetWidth(self.imageView.frame) + edges.left + edges.right, CGRectGetHeight(self.imageView.frame) + edges.top + edges.bottom)];
self.imageView.layer.shadowPath = path.CGPath;
复制代码

比较三种网络框架上传图片过程中的不同点?

作者: 陈满iOS

AFNetworking 上传图片的步骤是利用图片设置到 request 的 HTTPBodyStream 中去,然后利用带有图片的 request 新建 task 上传。HYNetworking 内部实现上传图片的时候,其实就是采用 AFNetworking 关于上传图片的 API,都是 AFNetworking 里面一个 API。XMNetworking 上传图片请求也是基于 AFNetworking 上传进行的封装,不过比 HYNetworking 更加隐晦而已,另外它封装了上次图片数组的方法。

AFNetworking

  1. 压缩转换:UIImage 实例对象通过 UIImageJPEGRepresentation (压缩)转换为 NSData,下面称之为 imageData。
  2. 信息整合:将 imageData 与文件名 fileName,文件路径 name,类型名 mimeType 整合成图片模型(AFHTTPBodyPart)的一个对象 bodyPart 中去。
  3. 添加图片模型:将上面新建好的图片模型对象 bodyPart,向图片输入流(AFMultipartBodyStream)的对象 bodyStream 的数组属性(HTTPBodyParts)添加。
  4. 设置 request 的 HTTPBodyStream 属性为 bodyStream:封装为 requestByFinalizingMultipartFormData
  5. 将图片模型对象 formData 用 AFNetwork 的 POST 请求与 uploadTaskWithStreamedRequest 方法进行上传。

HYBNetworking

  1. 压缩转换:UIImage 实例对象通过 UIImageJPEGRepresentation 压缩转换为 NSData,下面称之为 imageData。
  2. 信息整合:利用 AFNetwork的appendPartWithFileData,将 imageData 与文件名 fileName,文件路径 name,类型名 mimeType 整合成图片模型(AFStreamingMultipartFormData)的一个对象 formData 中去。
  3. 将图片模型对象 formData 用 AFNetwork 的 POST 请求与 uploadTaskWithStreamedRequest 方法进行上传。

XMNetworking

  1. 压缩转换:UIImage 实例对象通过 UIImageJPEGRepresentation 压缩转换为 NSData,下面称之为 imageData。
  2. 信息整合:利用 AFNetwork的appendPartWithFileData,将 imageData 与文件名 fileName,文件路径 name,类型名 mimeType 整合成图片模型(XMUploadFormData)的一个对象 formData 中去。
  3. 添加图片模型:向管理器的图片模型数组 uploadFormDatas 添加上面新建好的图片模型对象 formData。
  4. 遍历图片模型数组,获得图片模型,利用 AFNetwork 的 POST 请求与 uploadTaskWithStreamedRequest 方法进行上传。

【总结】 可见,上面三种框架都是基于 AFNetworking 进行的封装,实质的流程还是一样的。上传图片的流程图如下所示。

通过 runtime 控制导航栏的 hidden 属性

在项目中,有时候会遇到某些个页面需要隐藏导航栏,一般情况下,我们会在 viewWillAppear 和 viewDidDisappear 去设置 navigationBar 的 hidden 属性,如图一。

这里介绍另一种方法,通过 runtime 去控制。

建立 vc 的 category,声明 Bool 类型的 hideNavigationBar 属性。主要思路是:重写 initialize 方法,并且自定义一个方法通过 runtime 去替代系统的 viewWillAppear,在该自定义方法里,就是去设置 navigationBar 的 hidden 属性。具体代码如图二。

代码中用到的几个 runtime 方法简单说明下:

//获取类中的方法实现
    class_getMethodImplementation
//替换Method
    method_exchangeImplementations
//获取类中的某个实例方法
    class_getInstanceMethod
//关联对象,相当于 setValue:forKey 
    objc_setAssociatedObject
复制代码

关于使用方法,由于 hideNavigationBar 默认是 NO,所以只在需要隐藏导航栏的页面调用即可,代码如图三。

关于 IAP 丢单的处理

作者: 高老师很忙

做 IAP(In-App Purchase)功能都有可能遇到丢单的问题,丢单是用户已经付款,但是因为某种原因客户端没有办法处理后续的操作,比如说根本没有收到苹果支付成功的回调,或者在与服务器验证票据过程中断网等等。如果处理不好,很容易击溃用户的对产品的信用度。

苹果的推荐做法是合理使用 transaction,在 AppDelegate.m 的 application:didFinishLaunchingWithOptions: 方法里添加 [[SKPaymentQueue defaultQueue] addTransactionObserver:xxx] ;如果还没有调用 [[SKPaymentQueue defaultQueue] finishTransaction:xxx],那么在你下次启动 App 的时候,就会在 paymentQueue:updatedTransactions: 回调里收到未完成的 transaction,然后继续进行处理,所以需要你在合适的时机去调用 finishTransaction 方法,比如说整个支付流程已经完成(包括已经成功验证票据)的时候,这个可以根据你的业务情况来确定调用时机,这样可以大大降低丢单的概率。不要忘了 removeTransactionObserver 哦!

如果觉得处理丢单的时间有点久,可以根据实际情况把相关信息存到本地(如果后续处理流程有需要业务信息的,这种情况是必须要存本地的),在切换到有网或者切换用户或者你觉得合适的实际去处理后续流程,这样也可以双保险,可以把损失降到最低。

关注我们

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

关注下面的标签,发现更多相似文章
评论