注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

《环信十周年趴——程序之路也有得失,不必介怀》

我的程序生涯可谓是充满了曲折和成长的旅程。从一开始的业余爱好,到如今的职业发展,我经历了许多挑战和机遇,也积累了不少宝贵的经验。回顾起来,我最初接触编程是在大学期间。当时,我被计算机的神秘和无限可能性所吸引,开始学习编写简单的代码。起初,我对编程还不太熟悉,但...
继续阅读 »


我的程序生涯可谓是充满了曲折和成长的旅程。从一开始的业余爱好,到如今的职业发展,我经历了许多挑战和机遇,也积累了不少宝贵的经验。

回顾起来,我最初接触编程是在大学期间。当时,我被计算机的神秘和无限可能性所吸引,开始学习编写简单的代码。起初,我对编程还不太熟悉,但是通过不断的学习和实践,我渐渐掌握了一些基本的编程语言和技巧。

毕业后,我决定将编程作为我的职业。我投身于软件开发行业,从一名初级程序员开始。在职场中,我面对了各种项目和团队合作的挑战。通过不断学习和与同事的交流,我的编程能力得到了提升,我也逐渐熟悉了软件开发的流程和方法。

随着时间的推移,我逐渐担任更高级的职位,并开始负责一些重要项目的开发。同时,我也积极追求自我提升,不断学习新的编程技术和工具。我学习了机器学习和数据科学的知识,掌握了一些流行的开发框架和库。这些新技能不仅提升了我的职业竞争力,还让我能够解决更加复杂的问题。

在职场上,我也遇到了一些奇葩和不愉快的经历。有一次,我加入了一家初创公司,他们开发了一款虚拟现实游戏。我被聘为首席程序员,负责游戏的核心功能开发。开始时,我对这个机会充满了期待,希望能够在这个新兴行业有所突破。

然而,不久之后,我发现这家公司的管理层存在着一些奇葩的决策和不合理的要求。他们对于开发进度的期望过高,要求我们在短时间内完成大量的工作。同时,他们也没有给予足够的资源和支持,导致我们在技术上遇到了很多困难。

更糟糕的是,公司的管理层对于员工的待遇也非常吝啬。工资低于行业平均水平,福利待遇简直可以说是微乎其微。而且,他们还经常加班,但却不提供加班补偿。这让我感到非常不满和失望。

在与同事的交流中,我发现大家都对公司的管理方式感到不满。许多人都在考虑离职,寻找更好的机会。尽管我对这个项目充满了热情,但最终我还是做出了离职的决定。

离开这家公司后,我感到一种解脱和自由。我决定重新评估自己的职业规划,并寻找更好的工作环境。我参加了一些技术研讨会和行业活动,扩展了人脉和知识面。

通过努力和坚持,我最终找到了一家知名游戏开发公司的工作机会。这家公司有着良好的声誉和优秀的团队氛围。在这里,我得到了更好的薪资待遇和职业发展机会。与同事们的合作也非常愉快,他们互相支持和激励,共同追求技术的进步和项目的成功。

在新的公司,我不仅继续提升自己的技术能力,还积极参与项目的管理和领导工作。我逐渐晋升为高级程序员,并负责指导和培养新人。我享受着这种成长和发展的过程,同时也在职业道路上收获了不断增长的薪资。

除了职业发展,我还热衷于扩展自己的知识领域。我广泛阅读与编程相关的书籍,不仅加深了对技术的理解,还开拓了思维的广度。这些书籍不仅拓宽了我的知识面,也为我在工作中遇到的问题提供了解决思路。

在编程道路上,我结识了许多优秀的同行和导师。他们在我职业发展中起到了关键的作用。他们与我分享自己的经验和知识,给予了我很多指导和支持。有时候,在解决问题的过程中,他们的帮助让我事半功倍。

总结而言,我的程序生涯经历了起伏和挑战,但也收获了许多成长和成功。通过不断学习和努力,我掌握了新的技能,取得了薪资的增长,结识了良师益友。我学会了在职场中勇敢面对困难,果断做出改变并寻找更好的机会。这些经历让我明白了职业选择的重要性,一个良好的工作环境和管理团队对于个人的成长和幸福感至关重要。

在我的职业规划中,我也意识到了不断学习和适应新技术的重要性。随着科技的迅猛发展,编程领域也在不断演进。我持续关注行业的趋势,并主动学习新的编程语言、框架和工具。这使我能够跟上潮流,提升自己的竞争力,并为公司的发展做出贡献。

此外,我也始终注重个人的硬件装备。一台高效的电脑和适合编程需求的工具是提高工作效率的关键。我不断更新我的硬件设备,并保持其良好状态,以确保在工作中能够高效地完成任务。

在这个职业生涯中,我经历了职场的起伏和挑战,但我始终坚持不懈地追求自己的梦想和目标。通过遇到的困难和不愉快的经历,我学会了坚持和勇敢面对挑战,也学会了在逆境中寻找机会和改变。

通过不断学习、拓展技能、寻找良师益友和适应职业发展的机会,我在程序生涯中取得了成长和进步。我不仅拥有了稳定的职业发展和增长的薪资,还培养了自己的领导能力和团队合作精神。

总的来说,程序生涯是一段充满挑战和机遇的旅程。通过坚持不懈的努力和持续学习,我在职业道路上取得了一定的成就。我相信,只要保持对技术的热情和对自我提升的追求,我将继续在编程的世界中不断成长和创造出更多的价值。

自己总结了一句话。

对于命运,不必抱怨什么。因为,你就是你的上帝。

                                                              ---- 致自己

收起阅读 »

iOS推送证书不受信任

问题:iOS推送证书不受信任 问题分析: 苹果已经使用了新的签名证书。 原文: Apple Worldwide Developer Relations Intermediate Certificate Expiration 解决方法: 打开苹果官方证书下载链接...
继续阅读 »

问题:iOS推送证书不受信任



问题分析:


苹果已经使用了新的签名证书。

原文: Apple Worldwide Developer Relations Intermediate Certificate Expiration


解决方法:


打开苹果官方证书下载链接:Apple PKI




下载G4证书,安装一下就可以了

收起阅读 »

移动端页面加载耗时监控方案

iOS
本文阐述了个人对移动端页面加载耗时监控的一些理解,主要从:节点划分及对应的实现方案,线上监控注意点,后续还能做的事 三个方面来和大家分享。前言移动端的页面加载速度,作为最为影响用户体验的因素之一,是我们做移动端性能优化的重点方向之一。而优化的效果体现,需要置信...
继续阅读 »

本文阐述了个人对移动端页面加载耗时监控的一些理解,主要从:节点划分及对应的实现方案,线上监控注意点,后续还能做的事 三个方面来和大家分享。

前言

移动端的页面加载速度,作为最为影响用户体验的因素之一,是我们做移动端性能优化的重点方向之一。

而优化的效果体现,需要置信的指标进行衡量(常见方法论:寻找方向->确定指标->实践->量化收益),而本文想要分享的就是:如何真实、完整、方便的获得页面加载时间,并会向线上监控环节,有一定延伸。

本文的示例代码都是OC(因为Java和kotlin我也不会😅),但相关思路和方案也适用于Android(Android端已实现并上线)。

页面加载耗时

常见方案

页面加载时长是一直以来大家都在攻坚的方向,所以市面上也有非常非常多的度量方案,从节点划分角度看:

较为基础的:ViewController 的 init -> viewDidLoad -> viewDidAppear

更进一步的:ViewController 的 init -> viewDidLoad -> viewDidAppear -> user Interactable

主流方案:ViewController 的 init -> viewDidLoad -> viewDidAppear -> view render completed -> user Interactable

还有什么地方可以改进的吗?

对于这些成熟方案,我还有什么可以更进一步的吗?主要总结为以下几个方面吧:

  • 完整反映用户体感

我们做性能优化,归根结底,更是用户体验优化,在满足功能需要的同时,不影响用户的使用体验。 所以,我个人认为,大多数的性能指标,都要考虑到用户体验这个方向;页面启动速度这一块,更是如此;而传统的方案,能够完整的反应用户体感吗? 我觉得还是有一部分的缺失的:用户主动发起交互到ViewController这个阶段。这一部分有什么呢,不就是直接tap触发的action里vc就初始化了吗? 实际在一些较为复杂、大型的项目中,并不然,中间可能会有很多其他处理,例如:方法hook、路由调度、参数解析、containerVC的初始化、动态库加载等等。这一部分的耗时,实际上也是用户体感的一部分,而这一部分的耗时,如果不加监控的话,也会对整体耗时产生劣化。(这里可能会有小伙伴问了,这些东西,不应该由各自负责的同学,例如负责路由的同学,自行监控吗?这里我想阐述的一个观点时,时长类的监控,如果由几个时间段拼接,相比于endTime - startTime,难免会产生gap,即,加入endTime = 10,startTime = 0,那么中间分成两段,很有可能endTime2 = 10,startTime2 = 6;endTime1 = 4,startTime1 = 0,造成总时长不准。总而言之,还是希望得到一个能够完整反映用户体感的时长。)

  • 数据采集与业务解耦

这一点其实市面上的很多方案已经做得很好了。解耦,一方面是为了,提效:避免后续有新的页面需要监控时,需要进行新的开发;另一方面,也是避免业务迭代对于监控数据的影响:如果是手动侵入性埋点,很难保证后续新增的耗时任务对监控数据不产生影响。 而本文方案,不需要在业务代码中插入任何代码,大都是通过方法hook来实现数据采集的;而对范围、以及匹配关系等的控制,也都是通过配置来完成的。

具体实现

节点确定&数据采集方式


根据一个页面(ViewController)的加载过程中,开发主要进行的处理,以及可能对用户体感产生影响的因素,将页面加载过程划分为如上图所示的11个节点,具体解释及实现方案如下:

1. 用户行为触发页面跳转

由于页面的跳转一般是通过用户点击、滑动等行为触发的,因此这里监听用户触摸屏幕的时间点;但有效节点仅为VC在初始化前的最后一次点击/交互。

具体实现: hook UIWidow 的 sendEvent:方法,在swizzle方法内记录信息;为了性能考虑,目前仅记录一个uint64_t的时间戳,且仅内存写; 注意这里需要记录手指抬起的时间,即 touch.phase == UITouchPhaseEnded,因为一般action被调用的时机就是此时; 同时,为了适配各种行为触发的新页面出现,还增加了一个手动添加该节点的方法,使一些较复杂且不通用,业务特性较强的初始化场景,也能够有该节点数据,且不依赖hook;但注意该手动方法为侵入式数据采集方式。

2. ViewController的初始化

具体实现:hook UIViewController或你的VC基类 的 - (instancetype)init 的方法;

3. 本地UI初始化

不依赖于网络数据的UI开始初始化。

这个节点,我实际上并没有在本次实现,这里的一个理想态是:将这部分行为(即UI初始化的代码),通过协议的方式,约束到指定方法中;例如,架构层面约束一个setupSubviews的接口,回调给各业务VC,供其进行基础UI绘制(目前这种方式再一些更复杂的业务场景下实现并运行较好);有这个基础约束的前提下,才能准确的采集我理想中该节点的耗时。而我目前所负责的模块,并没有这种强约束,而又不能简单的去认为所有基础UI都是在viewDidLoad中去完成的。因此需要 对原有架构的一定修改 或 能够保证所有基础UI行为都在viewDidLoad中实现,才能够实现该节点数据的准确采集。 因此2 ~ 3和3 ~ 4间的耗时,被融合为了一段2 ~ 4的耗时。

4. 本地UI初始化完成

不依赖于网络数据的UI初始化完成。

具体实现:监听主线程的闲时状态,VC初始化 节点后的首个闲时状态表示 本地UI初始化完成;(闲时状态即runloop进入kCFRunLoopBeforeWaiting

5. 发起网络请求

调用网络SDK的时间点。

这里描述的就是上面的节点划分图的第二条线,因为两条线的节点间没有强制的线性关系,虽然图中当前节点是放在了VC初始化平行的位置,但实际上,有些实现会在VC初始化之前就发起网络请求,进行预加载,这种情况在实现的时候也是需要兼容的。

具体实现:hook 业务调用网络SDK发起请求方法的api;这里的网络库各家实现方案就可能有较大差异了,根据自身情况实现即可。

6. 网络SDK回调

网络SDK的回调触发的时间点。

具体实现:hook 网络SDK向业务层回调的api;差异性同5。

7. send request
8. receive response

真正 发出网络请求 和 收到response 的时间点,用于计算真正的网络层耗时。 这俩和5、6是不是重复了啊?并不然,因为,网络库在接收到发起网络请求的请求后,实际上在端阶段,还会进行很多处理,例如公参的处理、签名、验签、json2Model等,都会产生耗时;而真正离开了端,在网上逛荡那一段,更是几乎“完全不可控”的状态。所以,分开来统计:端部分 和 网络阶段,才能够为后续的优化提供数据基础,这也是数据监控的意义所在

具体实现: 实际上系统网络api中就有对网络层详细性能数据的收集

- (void)URLSession:(NSURLSession *)session 
             task:(NSURLSessionTask *)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics;

根据官方文档中的描述


可以发现,我们实际上需要的时长就是从 fetchStartDateresponseEndDate 间的时间。 因此可以该delegate,获取这两个时间点。

9. 详细UI初始化

详细UI指,依赖于网络接口数据的UI,这部分UI渲染完成才是页面达到对用户可见的状态。

具体实现:这里我们认为从网络SDK触发回调时,即开始进行详细UI的渲染,因此该节点和节点6是同一个节点。

10. 详细UI渲染完成

页面对用户来说,真正达到可见状态的节点。

具体实现: 对于一个常规的App页面来说,如何定义一个页面是否真正渲染完成了呢?

被有效的视图铺满

什么是有效视图呢?视频,图片,文字,按钮,cell,能向用户传递信息,或者产生交互的view; 铺满,并不是指完全铺满,而是这些有效视图填充到一定比例即可,因为按照正常的视觉设计和交互体验,都不会让整个屏幕的每一个像素点都充满信息或具备交互能力;而这个比例,则是根据业务的不同而不同的。 下面则是上述逻辑的实现思路:

确定有效视图的具体类
UITextView 
UITextField
UIButton
UILabel
UIImageView
UITableViewCell
UICollectionViewCell

主流方案中比较常见的,是前几种类,并不包括最后的两个cell;而这里为什么将cell也作为有效视图类呢? 首先,出于业务特征考虑,目前应用该套监控方案的页面,主要是以卡片列表样式呈现的;而且个人认为,市面上很多App的页面也都是列表形式来呈现内容的;当然,如果业务特征并不相符,例如全屏的视频播放页,就可以不这样处理。 其次,将cell作为有效视图,确实能够极大的降低每次计算覆盖率的耗时的。性能监控本身产生的性能消耗,是性能方向一直以来需要着重关注的点,毕竟你一个为了性能优化服务的工具,反而带来了不小的劣化,怎样也说不太过去啊😂~ 我也测试了是否包含cell对计算耗时的影响: 下表中为,在一个层级较为复杂的业务页面,页面完全渲染完成之后,完成一次覆盖率达到阈值的扫描所需的时长。

有效视图包含 cell不包含 cell
检测一次覆盖率耗时(ms)1~515~18
耗时减少15ms/次(83%)

而且,有效视图的类,建议支持在线配置,也可以是一些自定义类。

将cell作为有效视图,大家可能会产生一个新的顾虑:占位cell的情况,再具体点,就是常见的骨架图怎么办?骨架图是什么,就是在网络请求未返回的时候,用缓存的data或者模拟样式,渲染出一个包含大致结构,但不包含具体内容的页面状态,例如这种:

这种情况下,cell已经铺满了屏幕,但实际上并未完成渲染。这里就要依赖于节点的前后顺序了,详细UI是依赖于网络数据的,而骨架图是在网络返回之前绘制完成的,所以真正的覆盖率计算,是从网络数据返回开始的,因此骨架图的填充完成节点,并不会被错误统计未详细UI渲染完成的节点。

覆盖率的计算方式


如上图所示,开辟两个数组a、b,数组空间分别为屏幕长宽的像素数,并以0填充,分别代表横纵坐标; 从ViewController的view开始递归遍历他的subView,遇见有效视图时,将其frame的width和height,对应在数组a、b中的range的内存空间,都填充为1,每次遍历结束后,计算数组a、b中内容为1的比例,当达到阈值比例时,则视为可见状态。 示例代码如下:

- (void)checkPageRenderStatus:(UIView *)rootView {
  if (kPhoneDeviceScreenSize.width <= 0 || kPhoneDeviceScreenSize.height <= 0) {
      return;
  }

  memset(_screenWidthBitMap, 0, kPhoneDeviceScreenSize.width);
  memset(_screenHeightBitMap, 0, kPhoneDeviceScreenSize.height);

  [self recursiveCheckUIView:rootView];
}

- (void)recursiveCheckUIView:(UIView *)view {
  if (_isCurrentPageLoaded) {
      return;
  }

  if (view.hidden) {
      return;
  }

  // 检查view是否是白名单中的实例,直接用于填充bitmap
  for (Class viewClass in _whiteListViewClass) {
      if ([view isKindOfClass:viewClass]) {
          [self fillAndCheckScreenBitMap:view isValidView:YES];
          return;
      }
  }

  // 最后递归检查subviews
  if ([[view subviews] count] > 0) {
      for (UIView *subview in [view subviews]) {
          [self recursiveCheckUIView:subview];
      }
  }
}

- (BOOL)fillAndCheckScreenBitMap:(UIView *)view isValidView:(BOOL)isValidView {

  CGRect rectInWindow = [view convertRect:view.bounds toView:nil];

  NSInteger widthOffsetStart = rectInWindow.origin.x;
  NSInteger widthOffsetEnd = rectInWindow.origin.x + rectInWindow.size.width;
  if (widthOffsetEnd <= 0 || widthOffsetStart >= _screenWidth) {
      return NO;
  }
  if (widthOffsetStart < 0) {
      widthOffsetStart = 0;
  }
  if (widthOffsetEnd > _screenWidth) {
      widthOffsetEnd = _screenWidth;
  }
  if (widthOffsetEnd > widthOffsetStart) {
      memset(_screenWidthBitMap + widthOffsetStart, isValidView ? 1 : 0, widthOffsetEnd - widthOffsetStart);
  }

  NSInteger heightOffsetStart = rectInWindow.origin.y;
  NSInteger heightOffsetEnd = rectInWindow.origin.y + rectInWindow.size.height;
  if (heightOffsetEnd <= 0 || heightOffsetStart >= _screenHeight) {
      return NO;
  }
  if (heightOffsetStart < 0) {
      heightOffsetStart = 0;
  }
  if (heightOffsetEnd > _screenHeight) {
      heightOffsetEnd = _screenHeight;
  }
  if (heightOffsetEnd > heightOffsetStart) {
      memset(_screenHeightBitMap + heightOffsetStart, isValidView ? 1 : 0, heightOffsetEnd - heightOffsetStart);
  }

  NSUInteger widthP = 0;
  NSUInteger heightP = 0;
  for (int i=0; i< _screenWidth; i++) {
      widthP += _screenWidthBitMap[i];
  }
  for (int i=0; i< _screenHeight; i++) {
      heightP += _screenHeightBitMap[i];
  }

  if (widthP > _screenWidth * kPageLoadWidthRatio && heightP > _screenHeight * kPageLoadHeightRatio) {
      _isCurrentPageLoaded = YES;
      return YES;
  }

  return NO;
}

但是也会有极端情况(类似下图)


无法正确反应有效视图的覆盖情况。但是出于性能考虑,并不会采用二维数组,因为w*h的量太大,遍历和计算的耗时,会有指数级的激增;而且,正常业务形态,应该不太会有类似的极端形态。

即使真的会较高频的出现类似情况,也有一套备选方案:计算有效视图的面积 占 总面积 的比例;该种方式会涉及到UI坐标系的频繁转换,耗时也会略差于当前的方式。

在某些业务场景下,例如 无/少结果情况,关于页面等,完全渲染后,也无法达到铺满阈值。 这种情况,会以用户发生交互(同 1、用户行为触发页面跳转 的获取方式)和 主线程闲时状态超过5s (可配)来做兜底,看是否属于这种状态,如果是,则相关性能数据不上报,因为此种页面对性能的消耗较正常铺满的情况要低,并不能真实的反应性能消耗、瓶颈,因此,仅正常铺满的业务场景进行监控并优化,即可。

扫描的触发时机

以帧刷新为准,因为只有每次帧刷新后,UI才会真正产生变化;出于性能考虑,不会每帧都进行扫描,每间隔x帧(x可配,默认为1),扫描一次;同时,考虑高刷屏 和 大量UI绘制时会丢帧 的情况,设置 扫描时间间隔 的上下限,即:满足 隔x帧 的前提下,如果和上次扫描的时间差小于 下限,仍不扫描;如果 某次扫描时,和上次扫描的时间间隔 大于 上限,则无论中间隔几帧,都开启一次扫描。

11. 用户可交互

用户可见之后的下一个对用户来说至关重要的节点。如果只是可见,然后就疯狂占用主线程或其他资源,造成用户的点击等交互行为,还是会被卡主,用户只能看,不能动,这个体感也是很差的;

具体实现:详细UI渲染完成 后的 首次主线程闲时状态。

监控方案

这里由于各家的基建并不相同,因此只是总结一些小的建议,可能会比较零散,大家见谅。

  1. 建议采样收集

首先,数据的采集或者其他的新增行为/方法,一定是会产生耗时的,虽然可能不多,但还是秉着尽善尽美的原则,还是能少点就少点的,所以数据的采集,包括前面的hook等等一切行为,都只是随机的面向一部分用户开放,降低影响范围; 而且,如果数据量极大,全量的数据上报,其实对数据链路本身也会产生压力、增加成本。 当前,采样的前提是基本数据量足够,不然的话,采样样本量过小,容易对统计结果产生较大波动,造成不置信的结果。

  1. 可配置

除了基本的是否开启的开关之外,还有其他的很多的点 需要/可以/建议 使用线上配置控制。个人认为,线上配置,除了实现对逻辑的控制,更重要的一个作用,就是出现问题时及时止损。 举一些我目前使用的配置中的例子: - 有效视图类 - 渲染完成状态,横纵坐标的填充百分比阈值 - 终态的兜底阈值 - VC的类名、对应的网络请求 等等。

  1. 本地异常数据过滤

由于我们的样本数据量会非常大,所以对于异常数据我们不需要“手软”,我们需要有一套本地异常数据过滤的机制,来保证上报的数据都是符合要求的;不然我们后续统计处理的时候,也会因此出现新的问题需要解决。

后续还能做的事

这一部分,是对后续可实现方案的一个美好畅想~

1)页面可见态的终点,不只是覆盖率

其实,实际业务场景中,很多cell,即使绘制完,并渲染到屏幕上,此时,用户可见的也没有达到我们真正希望用户可见的状态,很多内容,都还是一个placeholder的状态。例如,通过url加载的image,我们一般都是先把他的size算好,把他的位置留好,cell渲染完就直接展示了;再进一步,如果是一个视频的播放卡片,即使网络图片加载好了,还要等待视频帧的返回,才能真正达到这张卡片的业务终态\color{red}{业务终态}业务终态(求教这里标红后如何能够让字体大小一致)。

这个非常后置,而且我们端上可能也影响不了什么的节点,采集起来有意义吗?

我觉得这是一个非常有价值的节点。一直都在说“技术反哺业务”,那么业务想要用户真正看到的那个终态,就是很重要的一环;因此,用户能在什么时间点看到,从业务角度说,能够影响其后续的方案设计(表现形式),完善用户体感对业务指标的影响;从技术角度说,可以感知真实的全链路的表现(不只是端),从而有针对性的进行优化。

如何获取到所有的业务终态呢?

这里一定是和业务有所耦合的,因为每个业务的终态,只有业务自身才知道;但是我们还是要尽量降低耦合度。 这里可以用协议的方式,为各个业务增加一个达到终态的标识,那么在某个业务达到终态之后,设置该标识即可,这里就是唯一对业务的侵入了;然后和计算覆盖率类似,这里的遍历,是业务维度(这里想象为卡片更好理解一点),只有全部业务的标识都ready之后,才是真正达到业务上的终态。

2)性能指标 关联 业务行为

其实,现在性能监控,各类平台,各个团队,或多或少的都在做,我相信,性能数据采集的代码,在工程中,也不仅仅只有一份;这个现状,在很多成一定规模的互联网公司中都可能存在。

而如果您和我一样,作为一个业务团队,如何在不重复造轮子的情况下,夹缝中求生存呢?

我个人目前的理解:将 性能表现 与 业务场景 相关联。

帧率、启动耗时、CPU、内存等等,这些性能指标数据的获取,在业界都有非常成熟的方案,而且我们的工程里,一定也有相关的代码;而我们能做的,仅仅是,调一下人家的api,然后把数据再自己上传一份(甚至有的连上传都包含了),就完事了吗?

这样我觉得并不能体现出我们自建监控的价值。个人理解,监控的意义在于:暴露问题 + 辅助定位问题 + 验证问题的解决效果

所以我们作为业务团队,将 性能数据 和 我们的业务做了什么 bind 到一起了,是不是就能一定程度上完成了上面的目的呢?


我们可以明确,我们什么样的业务行为,会影响我们的性能数据,也就是影响我们的用户基础体验。这样,不仅会帮助我们定位问题的原因,甚至会影响产品侧的一些产品能力设计方案。

完成这些建设之后,可能我们的监控就可以变成这样,甚至更好的状态:


3)完善全链路对性能表现的关注

性能数据的关注、监控,不应该仅仅在线上阶段,开发期 → 测试期 → 线上,全链路各个环节都应该具有。

  • 目前各家都比较关注线上监控,相信都已经较为完善;

  • 测试期的业务流程性能脚本;对于测试的性能测试方案,开放应该参与共建或者有一定程度的参与,这样才能从一定程度上保证数据的准确性,以及双方性能数据的相互认可;

  • 开发期,目前能够提供展示实时CPU、FPS、内存数据的基础能力的工具很常见,也比较容易实现;但实际上,在日常开发的过程中,很难让RD同时关注需求情况与性能数据表现。因此,还是需要一些工具来辅助:例如,我们可以对某些性能指标,设置一些阈值,当日常开发中,超过阈值时,则弹窗提醒RD确认是否原因、是否需要优化,例如,详细UI绘制阶段的耗时阈值是800ms,如果某位同学在进行变更后,实际绘制耗时多次超越该值,则弹窗提醒。

作者:XTShow
来源:juejin.cn/post/7184033051289059384

收起阅读 »

Sourcery 的 Swift Package 命令行插件

什么是Sourcery?Sourcery 是当下最流行的 Swift 代码生成工具之一。其背后使用了 SwiftSyntax[1],旨在通过自动生成样板代码来节省开发人员的时间。Sourcery 通过扫描一组输入文件,然后借助模板的帮助,自动生成模板中定义的 ...
继续阅读 »

什么是Sourcery?

Sourcery 是当下最流行的 Swift 代码生成工具之一。其背后使用了 SwiftSyntax[1],旨在通过自动生成样板代码来节省开发人员的时间。Sourcery 通过扫描一组输入文件,然后借助模板的帮助,自动生成模板中定义的 Swift 代码。

示例

考虑一个为摄像机会话服务提供公共 API 的协议:

protocol Camera {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}

当使用此新的 Camera service 进行单元测试时,我们希望确保 AVCaptureSession 没有被真的创建。我们仅仅希望确认 camera service 被测试系统(SUT)正确的调用了,而不是去测试 camera service 本身。
因此,创建一个协议的 mock 实现,使用空方法和一组变量来帮助我们进行单元测试,并断言(asset)进行了正确的调用是有意义的。这是软件开发中非常常见的一个场景,如果你曾维护过一个包含大量单元测试的大型代码库,这么做也可能有点乏味。
好吧~不用担心!Sourcery 会帮助你!⭐️ 它有一个叫做 AutoMockable[2] 的模板,此模板会为任意输入文件中遵守 AutoMockable 协议的协议生成 mock 实现。

注意:在本文中,我扩展地使用了术语 Mock,因为它与 Sourcery 模板使用的术语一致。Mock 是一个相当重载的术语,但通常,如果我要创建一个 双重测试[3],我会根据它的用途进一步指定类型的名称(可能是 Spy 、 Fake 、 Stub 等)。如果您有兴趣了解更多关于双重测试的信息,马丁·福勒(Martin Fowler)有一篇非常好的文章,可以解释这些差异。

现在,我们让 Camera 遵守 AutoMockable。该接口的唯一目的是充当 Sourcery 的目标,从中查找并生成代码。

import UIKit

// Protocol to be matched
protocol AutoMockable {}

public protocol Camera: AutoMockable {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}

此时,可以在上面的输入文件上运行 Sourcery 命令,指定 AutoMockable 模板的路径:

sourcery --sources Camera.swift --templates AutoMockable.stencil --output .

💡 本文通过提供一个 .sourcery.yml 文件来配置 Sourcery 插件。如果提供了配置文件或 Sourcery 可以找到配置文件,则将忽略与其值冲突的所有命令行参数。如果您想了解有关配置文件的更多信息,Sourcery的 repo 中有一节[4]介绍了该主题。
命令执行完毕后,在输出目录下会生成一个 模板名 加 .generated.swift 为后缀的文件。在此例是 ./AutoMockable.generated.swift:

// Generated using Sourcery 1.8.2 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// swiftlint:disable line_length
// swiftlint:disable variable_name

import Foundation
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif

class CameraMock: Camera {

//MARK: - start

var startCallsCount = 0
var startCalled: Bool {
return startCallsCount > 0
}
var startClosure: (() -> Void)?

func start() {
startCallsCount += 1
startClosure?()
}

//MARK: - stop

var stopCallsCount = 0
var stopCalled: Bool {
return stopCallsCount > 0
}
var stopClosure: (() -> Void)?

func stop() {
stopCallsCount += 1
stopClosure?()
}

//MARK: - capture

var captureCallsCount = 0
var captureCalled: Bool {
return captureCallsCount > 0
}
var captureReceivedCompletion: ((UIImage?) -> Void)?
var captureReceivedInvocations: [((UIImage?) -> Void)] = []
var captureClosure: ((@escaping (UIImage?) -> Void) -> Void)?

func capture(_ completion: @escaping (UIImage?) -> Void) {
captureCallsCount += 1
captureReceivedCompletion = completion
captureReceivedInvocations.append(completion)
captureClosure?(completion)
}

//MARK: - rotate

var rotateCallsCount = 0
var rotateCalled: Bool {
return rotateCallsCount > 0
}
var rotateClosure: (() -> Void)?

func rotate() {
rotateCallsCount += 1
rotateClosure?()
}

}

上面的文件(AutoMockable.generated.swift)包含了你对mock的期望:使用空方法实现与目标协议的一致性,以及检查是否调用了这些协议方法的一组变量。最棒的是… Sourcery 为您编写了这一切!🎉

怎么运行 Sourcery?

怎么使用 Swift package 运行 Sourcery?
至此你可能在想如何以及怎样在 Swift package 中运行 Sourcery。你可以手动执行,然后讲文件拖到包中,或者从包目录中的命令运行脚本。但是对于 Swift Package 有两种内置方式运行可执行文件:
通过命令行插件,可根据用户输入任意运行
通过构建工具插件,该插件作为构建过程的一部分运行。
在本文中,我将介绍 Sourcery 命令行插件,但我已经在编写第二部分,其中我将创建构建工具插件,这带来了许多有趣的挑战。

创建插件包

让我们首先创建一个空包,并去掉测试和其他我们现在不需要的文件夹。然后我们可以创建一个新的插件 Target 并添加 Sourcery 的二进制文件作为其依赖项。
为了让消费者使用这个插件,它还需要被定义为一个产品:

// swift-tools-version: 5.6
import PackageDescription

let package = Package(
name: "SourceryPlugins",
products: [
.plugin(name: "SourceryCommand", targets: ["SourceryCommand"])
],
targets: [
// 1
.plugin(
name: "SourceryCommand",
// 2
capability: .command(
intent: .custom(verb: "sourcery-code-generation", description: "Generates Swift files from a given set of inputs"),
// 3
permissions: [.writeToPackageDirectory(reason: "Need access to the package directory to generate files")]
),
dependencies: ["Sourcery"]
),
// 4
.binaryTarget(
name: "Sourcery",
path: "Sourcery.artifactbundle"
)
]
)

让我们一步一步地仔细查看上面的代码:

1.定义插件目标。
2.以 custom 为意图,定义了 .command 功能,因为没有任何默认功能( documentationGeneration 和 sourceCodeFormatting)与该命令的用例匹配。给动词一个合理的名称很重要,因为这是从命令行调用插件的方式。
3.插件需要向用户请求写入包目录的权限,因为生成的文件将被转储到该目录。
4.为插件定义了一个二进制目标文件。这将允许插件通过其上下文访问可执行文件。
💡 我知道我并没有详细介绍上面的一些概念,但如果您想了解更多关于命令插件的信息,这里有一篇由 Tibor Bödecs 写的超级棒的文章⭐。如果你还想了解更多关于 Swift Packages 中二级制的目标(文件),我同样有一篇现今 Swift 包中的二进制目标。

编写插件

现在已经创建了包,是时候编写一些代码了!我们首先在 Plugins/SourceryCommand 下创建一个名为 SourceryCommand.swift 的文件,然后添加一个 CommandPlugin 协议的结构体,这将作为该插件的入口:

import PackagePlugin
import Foundation

@main
struct SourceryCommand: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {

}
}

然后我们为命令编写实现:

func performCommand(context: PluginContext, arguments: [String]) async throws {
// 1
let configFilePath = context.package.directory.appending(subpath: ".sourcery.yml").string
guard FileManager.default.fileExists(atPath: configFilePath) else {
Diagnostics.error("Could not find config at: \(configFilePath)")
return
}

//2
let sourceryExecutable = try context.tool(named: "sourcery")
let sourceryURL = URL(fileURLWithPath: sourceryExecutable.path.string)

// 3
let process = Process()
process.executableURL = sourceryURL

// 4
process.arguments = [
"--disableCache"
]

// 5
try process.run()
process.waitUntilExit()

// 6
let gracefulExit = process.terminationReason == .exit && process.terminationStatus == 0
if !gracefulExit {
Diagnostics.error("🛑 The plugin execution failed")
}
}

让我们仔细看看上面的代码:

1.首先 .sourcery.yml 文件必须在包的根目录,否则将报错。这将使 Sourcery 神奇的工作,并使包可配置。
2.可执行文件路径的 URL 是从命令的上下文中检索的。
3.创建一个进程,并将 Sourcery 的可执行文件的 URL 设置为其可执行文件路径。
4.这一步有点麻烦。Sourcery 使用缓存来减少后续运行的代码生成时间,但问题是这些缓存是在包文件夹之外读取和写入的文件。插件的沙箱规则不允许这样做,因此 --disableCache 标志用于禁用此行为并允许命令运行。
5.进程同步运行并等待。
6.最后,检查进程终止状态和代码,以确保进程已正常退出。在任何其他情况下,通过 Diagnostics API 向用户告知错误。
就这样!现在让我们使用它

使用(插件)包

考虑一个用户正在使用插件,该插件将依赖项引入了他们的 Package.swift 文件:

// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "SourceryPluginSample",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "SourceryPluginSample",
targets: ["SourceryPluginSample"]),
],
dependencies: [
.package(url: "https://github.com/pol-piella/sourcery-plugins.git", branch: "main")
],
targets: [
.target(
name: "SourceryPluginSample",
dependencies: [],
exclude: ["SourceryTemplates"]
),
]
)

💡 注意,与构建工具插件不同,命令插件不需要应用于任何目标,因为它们需要手动运行。
用户只使用了上面的 AutoMockable 模板(可以在 Sources/SourceryPluginSample/SourceryTemplates 下找到),与本文前面显示的示例相匹配:

protocol AutoMockable {}

protocol Camera: AutoMockable {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}

根据插件的要求,用户还提供了一个位于 SourceryPluginSample 目录下的 .sourcery.yml 配置文件:

sources:
- Sources/SourceryPluginSample
templates:
- Sources/SourceryPluginSample/SourceryTemplates
output: Sources/SourceryPluginSample

运行命令

用户已经设置好了,但是他们现在如何运行包?🤔 有两种方法:

命令行

运行插件的一种方法是用命令行。可以通过从包目录中运行 swift package plugin --list 来检索特定包的可用插件列表。然后可以从列表中选择一个包,并通过运行 swift package <command's verb> 来执行,在这个特殊的例子中,运行: swift package sourcery-code-generation。
注意,由于此包需要特殊权限,因此 --allow-writing-to-package-directory 必须与命令一起使用。
此时,你可能会想,为什么我要费心编写一个插件,仍然必须从命令行运行,而我可以用一个简单的脚本在几行 bash 中完成相同的工作?好吧,让我们来看看 Xcode 14 中会出现什么,你会明白为什么我会提倡编写插件📦。

Xcode

这是运行命令插件最令人兴奋的方式,但不幸的是,它仅在 Xcode 14 中可用。因此,如果您需要运行命令,但尚未使用 Xcode 14,请参阅命令行部分。
如果你正好在使用 Xcode 14,你可以通过在文件资源管理器中右键单击包,从列表中找到要执行的插件,然后单击它来执行包的任何命令。

下一步

这是插件的初始实现。我将研究如何改进它,使它更加健壮。和往常一样,我非常致力于公开构建,并使我的文章中的所有内容都开源,这样任何人都可以提交问题或创建任何具有改进或修复的 PRs。这没有什么不同😀, 这是 公共仓库的链接。
此外,如果您喜欢这篇文章,请关注即将到来的第二部分,其中我将制作一个 Sourcery 构建工具插件。我知道这听起来不多,但这不是一项容易的任务!

收起阅读 »

Sendable 和 @Sendable 闭包代码实例详解

前言Sendable 和 @Sendable 是 Swift 5.5 中的并发修改的一部分,解决了结构化的并发结构体和执行者消息之间传递的类型检查的挑战性问题。使用 Sendable应该在什么时候使用 Sendable?Sendable协议和闭包表明那些传递的...
继续阅读 »

前言

Sendable 和 @Sendable 是 Swift 5.5 中的并发修改的一部分,解决了结构化的并发结构体和执行者消息之间传递的类型检查的挑战性问题。

使用 Sendable

应该在什么时候使用 Sendable?
Sendable协议和闭包表明那些传递的值的公共API是否线程安全的向编译器传递了值。当没有公共修改器、有内部锁定系统或修改器实现了与值类型一样的复制写入时,公共API可以安全地跨并发域使用。
标准库中的许多类型已经支持了Sendable协议,消除了对许多类型添加一致性的要求。由于标准库的支持,编译器可以为你的自定义类型创建隐式一致性。
例如,整型支持该协议:

extension Int: Sendable {}

一旦我们创建了一个具有 Int 类型的单一属性的值类型结构体,我们就隐式地得到了对 Sendable 协议的支持。

// 隐式地遵守了 Sendable 协议
struct Article {
var views: Int
}

与此同时,同样的 Article 内容的类,将不会有隐式遵守该协议:

// 不会隐式的遵守 Sendable 协议
class Article {
var views: Int
}

类不符合要求,因为它是一个引用类型,因此可以从其他并发域变异。换句话说,该类文章(Article)的传递不是线程安全的,所以编译器不能隐式地将其标记为遵守Sendable协议。


使用泛型和枚举时的隐式一致性

很好理解的是,如果泛型不符合Sendable协议,编译器就不会为泛型添加隐式的一致性。

// 因为 Value 没有遵守 Sendable 协议,所以 Container 也不会自动的隐式遵守该协议
struct Container<Value> {
var child: Value
}

然而,如果我们将协议要求添加到我们的泛型中,我们将得到隐式支持:

// Container 隐式地符合 Sendable,因为它的所有公共属性也是如此。
struct Container<Value: Sendable> {
var child: Value
}

对于有关联值的枚举也是如此:


如果枚举值们不符合 Sendable 协议,隐式的Sendable协议一致性就不会起作用。

你可以看到,我们自动从编译器中得到一个错误:

Associated value ‘loggedIn(name:)’ of ‘Sendable’-conforming enum ‘State’ has non-sendable type ‘(name: NSAttributedString)’

我们可以通过使用一个值类型String来解决这个错误,因为它已经符合Sendable。

enum State: Sendable {
case loggedOut
case loggedIn(name: String)
}

从线程安全的实例中抛出错误

同样的规则适用于想要符合Sendable的错误类型。

struct ArticleSavingError: Error {
var author: NonFinalAuthor
}

extension ArticleSavingError: Sendable { }

由于作者不是不变的(non-final),而且不是线程安全的(后面会详细介绍),我们会遇到以下错误:

Stored property ‘author’ of ‘Sendable’-conforming struct ‘ArticleSavingError’ has non-sendable type ‘NonFinalAuthor’

你可以通过确保ArticleSavingError的所有成员都符合Sendable协议来解决这个错误。


如何使用Sendable协议

隐式一致性消除了很多我们需要自己为Sendable协议添加一致性的情况。然而,在有些情况下,我们知道我们的类型是线程安全的,但是编译器并没有为我们添加隐式一致性。
常见的例子是被标记为不可变和内部具有锁定机制的类:

/// User 是不可改变的,因此是线程安全的,所以可以遵守 Sendable 协议
final class User: Sendable {
let name: String

init(name: String) { self.name = name }
}

你需要用@unchecked属性来标记可变类,以表明我们的类由于内部锁定机制所以是线程安全的:

extension DispatchQueue {
static let userMutatingLock = DispatchQueue(label: "person.lock.queue")
}

final class MutableUser: @unchecked Sendable {
private var name: String = ""

func updateName(_ name: String) {
DispatchQueue.userMutatingLock.sync {
self.name = name
}
}
}


遵守 Sendable的限制

Sendable协议的一致性必须发生在同一个源文件中,以确保编译器检查所有可见成员的线程安全。
例如,你可以在例如 Swift package这样的模块中定义以下类型:

public struct Article {
internal var title: String
}

Article 是公开的,而标题title是内部的,在模块外不可见。因此,编译器不能在源文件之外应用Sendable一致性,因为它对标题属性不可见,即使标题使用的是遵守Sendable协议的String类型。
同样的问题发生在我们想要使一个可变的非最终类遵守Sendable协议时:


可变的非最终类无法遵守 Sendable 协议

由于该类是非最终的,我们无法符合Sendable协议的要求,因为我们不确定其他类是否会继承User的非Sendable成员。因此,我们会遇到以下错误:

Non-final class ‘User’ cannot conform to Sendable; use @unchecked Sendable

正如你所看到的,编译器建议使用@unchecked Sendable。我们可以把这个属性添加到我们的User类中,并摆脱这个错误:

class User: @unchecked Sendable {
let name: String

init(name: String) { self.name = name }
}

然而,这确实要求我们无论何时从User继承,都要确保它是线程安全的。由于我们给自己和同事增加了额外的责任,我不鼓励使用这个属性,建议使用组合、最终类或值类型来实现我们的目的。


如何使用 @Sendabele

函数可以跨并发域传递,因此也需要可发送的一致性。然而,函数不能符合协议,所以Swift引入了@Sendable属性。你可以传递的函数的例子是全局函数声明、闭包和访问器,如getters和setters。
SE-302的部分动机是执行尽可能少的同步

我们希望这样一个系统中的绝大多数代码都是无同步的。

使用@Sendable属性,我们将告诉编译器,他不需要额外的同步,因为闭包中所有捕获的值都是线程安全的。一个典型的例子是在Actor isolation中使用闭包。

actor ArticlesList {
func filteredArticles(_ isIncluded: @Sendable (Article) -> Bool) async -> [Article] {
// ...
}
}

如果你用非 Sendabel 类型的闭包,我们会遇到一个错误:

let listOfArticles = ArticlesList()
var searchKeyword: NSAttributedString? = NSAttributedString(string: "keyword")
let filteredArticles = await listOfArticles.filteredArticles { article in

// Error: Reference to captured var 'searchKeyword' in concurrently-executing code
guard let searchKeyword = searchKeyword else { return false }
return article.title == searchKeyword.string
}

当然,我们可以通过使用一个普通的String来快速解决这种情况,但它展示了编译器如何帮助我们执行线程安全。


Swift 6: 代码启用并发性检查

Xcode 14 允许您通过 SWIFT_STRICT_CONCURRENCY 构建设置启用严格的并发性检查。


启用严格的并发性检查,以修复 Sendable 的符合性

这个构建设置控制编译器对Sendable和actor-isolation检查的执行水平:
Minimal : 编译器将只诊断明确标有Sendable一致性的实例,并等同于Swift 5.5和5.6的行为。不会有任何警告或错误。
Targeted: 强制执行Sendable约束,并对你所有采用async/await等并发的代码进行actor-isolation检查。编译器还将检查明确采用Sendable的实例。这种模式试图在与现有代码的兼容性和捕捉潜在的数据竞赛之间取得平衡。
Complete: 匹配预期的 Swift 6语义,以检查和消除数据竞赛。这种模式检查其他两种模式所做的一切,并对你项目中的所有代码进行这些检查。
严格的并发检查构建设置有助于 Swift 向数据竞赛安全迈进。与此构建设置相关的每一个触发的警告都可能表明你的代码中存在潜在的数据竞赛。因此,必须考虑启用严格并发检查来验证你的代码。

Enabling strict concurrency in Xcode 14

你会得到的警告数量取决于你在项目中使用并发的频率。对于Stock Analyzer,我有大约17个警告需要解决:


并发相关的警告,表明潜在的数据竞赛.

这些警告可能让人望而生畏,但利用本文的知识,你应该能够摆脱大部分警告,防止数据竞赛的发生。然而,有些警告是你无法控制的,因为是外部模块触发了它们。在我的例子中,我有一个与SWHighlight有关的警告,它不符合Sendable,而苹果在他们的SharedWithYou框架中定义了它。
在上述SharedWithYou框架的例子中,最好是等待库的所有者添加Sendable支持。在这种情况下,这就意味着要等待苹果公司为SWHighlight实例指明Sendable的一致性。对于这些库,你可以通过使用@preconcurrency属性来暂时禁用Sendable警告:

@preconcurrency import SharedWithYou

重要的是要明白,我们并没有解决这些警告,而只是禁用了它们。来自这些库的代码仍然有可能发生数据竞赛。如果你正在使用这些框架的实例,你需要考虑实例是否真的是线程安全的。一旦你使用的框架被更新为Sendable的一致性,你可以删除@preconcurrency属性,并修复可能触发的警告。

收起阅读 »

iOS的CoreData技术笔记

前言最近因为新项目想用到数据持久化,本来这是很简单的事情,复杂数据一般直接SQLite就可以解决了。但是一直以来使用SQLite确实存在要自己设计数据库,处理逻辑编码,还有调试方面的种种繁琐问题。所以考虑使用iOS的Core Data方案。上网查了一堆资料后,...
继续阅读 »

前言

最近因为新项目想用到数据持久化,本来这是很简单的事情,复杂数据一般直接SQLite就可以解决了。
但是一直以来使用SQLite确实存在要自己设计数据库,处理逻辑编码,还有调试方面的种种繁琐问题。所以考虑使用iOS的Core Data方案。
上网查了一堆资料后,发现很多代码都已经是陈旧的了。甚至苹果官方文档提供的代码样例都未必是最新的Swift版本。于是萌生了自己写一篇文章来整理一遍思路的想法。尽可能让新人快速的上手,不但要知道其然,还要知道其设计的所以然,这样用起来才更得心应手。

什么是Core Data

我们写app肯定要用到数据持久化,说白了,就是把数据保存起来,app不删除的话可以继续读写。
iOS提供数据持久化的方案有很多,各自有其特定用途。
比如很多人熟知的UserDefaults,大部分时候是用来保存简单的应用配置信息;而NSKeyedArchiver可以把代码中的对象保存为文件,方便后来重新读取。
另外还有个常用的保存方式就是自己创建文件,直接在磁盘文件中进行读写。
而对于稍微复杂的业务数据,比如收藏夹,用户填写的多项表格等,SQLite就是更合适的方案了。关于数据库的知识,我这里就不赘述了,稍微有点技术基础的童鞋都懂。
Core DataSQLite做了更进一步的封装,SQLite提供了数据的存储模型,并提供了一系列API,你可以通过API读写数据库,去处理想要处理的数据。但是SQLite存储的数据和你编写代码中的数据(比如一个类的对象)并没有内置的联系,必须你自己编写代码去一一对应。
Core Data却可以解决一个数据在持久化层和代码层的一一对应关系。也就是说,你处理一个对象的数据后,通过保存接口,它可以自动同步到持久化层里,而不需要你去实现额外的代码。
这种 对象→持久化 方案叫 对象→关系映射(英文简称ORM)。
除了这个最重要的特性,Core Data还提供了很多有用的特性,比如回滚机制,数据校验等。


图1: Core Data与应用,磁盘存储的关系

数据模型文件 - Data Model

当我们用Core Data时,我们需要一个用来存放数据模型的地方,数据模型文件就是我们要创建的文件类型。它的后缀是.xcdatamodeld。只要在项目中选 新建文件→Data Model 即可创建。
默认系统提供的命名为 Model.xcdatamodeld 。下面我依然以 Model.xcdatamodeld 作为举例的文件名。
这个文件就相当于数据库中的“库”。通过编辑这个文件,就可以去添加定义自己想要处理的数据类型。

数据模型中的表格 - Entity

当在xcode中点击Model.xcdatamodeld时,会看到苹果提供的编辑视图,其中有个醒目的按钮Add Entity
什么是Entity呢?中文翻译叫“实体”,但是我这里就不打算用各种翻译名词来提高理解难度了。
如果把数据模型文件比作数据库中的“库”,那么Entity就相当于库里的“表格”。这么理解就简单了。Entity就是让你定义数据表格类型的名词。
假设我这个数据模型是用来存放图书馆信息的,那么很自然的,我会想建立一个叫BookEntity

属性 - Attributes

当建立一个名为BookEntity时,会看到视图中有栏写着Attributes,我们知道,当我们定义一本书时,自然要定义书名,书的编码等信息。这部分信息叫Attributes,即书的属性。
Book的Entity
属性名类型
nameString
isbmString
pageInteger32
其中,类型部分大部分是大家熟知的元数据类型,可以自行查阅。
同理,也可以再添加一个读者:Reader的Entity描述。
Reader的Entity
属性名类型
nameString
idCardString


图2: 在项目中创建数据模型文件

关系 - Relationship

在我们使用Entity编辑时,除了看到了Attributes一栏,还看到下面有Relationships一栏,这栏是做什么的?
回到例子中来,当定义图书馆信息时,刚书籍和读者的信息,但这两个信息彼此是孤立的,而事实上他们存在着联系。
比如一本书,它被某个读者借走了,这样的数据该怎么存储?
直观的做法是再定义一张表格来处理这类关系。但是Core Data提供了更有效的办法 - Relationship
Relationship的思路来思考,当一本书A被某个读者B借走,我们可以理解为这本书A当前的“借阅者”是该读者B,而读者B的“持有书”是A。
从以上描述可以看出,Relationship所描述的关系是双向的,即A和B互相以某种方式形成了联系,而这个方式是我们来定义的。
ReaderRelationship下点击+号键。然后在Relationship栏的名字上填borrow,表示读者和书的关系是“借阅”,在Destination栏选择Book,这样,读者和书籍的关系就确立了。
对于第三栏,Inverse,却没有东西可以填,这是为什么?
因为我们现在定义了读者和书的关系,却没有定义书和读者的关系。记住,关系是双向的。
就好比你定义了A是B的父亲,那也要同时去定义B是A的儿子一个道理。计算机不会帮我们打理另一边的联系。
理解了这点,我们开始选择Book的一栏,在Relationship下添加新的borrowByDestinationReader,这时候点击Inverse一栏,会发现弹出了borrow,直接点上。
这是因为我们在定义BookRelationship之前,我们已经定义了ReaderRelationship了,所以电脑已经知道了读者和书籍的关系,可以直接选上。而一旦选好了,那么在ReaderRelationship中,我们会发现Inverse一栏会自动补齐为borrowBy。因为电脑这时候已经完全理解了双方的关系,自动做了补齐。


一对一和一对多 - to one和to many


我们建立ReaderBook之间的联系的时候,发现他们的联系逻辑之间还漏了一个环节。
假设一本书被一个读者借走了,它就不能被另一个读者借走,而当一个读者借书时,却可以借很多本书。
也就是说,一本书只能对应一个读者,而一个读者却可以对应多本书。
这就是 一对一→to one 和 一对多→to many 。
Core Data允许我们配置这种联系,具体做法就是在RelationShip栏点击对应的关系栏,它将会出现在右侧的栏目中。(栏目如果没出现可以在xcode右上角的按钮调出,如果点击后栏目没出现Relationship配置项,可以多点击几下,这是xcode的小bug)。
Relationship的配置项里,有一项项名为Type,点击后有两个选项,一个是To One(默认值),另一个就是To Many了。


图3: 数据模型的关系配置


Core Data框架的主仓库 - NSPersistentContainer


当我们配置完Core Data的数据类型信息后,我们并没有产生任何数据,就好比图书馆已经制定了图书的规范 - 一本书应该有名字、isbm、页数等信息,规范虽然制定了,却没有真的引进书进来。
那么怎么才能产生和处理数据呢,这就需要通过代码真刀真枪的和Core Data打交道了。
由于Core Data的功能较为强大,必须分成多个类来处理各种逻辑,一次性学习多个类是不容易的,还容易混淆,所以后续我会分别一一列出。
要和这些各司其职的类打交道,我们不得不提第一个要介绍的类,叫NSPersistentContainer,因为它就是存放这多个类成员的“仓库类”。
这个NSPersistentContainer,就是我们通过代码和Core Data打交道的第一个目标。它存放着几种让我们和Core Data进行业务处理的工具,当我们拿到这些工具之后,就可以自由的访问数据了。所以它的名字 - Container 蕴含着的意思,就是 仓库、容器、集装箱。
进入正式的代码编写的第一步,我们先要在使用Core Data框架的swift文件开头引入这个框架:

import CoreData

早期,在iOS 10之前,还没有NSPersistentContainer这个类,所以Core Data提供的几种各司其职的工具,我们都要写代码一一获得,写出来的代码较为繁琐,所以NSPersistentContainer并不是一开始就有的,而是苹果框架设计者逐步优化出来的较优设计。


图4: NSPersistentContainer和其他成员的关系


NSPersistentContainer的初始化


在新建的UIKIT项目中,找到我们的AppDelegate类,写一个成员函数(即方法,后面我直接用函数这个术语替代):

private func createPersistentContainer() {
let container = NSPersistentContainer(name: "Model")
}

这样,NSPersistentContainer类的建立就完成了,其中"Model"字符串就是我们建立的Model.xcdatamodeld文件。但是输入参数的时候,我们不需要(也不应该)输入.xcdatamodeld后缀。
当我们创建了NSPersistentContainer对象时,仅仅完成了基础的初始化,而对于一些性能开销较大的初始化,比如本地持久化资源的加载等,都还没有完成,我们必须调用NSPersistentContainer的成员函数loadPersistentStores来完成它。

private func createPersistentContainer() {
let container = NSPersistentContainer(name: "Model")
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Error: \(error)")
}
print("Load stores success")
}
}

从代码设计的角度看,为什么NSPersistentContainer不直接在构造函数里完成数据库的加载?这就涉及到一个面向对象的开发原则,即构造函数的初始化应该是(原则上)倾向于原子级别,即简单的、低开销内存操作,而对于性能开销大的,内存之外的存储空间处理(比如磁盘,网络),应尽量单独提供成员函数来完成。这样做是为了避免在构造函数中出错时错误难以捕捉的问题。


表格属性信息的提供者 - NSManagedObjectModel


现在我们已经持有并成功初始化了Core Data的仓库管理者NSPersistentContainer了,接下去我们可以使用向这个管理者索取信息了,我们已经在模型文件里存放了读者和书籍这两个Entity了,如何获取这两个Entity的信息?
这就需要用到NSPersistentContainer的成员,即managedObjectModel,该成员就是标题所说的NSManagedObjectModel类型。
为了讲解NSManagedObjectModel能提供什么,我通过以下函数来提供说明:

private func parseEntities(container: NSPersistentContainer) {
let entities = container.managedObjectModel.entities
print("Entity count = \(entities.count)\n")
for entity in entities {
print("Entity: \(entity.name!)")
for property in entity.properties {
print("Property: \(property.name)")
}
print("")
}
}

为了执行上面这个函数,需要修改createPersistentContainer,在里面调用parseEntities

private func createPersistentContainer() {
let container = NSPersistentContainer(name: "Model")
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Error: \(error)")
}

self.parseEntities(container: container)
}
}

在这个函数里,我们通过NSPersistentContainer获得了NSManagedObjectModel类型的成员managedObjectModel,并通过它获得了文件Model.xcdatamodeld中我们配置好的Entity信息,即图书和读者。
由于我们配置了两个Entity信息,所以运行正确的话,打印出来的第一行应该是Entity count = 2
container的成员managedObjectModel有一个成员叫entities,它是一个数组,这个数组成员的类型叫NSEntityDescription,这个类名一看就知道是专门用来处理Entity相关操作的,这里就没必要多赘述了。
示例代码里,获得了entity数组后,打印entity的数量,然后遍历数组,逐个获得entity实例,接着遍历entity实例的properties数组,该数组成员是由类型NSPropertyDescription的对象组成。
关于名词Property,不得不单独说明下,学习一门技术最烦人的事情之一就是理解各种名词,毕竟不同技术之间名词往往不一定统一,所以要单独理解一下。
Core Data的术语环境下,一个Entity由若干信息部分组成,之前已经提过的EntityRelationship就是了。而这些信息用术语统称为propertyNSPropertyDescription看名字就能知道,就是处理property用的。
只要将这一些知识点梳理清楚了,接下去打印的内容就不难懂了:

Entity count = 2

Entity: Book
Property: isbm
Property: name
Property: page
Property: borrowedBy

Entity: Reader
Property: idCard
Property: name
Property: borrow

我们看到,打印出来我们配置的图书有4个property,最后一个是borrowedBy,明显这是个Relationship,而前面三个都是Attribute,这和我刚刚对property的说明是一致的。

Entity对应的类

开篇我们就讲过,Core Data是一个 对象-关系映射 持久化方案,现在我们在Model.xcdatamodeld已经建立了两个Entity,那么如果在代码里要操作他们,是不是会有对应的类?
答案是确实如此,而且你还不需要自己去定义这个类。
如果你点击Model.xcdatamodeld编辑窗口中的Book这个Entity,打开右侧的属性面板,属性面板会给出允许你编辑的关于这个Entity的信息,其中Entity部分的Name就是我们起的名字Book,而下方还有一个Class栏,这一栏就是跟Entity绑定的类信息,栏目中的Name就是我们要定义的类名,默认它和Entity的名字相同,也就是说,类名也是Book。所以改与不改,看个人思路以及团队的规范。
所有Entity对应的类,都继承自NSManagedObject
为了检验这一点,我们可以在代码中编写这一行作为测试:

var book: Book! // 纯测验代码,无业务价值

如果写下这一行编译通过了,那说明开发环境已经给我们生成了Book这个类,不然它就不可能编译通过。
测试结果,完美编译通过。说明不需要我们自己编写,就可以直接使用这个类了。
关于类名,官方教程里一般会把类名更改为Entity名 + MO,比如我们这个Entity名为Book,那么如果是按照官方教程的做法,可以在面板中编辑Class的名字为BookMO,这里MO大概就是Model Object的简称吧。
但是我这里为简洁起见,就不做任何更改了,Entity名为Book,那么类名也一样为Book
另外,你也可以自己去定义Entity对应的类,这样有个好处是可以给类添加一些额外的功能支持,这部分Core Data提供了编写的规范,但是大部分时候这个做法反而会增加代码量,不属于常规操作。


数据业务的操作员 - NSManagedObjectContext


接下来我们要隆重介绍NSPersistentContainer麾下的一名工作任务最繁重的大将,成员viewContext,接下去我们和实际数据打交道,处理增删查改这四大操作,都要通过这个成员才能进行。
viewContext成员的类型是NSManagedObjectContext
NSManagedObjectContext,顾名思义,它的任务就是管理对象的上下文。从创建数据,对修改后数据的保存,删除数据,修改,五一不是以它为入口。
从介绍这个成员开始,我们就正式从 定义数据 的阶段,正式进入到 产生和操作数据 的阶段。


数据的插入 - NSentityDescription.insertNewObject


梳理完前面的知识,就可以正式踏入数据创建的学习了。
这里,我们先尝试创建一本图书,用一个createBook函数来进行。示例代码如下:

private func createBook(container: NSPersistentContainer,
name: String, isbm: String, pageCount: Int) {
let context = container.viewContext
let book = NSEntityDescription.insertNewObject(forEntityName: "Book",
into: context) as! Book
book.name = name
book.isbm = isbm
book.page = Int32(pageCount)
if context.hasChanges {
do {
try context.save()
print("Insert new book(\(name)) successful.")
} catch {
print("\(error)")
}
}
}

在这个代码里,最值得关注的部分就是NSEntityDescription的静态成员函数insertNewObject了,我们就是通过这个函数来进行所要插入数据的创建工作。
insertNewObject对应的参数forEntityName就是我们要输入的Entity名,这个名字当然必须是我们之前创建好的Entity有的名字才行,否则就出错了。因为我们要创建的是书,所以输入的名字就是Book
into参数就是我们的处理增删查改的大将NSManagedObjectContext类型。
insertNewObject返回的类型是NSManagedObject,如前所述,这是所有Entity对应类的父类。因为我们要创建的EntityBook,我们已经知道对应的类名是Book了,所以我们可以放心大胆的把它转换为Book类型。
接下来我们就可以对Book实例进行成员赋值,我们可以惊喜的发现Book类的成员都是我们在Entity表格中编辑好的,真是方便极了。
那么问题来了,当我们把Book编辑完成后,是不是这个数据就完成了持久化了,其实不是的。
这里要提一下Core Data的设计理念:懒原则。Core Data框架之下,任何原则操作都是内存级的操作,不会自动同步到磁盘或者其他媒介里,只有开发者主动发出存储命令,才会做出存储操作。这么做自然不是因为真的很懒,而是出于性能考虑。
为了真的把数据保存起来,首先我们通过context(即NSManagedObjectContext成员)的hasChanges成员询问是否数据有改动,如果有改动,就执行contextsave函数。(该函数是个会抛异常的函数,所以用do→catch包裹起来)。
至此,添加书本的操作代码就写完了。接下来我们把它放到合适的地方运行。
我们对createPersistentContainer稍作修改:

private func createPersistentContainer() {
let container = NSPersistentContainer(name: "Model")
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Error: \(error)")
}

//self.parseEntities(container: container)
self.createBook(container: container,
name: "算法(第4版)",
isbm: "9787115293800",
pageCount: 636)
}
}

运行项目,会看到如下打印输出:

Insert new book(算法(第4版)) successful.

至此,书本的插入工作顺利完成!

因为这个示例没有去重判定,如果程序运行两次,那么将会插入两条书名都为"算法(第4版)"的book记录。

数据的获取

有了前面基础知识的铺垫,接下去的例子只要 记函数 就成了,读取的示例代码:

private func readBooks(container: NSPersistentContainer) {
let context = container.viewContext
let fetchBooks = NSFetchRequest<Book>(entityName: "Book")
do {
let books = try context.fetch(fetchBooks)
print("Books count = \(books.count)")
for book in books {
print("Book name = \(book.name!)")
}
} catch {

}
}

处理数据处理依然是我们的数据操作主力context,而处理读取请求配置细节则是交给一个专门的类,NSFetchRequest来完成,因为我们处理读取数据有各种各样的类型,所以Core Data设计了一个泛型模式,你只要对NSFetchRequest传入对应的类型,比如Book,它就知道应该传回什么类型的对应数组,其结果是,我们可以通过Entity名为Book的请求直接拿到Book类型的数组,真是很方便。

打印结果:

Books count = 1
Book name = 算法(第4版)


数据获取的条件筛选 - NSPredicate


通过NSFetchRequest我们可以获取所有的数据,但是我们很多时候需要的是获得我们想要的特定的数据,通过条件筛选功能,可以实现获取出我们想要的数据,这时候需要用到NSFetchRequest的成员predicate来完成筛选,如下所示,我们要找书名叫 算法(第4版) 的书。
在新的代码示例里,我们在之前实现的readBooks函数代码里略作修改:

private func readBooks(container: NSPersistentContainer) {
let context = container.viewContext
let fetchBooks = NSFetchRequest<Book>(entityName: "Book")
fetchBooks.predicate = NSPredicate(format: "name = \"算法(第4版)\"")
do {
let books = try context.fetch(fetchBooks)
print("Books count = \(books.count)")
for book in books {
print("Book name = \(book.name!)")
}
} catch {
print("\(error)")
}
}

通过代码:

fetchBooks.predicate = NSPredicate(format: "name = \"算法(第4版)\"")

我们从书籍中筛选出书名为 算法(第4版) 的书,因为我们之前已经保存过这本书,所以可以正确筛选出来。
筛选方案还支持大小对比,如

fetchBooks.predicate = NSPredicate(format: "page > 100")

这样将筛选出page数量大于100的书籍。

数据的修改

当我们要修改数据时,比如说我们要把 isbm = "9787115293800" 这本书书名修改为 算法(第5版) ,可以按照如下代码示例:

let context = container.viewContext
let fetchBooks = NSFetchRequest<Book>(entityName: "Book")
fetchBooks.predicate = NSPredicate(format: "isbm = \"9787115293800\"")
do {
let books = try context.fetch(fetchBooks)
if !books.isEmpty {
books[0].name = "算法(第5版)"
if context.hasChanges {
try context.save()
print("Update success.")
}
}
} catch {
print("\(error)")
}

在这个例子里,我们遵循了 读取→修改→保存 的思路,先拿到筛选的书本,然后修改书本的名字,当名字被修改后,context将会知道数据被修改了,这时候判断数据是否被修改(实际上不需要判断我们也知道被修改了,只是出于编码规范加入了这个判断),如果被修改,就保存数据,通过这个方式,成功更改了书名。

数据的删除

数据的删除依然遵循 读取→修改→保存 的思路,找到我们想要的思路,并且删除它。删除的方法是通过contextdelete函数。
以下例子中,我们删除了所有 isbm="9787115293800" 的书籍:

let context = container.viewContext
let fetchBooks = NSFetchRequest<Book>(entityName: "Book")
fetchBooks.predicate = NSPredicate(format: "isbm = \"9787115293800\"")
do {
let books = try context.fetch(fetchBooks)
for book in books {
context.delete(books[0])
}
if context.hasChanges {
try context.save()
}
} catch {
print("\(error)")
}

扩展和进阶主题的介绍

如果跟我一步步走到这里,那么关于Core Data的基础知识可以说已经掌握的差不多了。当然了,这部分基础对于日常开发已经基本够用了。
关于Core Data开发的进阶部分,我在这里简单列举一下:
  1. Relationship部分的开发,事实上通过之前的知识可以独立完成。
  2. 回滚操作,相关类:UndoManager
  3. EntityFetched Property属性。
  4. 多个context一起操作数据的冲突问题。
  5. 持久化层的管理,包括迁移文件地址,设置多个存储源等。
以上诸个主题都可以自己进一步探索,不在这篇文章的讲解范围。不过后续不排除会单独出文探索。

结语

Core Data在圈内是比较出了名的“不好用”的框架,主要是因为其抽象的功能和机制较为不容易理解。本文已经以最大限度的努力试图从设计的角度去阐述该框架,希望对你有所帮助。

收起阅读 »

项目中第三方库并不是必须的

前言有时候集成一个特定的库(比如 PayPal)是必须的,有时候是避免去开发一些非常复杂的功能,有时候仅仅只是避免重复造轮子。虽然这些都是合理的考量,但使用第三方库的风险和相关成本往往被忽视或误解。在某些情况下,风险是值得的,但是在决定冒险之前,首先...
继续阅读 »

前言

有时候集成一个特定的库(比如 PayPal)是必须的,有时候是避免去开发一些非常复杂的功能,有时候仅仅只是避免重复造轮子。

虽然这些都是合理的考量,但使用第三方库的风险和相关成本往往被忽视或误解。在某些情况下,风险是值得的,但是在决定冒险之前,首先要能够明确的定义风险。为了使风险评估更加的透明和一致,我们制定了一个流程来衡量我们将其集成到app有多大的风险。


风险

大多数大型组织,包括我们,都有某种形式的代码审查,作为开发实践的一部分。对这些团队来说,添加一个第三方库就相当于添加了一堆由不属于团队成员开发,未经审查的代码。这破坏了团队一直坚持的代码审查原则,交付了质量未知的代码。这给app的运行方式以及长期开发带来了风险,对于大型团队而言,更是对整体业务带来了风险。

运行时风险

库代码通常来说,对于系统资源,和app拥有相同级别的访问权限,但它们不一定应用团队为管理这些资源而制定的最佳实践。这意味着它们可以在没有限制的情况下访问磁盘,网络,内存,CPU等等,因此,它们可以(过度)将文件写入磁盘,使用未优化的代码占用内存或CPU,导致死锁或主线程延迟,下载(和上传!)大量数据等等。更糟糕的是他们会导致崩溃,甚至崩溃循环。两次。

其中许多情况直到 app 已经上架才被发现,在这种情况下,修复它需要创建一个新版本,并通过审核,这通常需要大量时间和成本。这种风险可以通过一个变量控制是否调用来进行一定程度的控制,但是这种方法也并非万无一失(看下文)。

开发风险

引用一个同事的话:“每一行代码都是一种负担”,对不是你自己写的代码而言,这句话更甚。库在适配新技术或API时可能很慢,这阻碍了代码开发,或者太快,导致开发的版本过高。

库在采用新技术或API时可能很慢,阻碍了代码库,或者太快,导致部署目标太高。每当 Apple 和 Google 每年发布一个新 OS 版本时,他们通常要求开发人员根据SDK的变化更新代码,库开发人员也必须这样做。这需要协调一致的努力、优先事项的一致性以及及时完成工作的能力。

随着移动平台的不断变化,以及团队(成员)也不是一成不变,这将会成为一个持续不断的风险。当被集成的库不存在了,而库又需要更新时,会花很多时间来决定谁来做。事实证明一旦一个库存在,就很少也很难被移除,因此我们将其视为长期维护成本。

商业风险

如同我上面所说,现代的操作系统并没有对 app 代码和库代码进行区分,因此除了系统资源之外,它们还可以访问用户信息。作为 app 的开发者,我们负责恰当的使用这部分信息,也需要为任何第三方库负责。

如果用户给了 Lyft app 地理位置授权,任何第三方库也将自动得获得授权。他们可以将那些(地理位置)数据上传到自己服务器,竞对服务器,或者谁知道还有什么地方。当一个库需要我们没有的权限时,那问题就更大了。

同样,一个系统的安全取决于其最薄弱的环节,但如果其中包含未经审核的代码,那么你就不知道它到底有多安全。你精心设计的安全编码实践可能会被一个行为不当的库所破坏。苹果和谷歌实施的任何政策都是如此,例如“你不得对用户追踪”。


减少风险

当对一个库(是否)进行使用评估时,我们首先要问几个问题,以了解对库的需求。

我们内部能做么?

有时候我们只需要简单的粘贴复制真正需要的部分。在更复杂的场景中,库与自定义后端通信,我们对该API进行了逆向,并自己构建了一个迷你SDK(同样,只构建了我们需要的部分)。在90%的情况下,这是首选,但在与非常特定的供应商或需求集成时并不总是可行。

有多少用户从该库中受益?

在一种情况下,我们正在考虑添加一个风险很大的库(根据下面的标准),旨在为一小部分用户提供服务,同时将我们的所有用户都暴露在该库中。对于我们认为会从中受益的一小部分客户,我们冒了为我们所有用户带来问题的风险。

这个库有什么传递依赖?

我们还需要评估库的所有依赖项的以下标准。

退出标准是什么?

如果集成成功,是否有办法将其转移到内部?如果不成功,是否有办法删除?


评价标准

如果此时团队仍然希望集成库,我们要求他们根据一组标准对库进行“评分”。下面的列表并不全面,但应该能很好地说明我们希望看到的。

阻断标准

这些标准将阻止我们从技术上或者公司政策上集成此库,在进行下一步之前,我们必须解决:

过高的 deployment target/target SDKs。 我们支持过去4年主流的操作系统(版本),所以第三方库至少也需要支持一样多。

许可证不正确/缺失。 我们将许可文件与应用捆绑在一起,以确保我们可以合法使用代码并将其归属于许可持有人。

没有冲突的传递依赖关系。 一个库不能有一个我们已经包含但版本不同的传递依赖项。

不显示它自己的 UI 。 我们非常小心地使我们的产品看起来尽可能统一,定制用户界面对此不利。

它不使用私有 API 。 我们不愿意冒 app 因使用私有 API 而被拒绝的风险。

主要关注点

闭源。 访问源代码意味着我们可以选择我们想要包含的库的哪些部分,以及如何将该源代码与应用程序的其余部分捆绑在一起。对于我们来说,一个封闭源代码的二进制发行版更难集成。

编译时有警告。 我们启用了“警告视为错误”,具有编译警告的库是库整体质量(下降)的良好指示。

糟糕的文档。 我们希望有高质量的内联文档,外部”如何使用“文档,以及有意义的更新日志。

二进制体积。 这个库有多大?一些库提供了很多功能,而我们只需要其中的一小部分。尤其是在没有访问源码权限的情况下,这通常是一个全有或全无的情况。

外部的网络流量。 与我们无法控制的上游服务器/端点通信的库可能会在服务器关闭、错误数据被发回等时关闭整个应用程序。这也与我上面提到的隐私问题相同。

技术支持。 当事情不能正常工作时,我们需要能够报告/上报问题,并在合理的时间内解决问题。开源项目通常由志愿者维护,也很难有一个时间线,但至少我们可以自己进行修改。这在闭源项目是不可能的。

无法禁用。 虽然大多数库特别要求我们初始化它,但有些库在实例化时更“主动”,并且在我们不调用它的情况下可以自己执行工作。这意味着当库导致问题时,我们无法通过功能变量或其他机制将其关闭。

我们为所有这些(和其他一些)标准分配了点数,并要求工程师为他们想要集成的库汇总这些点数。虽然默认情况下,低分数并不难被拒绝,但我们通常会要求更多的理由来继续前进。


最后

虽然这个过程看起来非常严格,在许多情况下,潜在风险是假设的,但我们有我在这篇博文中描述的每个场景的实际例子。将评估记录下来并公开,也有助于将相对风险传达给不熟悉移动平台工作方式的人,并证明我们没有随意评估风险。

收起阅读 »

淘宝iOS扫一扫架构升级 - 设计模式的应用

iOS
本文在“扫一扫功能的不断迭代,基于设计模式的基本原则,逐步采用设计模式思想进行代码和架构优化”的背景下,对设计模式在扫一扫中新的应用进行了总结。背景扫一扫是淘宝镜头页中的一个重要组成,功能运行久远,其历史代码中较少采用面向对象编程思想,而较多采用面向过程的程序...
继续阅读 »

本文在“扫一扫功能的不断迭代,基于设计模式的基本原则,逐步采用设计模式思想进行代码和架构优化”的背景下,对设计模式在扫一扫中新的应用进行了总结。

背景

扫一扫是淘宝镜头页中的一个重要组成,功能运行久远,其历史代码中较少采用面向对象编程思想,而较多采用面向过程的程序设计。

随着扫一扫功能的不断迭代,我们基于设计模式的基本原则,逐步采用设计模式思想进行代码和架构优化。本文就是在这个背景下,对设计模式在扫一扫中新的应用进行了总结。

扫一扫原架构

扫一扫的原架构如图所示。其中逻辑&展现层的功能逻辑很多,并没有良好的设计和拆分,举几个例子:

  1. 所有码的处理逻辑都写在同一个方法体里,一个方法就接近 2000 多行。

  2. 庞大的码处理逻辑写在 viewController 中,与 UI 逻辑耦合。

按照现有的代码设计,若要对某种码逻辑进行修改,都必须将所有逻辑全量编译。如果继续沿用此代码,扫一扫的可维护性会越来越低。

图片

因此我们需要对代码和架构进行优化,在这里优化遵循的思路是:

  1. 了解业务能力

  2. 了解原有代码逻辑,不确定的地方通过埋点等方式线上验证

  3. 对原有代码功能进行重写/重构

  4. 编写单元测试,提供测试用例

  5. 测试&上线

扫码能力综述

扫一扫的解码能力决定了扫一扫能够处理的码类型,这里称为一级分类。基于一级分类,扫一扫会根据码的内容和类型,再进行二级分类。之后的逻辑,就是针对不同的二级类型,做相应的处理,如下图为技术链路流程。

图片

设计模式

责任链模式

图片

上述技术链路流程中,码处理流程对应的就是原有的 viewController 里面的巨无霸逻辑。通过梳理我们看到,码处理其实是一条链式的处理,且有前后依赖关系。优化方案有两个,方案一是拆解成多个方法顺序调用;方案二是参考苹果的 NSOperation 独立计算单元的思路,拆解成多个码处理单元。方案一本质还是没解决开闭原则(对扩展开放,对修改封闭)问的题。方案二是一个比较好的实践方式。那么怎么设计一个简单的结构来实现此逻辑呢?

码处理链路的特点是,链式处理,可控制处理的顺序,每个码处理单元都是单一职责,因此这里引出改造第一步:责任链模式。

责任链模式是一种行为设计模式, 它将请求沿着处理者链进行发送。收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

本文设计的责任链模式,包含三部分:

  1. 创建数据的 Creator

  2. 管理处理单元的 Manager

  3. 处理单元 Pipeline

三者结构如图所示

图片

创建数据的 Creator

包含的功能和特点:

  1. 因为数据是基于业务的,所以它只被声明为一个 Protocol ,由上层实现。

  2. Creator 对数据做对象化,对象生成后 self.generateDataBlock(obj, Id) 即开始执行

API 代码示例如下

/// 数据产生协议 <CreatorProtocol>
@protocol TBPipelineDataCreatorDelegate <NSObject>
@property (nonatomic, copy) void(^generateDataBlock)(id data, NSInteger dataId);
@end
复制代码

上层业务代码示例如下

@implementation TBDataCreator
@synthesize generateDataBlock;
- (void)receiveEventWithScanResult:(TBScanResult *)scanResult                                                        eventDelegate:(id <TBScanPipelineEventDeletate>)delegate {
   //对数据做对象化
   TBCodeData *data = [TBCodeData new];
   data.scanResult = scanResult;
   data.delegate = delegate;
   
   NSInteger dataId = 100;
   //开始执行递归
   self.generateDataBlock(data, dataId);
}
@end
复制代码

管理处理单元的 Manager

包含的功能和特点:

  1. 管理创建数据的 Creator

  2. 管理处理单元的 Pipeline

  3. 采用支持链式的点语法,方便书写

API 代码示例如下

@interface TBPipelineManager : NSObject
/// 添加创建数据 Creator
- (TBPipelineManager *(^)(id<TBPipelineDataCreatorDelegate> dataCreator))addDataCreator;
/// 添加处理单元 Pipeline
- (TBPipelineManager *(^)(id<TBPipelineDelegate> pipeline))addPipeline;
/// 抛出经过一系列 Pipeline 的数据。当 Creator 开始调用 generateDataBlock 后,Pipeline 就开始执行
@property (nonatomic, strong) void(^throwDataBlock)(id data);
@end
复制代码

实现代码示例如下

@implementation TBPipelineManager
- (TBPipelineManager *(^)(id<TBPipelineDataCreatorDelegate> dataCreator))addDataCreator {    
   @weakify
   return ^(id<TBPipelineDataCreatorDelegate> dataCreator) {
       @strongify
       if (dataCreator) {
          [self.dataGenArr addObject:dataCreator];
      }
       return self;
  };
}

- (TBPipelineManager *(^)(id<TBPipelineDelegate> pipeline))addPipeline {
   @weakify
   return ^(id<TBPipelineDelegate> pipeline) {
       @strongify
       if (pipeline) {
          [self.pipelineArr addObject:pipeline];
           
           //每一次add的同时,我们做链式标记(通过runtime给每个处理加Next)
           if (self.pCurPipeline) {
               NSObject *cur = (NSObject *)self.pCurPipeline;                
               cur.tb_nextPipeline = pipeline;
          }
           self.pCurPipeline = pipeline;
      }
       return self;
  };
}

- (void)setThrowDataBlock:(void (^)(id _Nonnull))throwDataBlock {
   _throwDataBlock = throwDataBlock;
   
   @weakify
   //Creator的数组,依次对 Block 回调进行赋值,当业务方调用此 Block 时,就是开始处理数据的时候    
  [self.dataGenArr enumerateObjectsUsingBlock:^(id<TBPipelineDataCreatorDelegate>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
       obj.generateDataBlock = ^(id<TBPipelineBaseDataProtocol> data, NSInteger dataId) {                 @strongify
           data.dataId = dataId;
           //开始递归处理数据
          [self handleData:data];
      };
  }];
}

- (void)handleData:(id)data {
  [self recurPipeline:self.pipelineArr.firstObject data:data];
}

- (void)recurPipeline:(id<TBPipelineDelegate>)pipeline data:(id)data {
   if (!pipeline) {
       return;
  }
   
   //递归让pipeline处理数据
   @weakify
  [pipeline receiveData:data throwDataBlock:^(id  _Nonnull throwData) {
       @strongify
       NSObject *cur = (NSObject *)pipeline;
       if (cur.tb_nextPipeline) {
          [self recurPipeline:cur.tb_nextPipeline data:throwData];
      } else {
           !self.throwDataBlock?:self.throwDataBlock(throwData);
      }
  }];
}
@end
复制代码

处理单元 Pipeline

包含的功能和特点:

  1. 因为数据是基于业务的,所以它只被声明为一个 Protocol ,由上层实现。

API 代码示例如下

@protocol TBPipelineDelegate <NSObject>
//如果有错误,直接抛出
- (void)receiveData:(id)data throwDataBlock:(void(^)(id data))block;
@end
复制代码

上层业务代码示例如下

//以A类型码码处理单元为例
@implementation TBGen3Pipeline
- (void)receiveData:(id <TBCodeDataDelegate>)data throwDataBlock:(void (^)(id data))block {    
   TBScanResult *result = data.scanResult;
   NSString *scanType = result.resultType;
   NSString *scanData = result.data;
   
   if ([scanType isEqualToString:TBScanResultTypeA]) {
       //跳转逻辑
      ...
       //可以处理,终止递归
       BlockInPipeline();
  } else {
       //不满足处理条件,继续递归:由下一个 Pipeline 继续处理
       PassNextPipeline(data);
  }
}
@end
复制代码

业务层调用

有了上述的框架和上层实现,生成一个码处理管理就很容易且能达到解耦的目的,代码示例如下

- (void)setupPipeline { 
  //创建 manager 和 creator
  self.manager = TBPipelineManager.new;
  self.dataCreator = TBDataCreator.new;
   
  //创建 pipeline
  TBCodeTypeAPipelie *codeTypeAPipeline = TBCodeTypeAPipelie.new;
  TBCodeTypeBPipelie *codeTypeBPipeline = TBCodeTypeBPipelie.new;
  //...
  TBCodeTypeFPipelie *codeTypeFPipeline = TBCodeTypeFPipelie.new;
   
  //往 manager 中链式添加 creator 和 pipeline
  @weakify
  self.manager
  .addDataCreator(self.dataCreator)
  .addPipeline(codeTypeAPipeline)
  .addPipeline(codeTypeBPipeline)
  .addPipeline(codeTypeFPipeline)
  .throwDataBlock = ^(id data) {
      @strongify
      if ([self.proxyImpl respondsToSelector:@selector(scanResultDidFailedProcess:)]) {                   [self.proxyImpl scanResultDidFailedProcess:data];
      }
  };
}
复制代码

状态模式

image.png

image.png

回头来看下码展示的逻辑,这是我们用户体验优化的一项重要内容。码展示的意思是对于当前帧/图片,识别到码位置,我们进行锚点的高亮并跳转。这里包含三种情况:

  1. 未识别到码的时候,无锚点展示

  2. 识别到单码的时候,展示锚点并在指定时间后跳转

  3. 识别到多码额时候,展示锚点并等待用户点击

可以看到,这里涉及到简单的展示状态切换,这里就引出改造的第二步:状态模式

image.png

状态模式是一种行为设计模式, 能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。

本文设计的状态模式,包含两部分:

  1. 状态的信息 StateInfo

  2. 状态的基类 BaseState

两者结构如图所示

image.png

状态的信息 StateInfo

包含的功能和特点:

  1. 当前上下文仅有一种状态信息流转

  2. 业务方可以保存多个状态键值对,状态根据需要执行相应的代码逻辑。

状态信息的声明和实现代码示例如下

@interface TBBaseStateInfo : NSObject {
   @private
   TBBaseState<TBBaseStateDelegate> *_currentState; //记录当前的 State
}
//使用当前的 State 执行
- (void)performAction;
//更新当前的 State
- (void)setState:(TBBaseState <TBBaseStateDelegate> *)state;
//获取当前的 State
- (TBBaseState<TBBaseStateDelegate> *)getState;
@end

@implementation TBBaseStateInfo
- (void)performAction {
   //当前状态开始执行
  [_currentState perfromAction:self];
}
- (void)setState:(TBBaseState <TBBaseStateDelegate> *)state {
   _currentState = state;
}
- (TBBaseState<TBBaseStateDelegate> *)getState {
   return _currentState;
}
@end
复制代码

上层业务代码示例如下

typedef NS_ENUM(NSInteger,TBStateType) {
TBStateTypeNormal, //空状态
TBStateTypeSingleCode, //单码展示态
TBStateTypeMultiCode, //多码展示态
};

@interface TBStateInfo : TBBaseStateInfo
//以 key-value 的方式存储业务 type 和对应的状态 state
- (void)setState:(TBBaseState<TBBaseStateDelegate> *)state forType:(TBStateType)type;
//更新 type,并执行 state
- (void)setType:(TBStateType)type;
@end

@implementation TBStateInfo

- (void)setState:(TBBaseState<TBBaseStateDelegate> *)state forType:(TBStateType)type {
[self.stateDict tb_setObject:state forKey:@(type)];
}

- (void)setType:(TBStateType)type {
id oldState = [self getState];
//找到当前能响应的状态
id newState = [self.stateDict objectForKey:@(type)];
//如果状态未发生变更则忽略
if (oldState == newState)
return;
if ([newState respondsToSelector:@selector(perfromAction:)]) {
[self setState:newState];
//转态基于当前的状态信息开始执行
[newState perfromAction:self];
}
}
@end
复制代码

状态的基类 BaseState

包含的功能和特点:

  1. 定义了状态的基类

  2. 声明了状态的基类需要遵循的 Protocol

Protocol 如下,基类为空实现,子类继承后,实现对 StateInfo 的处理。

@protocol TBBaseStateDelegate <NSObject>
- (void)perfromAction:(TBBaseStateInfo *)stateInfo;
@end
复制代码

上层(以单码 State 为例)代码示例如下

@interface TBSingleCodeState : TBBaseState
@end

@implementation TBSingleCodeState

//实现 Protocol
- (void)perfromAction:(TBStateInfo *)stateAction {
   //业务逻辑处理 Start
  ...
   //业务逻辑处理 End
}

@end
复制代码

业务层调用

以下代码生成一系列状态,在合适时候进行状态的切换。

//状态初始化
- (void)setupState {
   TBSingleCodeState *singleCodeState =TBSingleCodeState.new; //单码状态
   TBNormalState *normalState =TBNormalState.new; //正常状态
   TBMultiCodeState *multiCodeState = [self getMultiCodeState]; //多码状态
   
  [self.stateInfo setState:normalState forType:TBStateTypeNormal];
  [self.stateInfo setState:singleCodeState forType:TBStateTypeSingleCode];
  [self.stateInfo setState:multiCodeState forType:TBStateTypeMultiCode];
}

//切换常规状态
- (void)processorA {
   //...
  [self.stateInfo setType:TBStateTypeNormal];
   //...
}

//切换多码状态
- (void)processorB {
   //...
  [self.stateInfo setType:TBStateTypeMultiCode];
   //...
}

//切换单码状态
- (void)processorC {
   //...
  [self.stateInfo setType:TBStateTypeSingleCode];
   //...
}
复制代码

最好根据状态机图编写状态切换代码,以保证每种状态都有对应的流转。

次态→ 初态↓状态A状态B状态C
状态A条件A......
状态B.........
状态C.........

代理模式

图片

在开发过程中,我们会在越来越多的地方使用到上图能力,比如「淘宝拍照」的相册中、「扫一扫」的相册中,用到解码码展示码处理的能力。

因此,我们需要把这些能力封装并做成插件化,以便在任何地方都能够使用。这里就引出了我们改造的第三步:代理模式。

代理模式是一种结构型设计模式,能够提供对象的替代品或其占位符。代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。 本文设计的状态模式,包含两部分:

  1. 代理单例 GlobalProxy

  2. 代理的管理 ProxyHandler

两者结构如图所示

图片

代理单例 GlobalProxy

单例的目的主要是减少代理重复初始化,可以在合适的时机初始化以及清空保存的内容。单例模式对于 iOSer 再熟悉不过了,这里不再赘述。

代理的管理 Handler

维护一个对象,提供了对代理增删改查的能力,实现对代理的操作。这里实现 Key - Value 的 Key 为 Protocol ,Value 为具体的代理。

代码示例如下

+ (void)registerProxy:(id)proxy withProtocol:(Protocol *)protocol {
   if (![proxy conformsToProtocol:protocol]) {
       NSLog(@"#TBGlobalProxy, error");
       return;
  }
   if (proxy) {
      [[TBGlobalProxy sharedInstance].proxyDict setObject:proxy forKey:NSStringFromProtocol(protocol)];
  }
}

+ (id)proxyForProtocol:(Protocol *)protocol {
   if (!protocol) {
       return nil;
  }
   id proxy = [[TBGlobalProxy sharedInstance].proxyDict objectForKey:NSStringFromProtocol(protocol)];
   return proxy;
}

+ (NSDictionary *)proxyConfigs {
   return [TBGlobalProxy sharedInstance].proxyDict;
}

+ (void)removeAll {
  [TBGlobalProxy sharedInstance].proxyDict = [[NSMutableDictionary alloc] init];
}
复制代码

业务层的调用

所以不管是什么业务方,只要是需要用到对应能力的地方,只需要从单例中读取 Proxy,实现该 Proxy 对应的 Protocol,如一些回调、获取当前上下文等内容,就能够获取该 Proxy 的能力。

//读取 Proxy 的示例
- (id <TBScanProtocol>)scanProxy {
   if (!_scanProxy) {
       _scanProxy = [TBGlobalProxy proxyForProtocol:@protocol(TBScanProtocol)];
  }
   _scanProxy.proxyImpl = self;
   return _scanProxy;
}

//写入 Proxy 的示例(解耦调用)
- (void)registerGlobalProxy {
   //码处理能力
  [TBGlobalProxy registerProxy:[[NSClassFromString(@"TBScanProxy") alloc] init]                                   withProtocol:@protocol(TBScanProtocol)];
   //解码能力
  [TBGlobalProxy registerProxy:[[NSClassFromString(@"TBDecodeProxy") alloc] init]                                 withProtocol:@protocol(TBDecodeProtocol)];}
复制代码

扫一扫新架构

基于上述的改造优化,我们将原扫一扫架构进行了优化:将逻辑&展现层进行代码分拆,分为属现层、逻辑层、接口层。已达到层次分明、职责清晰、解耦的目的。

image.png

总结

上述沉淀的三个设计模式作为扫拍业务的 Foundation 的 Public 能力,应用在镜头页的业务逻辑中。

通过此次重构,提高了扫码能力的复用性,结构和逻辑的清晰带来的是维护成本的降低,不用再大海捞针从代码“巨无霸”中寻找问题,降低了开发人日。


作者:阿里巴巴大淘宝技术
来源:https://juejin.cn/post/7127858822395199502

收起阅读 »

iOS 消息调用过程

iOS
iOS 消息调用属于基本知识,苹果官方有一个详细的介绍图:iOS 工程中,调用对象的方法,就是向对象发送消息。我们知道,iOS 中的方法分为实例方法和对象方法。iOS 所有的对象都是继承至 NSObject, 编译完成后,在对象的定义中,存在一个实例方法链表、...
继续阅读 »

iOS 消息调用属于基本知识,苹果官方有一个详细的介绍图:


iOS 工程中,调用对象的方法,就是向对象发送消息。我们知道,iOS 中的方法分为实例方法和对象方法。iOS 所有的对象都是继承至 NSObject, 编译完成后,在对象的定义中,存在一个实例方法链表、一个缓存方法链表。当对实例 son 发送消息后,会在 son 缓存方法链表中寻找;缓存中没有时,向实例方法链表寻找;再找不到,会向父类的实例方法缓存链表 -> 父类的实例方法链表寻找,直至 NSObject。在 NSObject 中会经历以下两个步骤:
1 - (BOOL)resolveInstanceMethod:(SEL)sel ; 
2 - (id)forwardingTargetForSelector:(SEL)aSelector ;

如果在步骤 2 中范围 nil, 就会触发 iOS 的崩溃。

当向 Son 发送类方法时,会首先向 Son 的元类 metaClass 中的类缓存方法链表中寻找,然后类方法链表,然后直接在 NSObject 进行缓存方法链表 -> 类方法链表的寻找路径 . 在 NSObject 中会经历如下两个步骤:


实例的 methodList 链表中寻找方法,找不到时会寻找 Son 的类方法,仍然找不到时,会寻找父类的方法链表,直到 NSObject 。


其中不同对象间的切换,通过 isa 指针完成,实例 son 的 isa 指向类 Son, 类 Son 的 isa 指向元类,元类的 isa 指向父类的元类, 父类的元类向上传递,直至 NSObject .


NSObject 的指针 isa 指向其本身,在想 NSObject 发送消息时,会经历如下步骤:

1 + (BOOL)resolveClassMethod:(SEL)sel ; 
2 - (void)doesNotRecognizeSelector:(SEL)aSelector ;
当调用方法 2 时,会触发 iOS 的崩溃。利用以上机制,可以对resolveInstanceMethod 和 resolveClassMethod 两个方法进行方法交换,拦截可能出现的 iOS 崩溃,然后自定义处理。
作者:iOS猿_员
链接:https://www.jianshu.com/p/1a76ccad4e73
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS面试--虎牙最新iOS开发面试题

iOS
关于面试题,可能没那么多时间来总结答案,有什么需要讨论的地方欢迎大家指教。主要记录一下准备过程,和面试的一些总结,希望能帮助到正在面试或者将要面试的同学吧。 一面 项目架构,项目是自己写的吗 fps是怎么计算的 除了用cadisplay,还有什么方法吗 kv...
继续阅读 »

关于面试题,可能没那么多时间来总结答案,有什么需要讨论的地方欢迎大家指教。主要记录一下准备过程,和面试的一些总结,希望能帮助到正在面试或者将要面试的同学吧。


一面



  • 项目架构,项目是自己写的吗

  • fps是怎么计算的

  • 除了用cadisplay,还有什么方法吗

  • kvo怎么实现

  • leaks怎么实现

  • 如何代码实现监听僵尸对象

  • imageWithName什么时候发生编解码,在什么线程

  • isa指针里面有什么

  • 消息发送和消息转发流程

  • 函数里面的参数怎么存储

  • oc一个空函数里面有参数吗

  • 他们存在栈还是寄存器

  • 红黑树等查找时间复杂度

  • nsdictionary的实现

  • iOS的各种锁

  • 如何实现dispatch once,要考虑什么问题

  • 同一线程里面使用两个@synconize会怎么样,是递归锁还是非递归锁

  • 如何增加按钮点击范围


二面



  • 说一下ARC

  • autoreleasepool可以用来干嘛

  • 里面的对象什么时候释放,是出来就释放吗

  • 消息转发可以用来干什么

  • runloop是干什么,你用来干什么了

  • 说一下c++多态和虚函数表

  • TCP如何保证数据传输完整性

  • TCP为什么三次握手

  • http和https,全程都是非对称加密吗

  • 开放性问题,很多乱序数据过来,你要怎么考虑排序方法的设计

  • 对RxSwift的看法,有用过吗?


三面



  • iOS对象指针大小

  • 对象分配到堆还是栈

  • http怎么区分header和body

  • 多线程可以访问同一个对象吗,多进程呢

  • 视频pts和dts

  • 视频丢帧丢哪个好点

  • iOS各种锁的性能,琐是毫秒级别还是微妙级别

  • http请求是异步还是同步

  • 怎么看待rn和flutter


作者:iOS弗森科
链接:https://www.jianshu.com/p/17849abb722c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS之iOS13适配总结

iOS
前言 随便iOS开发开始更新变成Xcode11,适配iOS13变成了现在的当务之急。 新特性适配 一、新添加的Dark Mode iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整,具...
继续阅读 »

前言


随便iOS开发开始更新变成Xcode11,适配iOS13变成了现在的当务之急。


新特性适配


一、新添加的Dark Mode


iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整,具体适配可见: Implementing Dark Mode on iOS


切换、修改当前 UIViewController 或 UIView的模式。只要设置了控制器为暗黑模式,那么它子view也会对应的修改。



  • 只修改当前UIViewController或UIView的模式。

  • 只要设置了控制器为暗黑模式,那么它子view也会对应的修改。


代码如下:

if (@available(iOS 13.0, *)) {
self.overrideUserInterfaceStyle = UIUserInterfaceStyleDark;//UIUserInterfaceStyleLight
} else {
// Fallback on earlier versions
}
注意当我们在window上设置 overrideUserInterfaceStyle的时候,就会影响 window下所有的controller,view,包括后续推出的 controller。



二、使用KVC访问私有变量已发崩溃


iOS13之后就不能通过KVC访问修改私有属性,不然就会找不到这个key,从而引发崩溃。


目前搜集到的KVC访问权限引发崩溃的方法:



  1. UIApplication -> _statusBar

  2. UITextField -> _placeholderLabel

  3. UITabBarButton -> _info

  4. UISearchBar -> _searchField

  5. UISearchBar -> _cancelButton

  6. UISearchBar -> _cancelButtonText

  7. UISearchBar -> UISearchBarBackground


1、UIApplication -> _statusBar 获取状态栏崩溃


在iOS13上获取状态栏statusBar,不能直接用KVC。要使用performSelector

UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;
UIView *statusBar;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {

UIView *localStatusBar= [statusBarManager performSelector:@selector(createLocalStatusBar)];
if ([localStatusBar respondsToSelector:@selector(statusBar)]) {

statusBar = [localStatusBar performSelector:@selector(statusBar)];
}
}



适配的时候就是iOS13和非iOS13

if(@available(iOS 13.0, *)) {

//上面获取statusBar代码
} else {

UIView *statusBar = [[UIApplication sharedApplication]
valueForKey:@"statusBar"];

}



2、UITextField -> _placeholderLabel


在iOS13 UITextField通过KVC来获取_placeholderLabel会引发崩溃。

//在ios13使用会引发崩溃
[self.textField setValue:self.placeholderColor
forKeyPath:@"_placeholderLabel.textColor"];



崩溃如下:

'Access to UITextField's _placeholderLabel ivar is prohibited. 
This is an application bug'

解决方案:UITextField有个attributedPlaceholder的属性,我们可以自定义这个富文本来达到我们需要的结果。

NSMutableAttributedString *placeholderString = [[NSMutableAttributedString alloc] initWithString:placeholder 
attributes:@{NSForegroundColorAttributeName : self.placeholderColor}];
_textField.attributedPlaceholder = placeholderString;



3、UISearchBar 黑线处理导致崩溃


iOS13之前为了处理搜索框的黑线问题,通常会遍历searchBar的 subViews,找到并删除UISearchBarBackground。


在 iOS13 中这么做会导致UI渲染失败,然后直接崩溃,崩溃信息如下:

erminating app due to uncaught exception'NSInternalInconsistencyException', reason: 'Missing or detached view for search bar layout'



解决方案:修改方法为:设置 UISearchBarBackground 的 layer.contents 为 nil

for (UIView *view in _searchBar.subviews.lastObject.subviews) {
if ([view isKindOfClass:NSClassFromString(@"UISearchBarBackground")]) {
view.layer.contents = nil;
break;
}
}



4、iOS UISearchBar通过kvc获取_cancelButtonText、_cancelButton、_searchField引发崩溃。


先说一下_searchField来说明的解决方案。


在iOS13之前,我们通过"_searchField"来获取UISearchTextField来修改一些属性。

UITextField *searchFiled = [self valueForKey:@"_searchField"];



但在iOS13会引发崩溃,解决方案就是在iOS13中引入了名为searchTextField的属性。

@property (nonatomic, readonly) UISearchTextField *searchTextField;

查看一下UISearchTextField

UIKIT_CLASS_AVAILABLE_IOS_ONLY(13.0)
@interface UISearchTextField : UITextField
///功能省略
@end



发现UISearchTextField继承UITextField,代码实现:

UITextField *searchField;
if(@available(iOS 13.0, *)) {
//UISearchBar的self.searchTextField属性是readonly,不能直接用
searchField = self.searchTextField;
} else {
searchField = [self valueForKey:@"_searchField"];
}

三、presentViewController 默认弹出样式



  • 苹果将 UIViewController 的 modalPresentationStyle 属性的默认值改成了新加的一个枚举值 UIModalPresentationAutomatic,对于多数 UIViewController,此值会映射成 UIModalPresentationPageSheet。

  • iOS13系统的默认样式是: UIModalPresentationAutomatic

  • iOS12及以下系统的默认样式是:UIModalPresentationFullScreen;


想要改成以前默认的样式

- (UIModalPresentationStyle)modalPresentationStyle {
return UIModalPresentationFullScreen;
}



四、AVPlayerViewController 替换MPMoviePlayerController


在 iOS 9 之前播放视频可以使用 MediaPlayer.framework 中的MPMoviePlayerController类来完成,它支持本地视频和网络视频播放。但是在 iOS 9 开始被弃用,如果在 iOS 13 中继续使用的话会直接抛出异常:

'MPMoviePlayerController is no longer available. Use AVPlayerViewController in AVKit.'

解决方案:

既然不能再用了,那只能换掉了。替代方案就是AVKit里面的那套播放器。


五、废弃UIWebview 改用 WKWebView


iOS13 开始苹果将 UIWebview 支持的系统(iOS2.0-iOS12.0),目前提交苹果应用市场(App Store)会发送邮件提示你在下一次提交时将应用中UIWebView的api移除。


虽然暂时没有强制必须替换WKWebView,但是在iOS13开始UIWebView已是废弃的API,所以还是越早换越好。


六、iOS13 获取window适配


在iOS13通过UIWindowScene的方式获取window

UIWindow* window = nil;
if (@available(iOS 13.0, *)) {
for (UIWindowScene* windowScene in [UIApplication sharedApplication].connectedScenes) {
if (windowScene.activationState == UISceneActivationStateForegroundActive) {
window = windowScene.windows.firstObject;
break;
}
}
}else{
window = [UIApplication sharedApplication].keyWindow;
}



七、iOS13 废弃LaunchImage


从iOS8的时候,苹果就引入了LaunchScreen,我们可以设置 LaunchScreen来作为启动页。当然,现在你还可以使用LaunchImage来设置启动图。


但是从2020年4月开始,所有使⽤ iOS13 SDK的 App将必须提供 LaunchScreen,LaunchImage即将退出历史舞台。使用LaunchScreen有点:



  • 不需要单独适配种屏幕尺寸的启动图

  • LaunchScreen是支持AutoLayout+SizeClass的,所以适配各种屏幕都不在话下


七、iOS13 适配UISegmentedControl


默认样式变为白底黑字,如果设置修改过颜色的话,页面需要修改。

如下图:



其次设置选中颜色的tintColor属性在iOS13已经失效,所以在iOS13新增新增了selectedSegmentTintColor 属性用以修改选中的颜色。


适配代码如下:

if ( @available(iOS 13.0, *)) {
self.segmentedControl.selectedSegmentTintColor = [UIColor yellowcolor];
} else {
self.segmentedControl.tintColor = [UIColor yellowcolor];
}
作者:枫叶无处漂泊
链接:https://www.jianshu.com/p/acde9bc3fc97
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

iOS推送通知及静默推送相关

iOS
    在IOS推送服务中,Apple提供了两种不同方式的推送形式,一种是在通知栏上面显示的推送;另一种则是不带消息提醒的推送,俗称“静默消息”。1. 普通推送和静默推送的区别      &...
继续阅读 »

    在IOS推送服务中,Apple提供了两种不同方式的推送形式,一种是在通知栏上面显示的推送;另一种则是不带消息提醒的推送,俗称“静默消息”。

1. 普通推送和静默推送的区别

        普通推送:收到推送后(有文字有声音),点开通知,进入APP后,才执行

- (void)application:(UIApplication didReceiveRemoteNotification:(NSDictionary fetchCompletionHandler:(void result))handler *)application *)userInfo (^)(UIBackgroundFetchResult


        静默推送:(Silent Push)并不是必须要“静默”(通常是没有文字没有声音),只要推送payload中aps字典里包含了"content-available": 1的键值对,都具有静默推送的特性,不用点开通知,不用打开APP,就能执行

-(void)application:(UIApplication )application)userInfo didReceiveRemoteNotification:(NSDictionary fetchCompletionHandler:(void (^)(UIBackgroundFetchResultresult))handler


用户完全感觉不到所以静默推送又被我们称做 Background Remote Notification(后台远程推送)。

        静默推送是在iOS7之后推出的一种推送方式。它与其他推送的区别在于允许应用收到通知后在后台(background)状态下运行一段代码,可用于从服务器获取内容更新。

PS:注册消息通知时通常的弹窗询问权限有什么用呢?其实只是请求用户允许在推送通知到来时能够有alert, badge和sound,而并不是在请求注册推送本身的权限。静默推送即使用户不允许应用的推送,静默推送依然会送达用户设备,只是不会有alert, badge和sound。这也符合静默推送的正常使用场景。



2. 远程推送时 , 应用的几种状态及对应回调方法

     (1) . 应用开启时 , 应用在前台

     (2) . 应用开启时 , 应用在后台

     (3) . 应用未启动(应用被杀死)

从苹果APNS服务器远程推送时:

不使用时(iOS10以后可用)

1 . 如果应用处于 (1) 状态 , 则不会发出声音 , 会直接调用appDelegate的代理方法didReceiveRemoteNotification(didReceiveRemoteNotification:fetchCompletionHandler:)

2 . 如果应用处于 (2) 状态 , 则会发出提示音, 点击推送消息 , 则会调用appDelegate的代理方法didReceiveRemoteNotification

3 . 如果应用处于 (3) 状态,则会发出提示音 , 点击推送消息 , 则会开启应用 , 在下面这个方法中会带上launchOptions这个参数,如果实现了application:didReceiveRemoteNotification:fetchCompletionHandler:这个方法,则还会调用这个方法

注:didReceiveRemoteNotification指以下两个方法。两个方法互斥。在两方法都实现的情况下方法2优先级高

1. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo

2. - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler


iOS10使用

1 . 如果应用处于 (1) 状态 , 会发出声音 , 会直接调用appDelegate的代理方法userNotificationCenter:willPresentNotification:withCompletionHandler

2 . 如果应用处于 (2) 状态 , 则会发出提示音, 点击推送消息 , 则会调用appDelegate的代理方法

userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler

3 . 如果应用处于 (3) 状态,则会发出提示音 , 点击推送消息 , 则会开启应用 , 在下面这个方法中会带上launchOptions这个参数,如果实现了userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler这个方法,则还会调用这个方法



2. 静默推送及app的状态切换

        在大多数情况下,启动一个app后都是进入前台,比如我们点击应用图标或点推送通知来启动应用。其实app在某些后台事件和特定条件下是可以直接启动到后台(launch into the background)的。

    2.1 应用状态之一Suspended

        这种状态其实和Background类似,而且从用户角度讲应用现在看起来确实是在“后台”,但它和Background状态不同的是Suspended下已经不能执行代码了。应用何时会进Suspended就是玄学了,这是由iOS系统自动控制的,而且不会有任何回调,可以看到UIApplicationDelegate里并没有像applicationWillBecomeSuspended:这种东西。这种状态下的应用虽然还在内存中,但是一旦设备内存吃尽,比如开了炉石传说的游戏,那么系统就会优先干掉(文档上用的是purge这个词)处于Suspended状态的应用,而且也不会有回调。

    2.2 应用启动到前台的生命周期(以点击应用图标开始)

    AppDelegate中走的回调方法 

 · application:willFinishLaunchingWithOptions:

· application:didFinishLaunchingWithOptions:

· applicationDidBecomeActive:


    静默推送可以使应用启动到后台

        前提是应用先被退到后台,过一段时间被系统移入Suspended状态,然后又被系统在内存吃紧时回收了内存(相当于应用已经被系统正当杀掉,而非用户双击Home键杀掉),在这以后,该应用收到静默推送即会启动应用到后台。

    AppDelegate中走的回调方法变为

 · application:willFinishLaunchingWithOptions:

· application:didFinishLaunchingWithOptions:

· applicationDidEnterBackground:


        这个过程中,系统不会显示应用的window,就是说我们不会看到手机屏幕上突然鬼畜一下应用启动,但是应用的第一屏会被加载和渲染,比如你的window.rootViewController是一个TabBarController,那么它及其默认选中的selectedViewController都会被加载和渲染。这是因为系统认为在后台执行完任务后可能会有UI上的更新,所以在applicationDidEnterBackground:方法执行结束后便会有个快速的截图,来更新用户双击Home时看到的那个应用截图。


3. 收到静默推送时的后续该如何处理。

        application:didReceiveRemoteNotification:fetchCompletionHandler:

        这是应用收到静默推送的回调方法,我们最多有30s的时间来处理数据,比如静默推送表示某个列表或资源有更新,你可以在此处下载数据,在下载处理完数据后需要尽快调用completionHandler(...)告诉系统处理完毕。

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {

[Downloader fetchData:^(id x){ // 处理数据,更新UI 等

completionHandler(UIBackgroundFetchResultNewData);

}];

}


        如果这次是启动到后台的情况,调用completionHandler(...)后会使应用马上进入之前的状态。那就有可能遇到这样的问题:很多时候我们需要在启动时发送一堆业务上的API请求,如果这次静默推送没有数据需要下载和处理,就会刚把启动处的API请求发出,就调用了completionHandler(...),导致发出的这些请求在下次打开应用时显示超时。这种情况下我们可以强行延时下completionHandler(...)的调用,来保证能在这次收到那些API的返回。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

completionHandler(UIBackgroundFetchResultNoData);

});



4. 静默推送


应用想收到静默推送需要满足的条件:

1.应用在前台/后台 (应用被杀死就收不到了)

2.应用实现了

application:didReceiveRemoteNotification:fetchCompletionHandler:/application:didReceiveRemoteNotification:

3. 消息定义时需设置:"content-available" = 1

流程:

  1. 移动端注册消息,向APNs服务器获取deviceToken,并提交给后台保存;

  2. 后台定义消息,并推送给APNs服务器。APNs根据deviceToken做分发。

  3. 移动端收到推送消息后的逻辑处理。

消息定义示例:

特殊说明:

1. APNS去掉alert、badge、sound字段实现静默推送,增加增加字段"content-available":1,也可以在后台做一些事情。

//静默推送消息格式

{

"aps":{

"alert":"",

"content-available":1

},

"userInfo":"test"

}


*/

小结:

1.应用在后台/前台/被杀死,都可以收到普通的远程推送

2.应用在后台/前台时,可以通过静默推送,修改一些数据

3.应用被杀死时(相当于应用已经被系统正当杀掉,而非用户双击Home键杀掉),可以通过Background Fetch短时间唤醒应用



作者:Aliv丶Zz
链接:https://www.jianshu.com/p/0275d9a9592b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

iOS获取设备的网络状态(已适配iOS13,iOS14无变化)

iOS
前言 小编最近在项目中遇到了一个问题,除刘海屏以外的iOS设备可以正常的搜索到硬件设备,但是刘海屏就不行。因此,小编花了一点时间研究了一下iOS设备获取当前设备的网络状态。 实现 因为iOS的系统是封闭的,所以是没有直接的APi去获取当前的网络状态。但是道高一...
继续阅读 »

前言


小编最近在项目中遇到了一个问题,除刘海屏以外的iOS设备可以正常的搜索到硬件设备,但是刘海屏就不行。因此,小编花了一点时间研究了一下iOS设备获取当前设备的网络状态。


实现


因为iOS的系统是封闭的,所以是没有直接的APi去获取当前的网络状态。但是道高一尺,魔高一尺。开发者总会有办法获取自己想要的东西。


1.网络状态获取


获取当前的网络类型

获取当前的网络类型是通过获取状态栏,然后遍历状态栏的视图完成的。

先导入头文件,如下:

#import "AppDelegate.h"

实现方法如下:

+ (NSString *)getNetworkType
{
UIApplication *app = [UIApplication sharedApplication];
id statusBar = nil;
// 判断是否是iOS 13
NSString *network = @"";
if (@available(iOS 13.0, *)) {
UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if ([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {
UIView *localStatusBar = [statusBarManager performSelector:@selector(createLocalStatusBar)];
if ([localStatusBar respondsToSelector:@selector(statusBar)]) {
statusBar = [localStatusBar performSelector:@selector(statusBar)];
}
}
#pragma clang diagnostic pop

if (statusBar) {
// UIStatusBarDataCellularEntry
id currentData = [[statusBar valueForKeyPath:@"_statusBar"] valueForKeyPath:@"currentData"];
id _wifiEntry = [currentData valueForKeyPath:@"wifiEntry"];
id _cellularEntry = [currentData valueForKeyPath:@"cellularEntry"];
if (_wifiEntry && [[_wifiEntry valueForKeyPath:@"isEnabled"] boolValue]) {
// If wifiEntry is enabled, is WiFi.
network = @"WIFI";
} else if (_cellularEntry && [[_cellularEntry valueForKeyPath:@"isEnabled"] boolValue]) {
NSNumber *type = [_cellularEntry valueForKeyPath:@"type"];
if (type) {
switch (type.integerValue) {
case 0:
// 无sim卡
network = @"NONE";
break;
case 1:
network = @"1G";
break;
case 4:
network = @"3G";
break;
case 5:
network = @"4G";
break;
default:
// 默认WWAN类型
network = @"WWAN";
break;
}
}
}
}
}else {
statusBar = [app valueForKeyPath:@"statusBar"];

if ([[[self alloc]init]isLiuHaiScreen]) {
// 刘海屏
id statusBarView = [statusBar valueForKeyPath:@"statusBar"];
UIView *foregroundView = [statusBarView valueForKeyPath:@"foregroundView"];
NSArray *subviews = [[foregroundView subviews][2] subviews];

if (subviews.count == 0) {
// iOS 12
id currentData = [statusBarView valueForKeyPath:@"currentData"];
id wifiEntry = [currentData valueForKey:@"wifiEntry"];
if ([[wifiEntry valueForKey:@"_enabled"] boolValue]) {
network = @"WIFI";
}else {
// 卡1:
id cellularEntry = [currentData valueForKey:@"cellularEntry"];
// 卡2:
id secondaryCellularEntry = [currentData valueForKey:@"secondaryCellularEntry"];

if (([[cellularEntry valueForKey:@"_enabled"] boolValue]|[[secondaryCellularEntry valueForKey:@"_enabled"] boolValue]) == NO) {
// 无卡情况
network = @"NONE";
}else {
// 判断卡1还是卡2
BOOL isCardOne = [[cellularEntry valueForKey:@"_enabled"] boolValue];
int networkType = isCardOne ? [[cellularEntry valueForKey:@"type"] intValue] : [[secondaryCellularEntry valueForKey:@"type"] intValue];
switch (networkType) {
case 0://无服务
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"NONE"];
break;
case 3:
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"2G/E"];
break;
case 4:
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"3G"];
break;
case 5:
network = [NSString stringWithFormat:@"%@-%@", isCardOne ? @"Card 1" : @"Card 2", @"4G"];
break;
default:
break;
}

}
}

}else {

for (id subview in subviews) {
if ([subview isKindOfClass:NSClassFromString(@"_UIStatusBarWifiSignalView")]) {
network = @"WIFI";
}else if ([subview isKindOfClass:NSClassFromString(@"_UIStatusBarStringView")]) {
network = [subview valueForKeyPath:@"originalText"];
}
}
}

}else {
// 非刘海屏
UIView *foregroundView = [statusBar valueForKeyPath:@"foregroundView"];
NSArray *subviews = [foregroundView subviews];

for (id subview in subviews) {
if ([subview isKindOfClass:NSClassFromString(@"UIStatusBarDataNetworkItemView")]) {
int networkType = [[subview valueForKeyPath:@"dataNetworkType"] intValue];
switch (networkType) {
case 0:
network = @"NONE";
break;
case 1:
network = @"2G";
break;
case 2:
network = @"3G";
break;
case 3:
network = @"4G";
break;
case 5:
network = @"WIFI";
break;
default:
break;
}
}
}
}
}

if ([network isEqualToString:@""]) {
network = @"NO DISPLAY";
}
return network;
}
获取当前的Wifi信息

获取当前的Wifi信息需要借助系统的SystemConfiguration这个库。
先导入头文件,如下:

#import <SystemConfiguration/CaptiveNetwork.h>

实现方法如下:

#pragma mark 获取Wifi信息
+ (id)fetchSSIDInfo
{
NSArray *ifs = (__bridge_transfer id)CNCopySupportedInterfaces();
id info = nil;
for (NSString *ifnam in ifs) {
info = (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)ifnam);

if (info && [info count]) {
break;
}
}
return info;
}
#pragma mark 获取WIFI名字
+ (NSString *)getWifiSSID
{
return (NSString *)[self fetchSSIDInfo][@"SSID"];
}
#pragma mark 获取WIFI的MAC地址
+ (NSString *)getWifiBSSID
{
return (NSString *)[self fetchSSIDInfo][@"BSSID"];
}
获取当前的Wifi信号强度

获取信号强度与获取网络状态有点类似,通过遍历状态栏,从而获取WIFI图标的信号强度。在获取前需注意当前状态是否为WIFI。如下:

+ (int)getWifiSignalStrength{

int signalStrength = 0;
// 判断类型是否为WIFI
if ([[self getNetworkType]isEqualToString:@"WIFI"]) {
// 判断是否为iOS 13
if (@available(iOS 13.0, *)) {
UIStatusBarManager *statusBarManager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager;

id statusBar = nil;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if ([statusBarManager respondsToSelector:@selector(createLocalStatusBar)]) {
UIView *localStatusBar = [statusBarManager performSelector:@selector(createLocalStatusBar)];
if ([localStatusBar respondsToSelector:@selector(statusBar)]) {
statusBar = [localStatusBar performSelector:@selector(statusBar)];
}
}
#pragma clang diagnostic pop
if (statusBar) {
id currentData = [[statusBar valueForKeyPath:@"_statusBar"] valueForKeyPath:@"currentData"];
id wifiEntry = [currentData valueForKeyPath:@"wifiEntry"];
if ([wifiEntry isKindOfClass:NSClassFromString(@"_UIStatusBarDataIntegerEntry")]) {
// 层级:_UIStatusBarDataNetworkEntry、_UIStatusBarDataIntegerEntry、_UIStatusBarDataEntry

signalStrength = [[wifiEntry valueForKey:@"displayValue"] intValue];
}
}
}else {
UIApplication *app = [UIApplication sharedApplication];
id statusBar = [app valueForKey:@"statusBar"];
if ([[[self alloc]init]isLiuHaiScreen]) {
// 刘海屏
id statusBarView = [statusBar valueForKeyPath:@"statusBar"];
UIView *foregroundView = [statusBarView valueForKeyPath:@"foregroundView"];
NSArray *subviews = [[foregroundView subviews][2] subviews];

if (subviews.count == 0) {
// iOS 12
id currentData = [statusBarView valueForKeyPath:@"currentData"];
id wifiEntry = [currentData valueForKey:@"wifiEntry"];
signalStrength = [[wifiEntry valueForKey:@"displayValue"] intValue];
// dBm
// int rawValue = [[wifiEntry valueForKey:@"rawValue"] intValue];
}else {
for (id subview in subviews) {
if ([subview isKindOfClass:NSClassFromString(@"_UIStatusBarWifiSignalView")]) {
signalStrength = [[subview valueForKey:@"_numberOfActiveBars"] intValue];
}
}
}
}else {
// 非刘海屏
UIView *foregroundView = [statusBar valueForKey:@"foregroundView"];

NSArray *subviews = [foregroundView subviews];
NSString *dataNetworkItemView = nil;

for (id subview in subviews) {
if([subview isKindOfClass:[NSClassFromString(@"UIStatusBarDataNetworkItemView") class]]) {
dataNetworkItemView = subview;
break;
}
}

signalStrength = [[dataNetworkItemView valueForKey:@"_wifiStrengthBars"] intValue];

return signalStrength;
}
}
}
return signalStrength;
}

2.Reachability的使用

下载开源类Reachability,然后根据文档使用即可(该类把移动网络统称为WWAN):+ (NSString *)getNetworkTypeByReachability

{
NSString *network = @"";
switch ([[Reachability reachabilityForInternetConnection]currentReachabilityStatus]) {
case NotReachable:
network = @"NONE";
break;
case ReachableViaWiFi:
network = @"WIFI";
break;
case ReachableViaWWAN:
network = @"WWAN";
break;
default:
break;
}
if ([network isEqualToString:@""]) {
network = @"NO DISPLAY";
}
return network;
}

上次发布了这篇文章之后,有人问我,怎么才能获取设备的IP地址呢?在这里,小编附上获取iP地址的方法。
先导入头文件,如下:

#import <ifaddrs.h>
#import <arpa/inet.h>

实现方法,如下:

#pragma mark 获取设备IP地址
+ (NSString *)getIPAddress
{
NSString *address = @"error";
struct ifaddrs *interfaces = NULL;
struct ifaddrs *temp_addr = NULL;
int success = 0;
// 检索当前接口,在成功时,返回0
success = getifaddrs(&interfaces);
if (success == 0) {
// 循环链表的接口
temp_addr = interfaces;
while(temp_addr != NULL) {
// 开热点时本机的IP地址
if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"bridge100"]
) {
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
}
if(temp_addr->ifa_addr->sa_family == AF_INET) {
// 检查接口是否en0 wifi连接在iPhone上
if([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) {
// 得到NSString从C字符串
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
}
}
temp_addr = temp_addr->ifa_next;
}
}
// 释放内存
freeifaddrs(interfaces);
return address;
}


收起阅读 »

ios - 真机无法运行

iOS
iOS 开发小记8.10日遇见问题新接手的苹果账号无法真机运行,查询一番以为是证书的问题。登录到苹果的官网发现手机有个7天无效的问题。最终解决的方式是换了个手机 添加到真机运行中就可以了但是无法运行的手机,估计是需要等到七天之后查看结果。七天之后应该是有所变化...
继续阅读 »

iOS 开发小记

8.10日遇见问题新接手的苹果账号无法真机运行,查询一番以为是证书的问题。登录到苹果的官网发现手机有个7天无效的问题。

最终解决的方式是换了个手机 添加到真机运行中就可以了

但是无法运行的手机,估计是需要等到七天之后查看结果。七天之后应该是有所变化

产生这个问题的原因:有大佬解答是因为 苹果账号被封过。导致手机的UUID被标记。换到其他的苹果账号会有这种情况的发生

(我遇到过的是被封过开发者账号的,一个开发者账号被封,里面的测试机也会被苹果标记,再换其他的开发者账号,就可以关联到一起了)

收起阅读 »

Swift 中的热重载

前言    这一年是2040年,我们最新的 MacBook M30X 处理器可以感知到瞬间编译大型 Swift 项目,听起来很神奇,对吧?除此之外,编译代码库只是我们迭代周期的一部分。包括:    1...
继续阅读 »

前言

    这一年是2040年,我们最新的 MacBook M30X 处理器可以感知到瞬间编译大型 Swift 项目,听起来很神奇,对吧?除此之外,编译代码库只是我们迭代周期的一部分。包括:
    1、重新启动它(或将其部署到设备)
    2、导航到您在应用程序中的先前位置
    3、重新生成您需要的数据。
    如果您只需要做一次的话,听起来还不错。但是如果您和我一样,在特别的一天中,对代码库进行 200 - 500 次迭代,该怎么办呢?它增加了。
    有一种更好的方法,被其他平台所接受,并且可以在 Swift/iOS 生态系统中实现。我已经用了十多年了。
    从今天开始,您想每周节省多达 10 小时的工作时间吗?


热重载

    热重载是关于摆脱编译整个应用程序并尽可能避免部署/重新启动周期,同时允许您编辑正在运行的应用程序代码并且能立即看到更改。
    这种流程改进可以每天为您节省数小时的开发时间。我跟踪我的工作一个多月,对我来说,每天节省了 1-2 小时。
    坦白地说,如果每周节省10个小时的开发时间都不能说服您去尝试,那么我认为任何方法都不能说服你。


其他平台在做什么?

    如果您只使用 Apple 平台,您会惊讶地发现有好多平台几十年前已经采用了热重载。无论您是编写 Node 还是任何其他 JS 框架,都有一个使用热重载的设置。Go 也提供了热重载(本博客使用了该特性)
    另一个例子是谷歌的 Flutter 架构,从一开始就设计用于热重载。如果您与从事 Flutter 工作的工程师交谈,你会发现他们最喜欢 Flutter 开发者体验的一点就是能够实时编写他们的应用程序。当我为《纽约时报》写了一个拼字游戏时,我很喜欢它。
    微软最近推出了 Visual Studio 2022,并为 .NET 和 标准 C++ 应用程序提供热重载,在过去的十年中,微软在开发工具和经验方面一直在大杀四方,所以这并不令人惊讶。


苹果生态系统怎么样?

    早在 2014 年推出时,很多人都对 Swift Playgrounds 感到敬畏,因为它们允许我们快速迭代并查看代码的结果,但它们并不能很好地工作,因为它存在崩溃、挂起等问题。不能支持整个iPad环境。
    在它们发布后不久,我启动了一个名为 Objective-C Playgrounds 的开源项目,它比官方 Playgrounds 运行得更快、更可靠。我的想法是设计一个架构/工作流程,利用我已经使用了几年的 DyCI 代码注入工具,该工具已经由 Paul 制作。
    自从 Swift Playgrounds 存在以来,已经过去了八年,而且它们变得更好了,但它们可靠吗?人们是否在使用它们来推动开发?

    SwiftUI 出现了,它是一项了不起的技术(尽管仍然存在错误),它引入了与 Playgrounds 非常相似的 Swift Previews 的想法,它们有什么好处吗?
    类似的故事,当它工作的时候是很好的,但是在更大的项目中,它的工作是不可靠的,而且往往中断的次数比它们工作的次数多。如果你有任何错误,他们不会为你提供调试代码的能力,因此,采用的情况有限。


我们需要等待 Apple 吗?

    如果你关注我一段时间,你就已经知道答案了,绝对不要。毕竟,我的职业生涯是构建普通 Apple 解决方案无法解决的问题:从像 Sourcery 这样的语言扩展、像 Sourcery Pro 这样的 Xcode 改进,再到 LifetimeTracker 以及许多其他开源工具。
    我们可以利用我最初在 2014 Playgrounds 中使用的相同方法。我已经使用它十多年了,并且在数十个 Swift 项目中使用它并取得了巨大的成功!
    许多年前,我从使用 DyCI 切换到 InjectionForXcode,通过利用 LLVM 互操作而不是任何 swizzling ,它的效果更好。它是一个完全免费的开源工具,您可以在菜单栏中运行,它是由多产的工程师 John Holdsworth 创建的。你应该看看他的书 Swift Secrets。
    我意识到 Playgrounds 的方法可能过于笨重,所以今天,我开源了。一个非常专注的名为 Inject 的微型库,与 InjectionForXcode 搭配使用时,将使您的 Apple 开发更加高效和愉快!
    但不要只相信我的话。看看 Alexandra 和 Nate 的反馈,在我将这个工作流程引入 The Browser Company 设置之前,他们已经非常精通了,这使得它更加令人印象深刻。


Inject

    这个小型库是完全通用的,无论您使用 UIKit、 AppKit 还是 SwiftUI,您都可以使用它。
    您无需为生产应用程序添加条件或删除 Inject 代码。它变成了无操作内联代码,将在非调试版本中被编译过程剥离。您可以在每个视图中集成一次,并持续使用数年。
    请参考 GitHub repo中关于配置项目的说明。现在让我们来看看您有哪些工作流程选项。


工作流

    SwiftUI
        只需要两行字就可以使任何 SwiftUI 启用实时编程,而当您这样做时,您将拥有比使用 Swift Previews 更快的工作流程,同时能够使用实际的生产数据。
        这是我的 Sourcery Pro 应用程序的示例,其中加载了我所有的实际数据和逻辑,使我能够即时快速迭代整个应用程序设计,而无需任何重新启动、重新加载或类似的事情。
        看看这个开发工作流程有多快吧,告诉我你宁愿在我每次接触代码时等待Xcode的重新构建和重新部署。


    UIKit / AppKit
        我们需要一种方法来清理标准命令式UI框架的代码注入阶段之间的状态。
        我创建了 Host 的概念并且在这种情况下工作的很好。有两个:

        - Inject.ViewHost
        - Inject.ViewControllerHost

        我们如何集成它?我们把我们想迭代的类包装在父级,因此我们不修改要注入的类型,而是改变父级的调用站点。
        例如,如果你有一个 SplitViewController ,它创建了 PaneA 和 PaneB ,而你想在PaneA 中迭代布局/逻辑代码,你就修改 SplitViewController 中的调用站点。

        paneA = Inject.ViewHost(
            PaneAView(whatever: arguments, you: want)
        )

        这就是你需要做的所有改变。注入现在允许你更改 PaneAView 中的任何东西,除了它的初始化API。这些变化将立即反映在你的应用程序中。


        一个更具体的例子?
        1、我下载了 Covid19 App
        2、添加 -Xlinker -interposable 到 Other Linker Flags
        3、交换了一行 Covid19TabController.swift:L63 行

        从这句:

        let vc = TwitterViewController(title: Tab.twitter.name, usernames: Twitter.content)

        替换为:

        let vc = Inject.ViewControllerHost(TwitterViewController(title: Tab.twitter.name, usernames: Twitter.content))

        现在,我可以在不重新启动应用程序的情况下迭代控制器设计。


这是如何运作的呢?

    Hosts 利用了自动闭包,因此每次您注入代码时,我们都会使用与最初相同的参数创建您类型的新实例,从而允许您迭代任何代码、内存布局和其他所有内容。你唯一不能改变的是你的初始化 API。


逻辑注入如何呢?

    像 MVVM / MVC 这样的标准架构可以获得免费的逻辑注入,重新编译你的类,当方法重新执行时,你已经在使用新代码了。
    如果像我一样,你喜欢 PointFree Composable Architecture,你可能想要注入 reducer 代码。Vanilla TCA 不允许这样做,因为 reducer 代码是一个免费功能,不能直接用注入替换,但我们在 The Browser Company 的分支 支持它。
    当我最初开始咨询 TBC 时,我想要的第一件事是将 Inject 和 XcodeInjection 集成到我们的工作流程中。公司管理层非常支持。
    如果您切换到我们的 TCA 分支(我们保持最新),你可以在 UI 和 TCA 层上使用 Inject 。


它有多可靠?

    没有什么是完美的,但我已经使用它十多年了。它比 Apple 技术(Playgrounds / Previews)可靠得多。
如果您投入时间学习它,它将为您和您的团队节省数千小时!

收起阅读 »

iOS-底层原理 04:NSObject的alloc 源码分析

iOS
主要自定义类的alloc的alloc的源码实现中加一个断点,同时需要暂时关闭断点运行target,断点断在alloc源码的断点,然后继续执行,会出现以下这种现象探索Why【第一步】探索Debug --> Debug Workflow --> 勾选 ...
继续阅读 »

主要NSObject中的alloc是与自定义类的alloc源码流程的区别,以及为什么NSObject中的alloc不走源码工程。

上一篇文章中分析了alloc的源码,这篇文章是作为对上一篇文章的补充,去探索为什么NSObject的alloc方法不走源码工程。

NSObject的alloc无法进入源码的问题

首先在objc4-781可编译源码中的main函数中增加一个NSObject定义的对象,NSObject 和 LGPersong同时加上断点



alloc的源码实现中加一个断点,同时需要暂时关闭断点


运行target,断点断在NSObject部分,打开alloc源码的断点,然后继续执行,会出现以下这种现象


探索Why

【第一步】探索[NSObject alloc]走的是哪步源码

接下来,我们就来探索为什么NSObject的alloc会出现这种情况,首先,

  • 打开Debug --> Debug Workflow --> 勾选 Always Show Disassemly,开启汇编调试

    关闭源码的断点,只留main中的断点,重新运行程序,然后通过下图的汇编可以发现NSObject并没有走 alloc源码,而是走的objc_alloc


然后关闭汇编调试,在全局搜索 objc_alloc,在objc_alloc中加一个断点,先暂时关闭,


重新运行进行调试,断住,然后打开objc_alloc的断点,发现会进入objc_alloc的源码实现,此时查看 cls 是 NSObject


【第二步】探索 NSObject 为什么走 objc_alloc?

首先,我们来看看 NSObject 与 LGPerson的区别

  • NSObject 是iOS中的基类,所有自定义的类都需要继承自NSObject
  • LGPerson 是继承NSObject类的,重写NSObject中的alloc方法

然后根据第一步中汇编的显示,可以看出,NSObject 和 LGPerson 都调用了objc_alloc,所以这里就有两个疑问

  • 为什么NSObject 调用alloc方法 会走到 objc_alloc 源码?
  • 为什么LGPerson中的alloc 会走两次?即调用了alloc,进入源码,然后还要走到 objc_alloc

LGPerson中alloc 走两次 的 Why?

首先,需要在源码中调试,在mainLGPerson加断点,断在LGPerson,再在alloc 、 objc_alloc 和 calloc 源码加断点,运行demo,会断在objc_alloc源码中(重新运行前需要暂时关闭源码中的所有断点)


继续运行,发现LGPerson 第一次的alloc会走到 objc_alloc --> callAlloc方法中最下方的objc_msgSend,表示向系统发送消息



所以由上述调试过程可以得出,LGPerson两次的原因是首先需要去查找sel,以及对应的imp的关系,当前需要查找的是 alloc的方法编号,但是为什么会找到objc_alloc?这个就需要问系统了,肯定是系统在底层做了一些操作。请接着往下看

NSObject中alloc 走到 objc_alloc 的 why?

这部分需要通过 LLVM源码(即llvm-project) 来分析

准备工作:首先需要一份llvm源码

在llvm源码中搜索objc_alloc


搜索shouldUseRuntimeFunctionForCombinedAllocInit,表示版本控制


搜索tryEmitSpecializedAllocInit,非常著名的特殊消息发送,在这里也没有找到 objc_alloc


继续尝试,开启上帝视角,通过alloc字符串搜索,如果还找不到,还可以通过omf_alloc:找到tryGenerateSpecializedMessageSend,表示尝试生成特殊消息发送


然后在这个case中可以找到调用alloc,转而调用了objc_objc的逻辑,其中的关键代码是EmitObjCAlloc


跳转至EmitObjCAlloc的定义可以看到alloc 的处理是调用了 objc_alloc


由此可以得出 NSObject中的alloc 会走到 objc_alloc,其实这部分是由系统级别的消息处理逻辑,所以NSObject的初始化是由系统完成的,因此也不会走到alloc的源码工程中

总结

总结下NSObject中alloc 和自定义类中alloc的调用流程

NSObject


自定义类


作者:style_月月
链接:https://blog.csdn.net/lin1109221208/article/details/108480971

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

iOS 底层原理03:objc4-781 源码编译 & 调试

iOS
准备工作环境版本 & 最新objc源码mac OS 10.15Xcode 11.4objc4-781依赖文件下载需要下载以下依赖文件源码编译源码编译就是不断的调试修改源码的问题,主要有以下问题问题一:unable to find sdk 'macosx...
继续阅读 »

准备工作

环境版本 & 最新objc源码

  • mac OS 10.15
  • Xcode 11.4
  • objc4-781

依赖文件下载

需要下载以下依赖文件


源码编译

源码编译就是不断的调试修改源码的问题,主要有以下问题

问题一:unable to find sdk 'macosx.internal'


选择 target -> objc -> Build Settings -> Base SDK -> 选择 macOS 【target中的 objc 和 obc-trampolines都需要更改】


问题二:文件找不到的报错问题

【1】‘sys/reason.h’ file not found


在Apple source的 macOS10.15 --> xnu-6153.11.26/bsd/sys/reason.h 路径自行下载

在objc4-781的根目录下新建CJLCommon文件, 同时在CJLCommon文件中创建sys文件

最后将 reason.h文件拷贝到sys文件中

设置文件检索路径:选择 target -> objc -> Build Settings,在工程的 Header Serach Paths 中添加搜索路径 $(SRCROOT)/CJLCommon

【2】‘mach-o/dyld_priv.h’ file not found

  • CJLCommon文件中 创建 mach-o 文件
  • 找到文件:dyld-733.6 -- include -- mach-o -- dyld_priv.h


拷贝到 mach-o文件中



  • 拷贝到文件后,还需要修改 dyld_priv.h 文件,即在 dyld_priv.h文件顶部加入一下宏:


【3】‘os/lock_private.h’ file not found 和 ‘os/base_private.h’ file not found

  • 在CJLCommon中创建 os文件
  • 找到lock_private.h、base_private.h文件:libplatform-220 --> private --> os --> lock_private.h 、base_private.h,并将文件拷贝至 os 文件中

【4】‘pthread/tsd_private.h’ file not found 和 ‘pthread/spinlock_private.h’ file not found

在CJLPerson中创建 pthread 文件
找到tsd_private.h、spinlock_private.h文件,h文件路径为:libpthread-416.11.1 --> private --> tsd_private.h、spinlock_private.h,并拷贝到 pthread文件


【5】‘System/machine/cpu_capabilities.h’ file not found

创建 System -- machine 文件
找到 cpu_capabilities.h文件拷贝到 machine文件,h文件路径为:xnu6153.11.26 --> osfmk --> machine --> cpu_capabilities.h


【6】os/tsd.h’ file not found

找到 tsd.h文件,拷贝到os文件, h文件路径为:xnu6153.11.26 --> libsyscall --> os --> tsd.h


【7】‘System/pthread_machdep.h’ file not found

  • 这个地址下载pthread_machdep.h文件,h文件路径为:Libc-583/pthreads/pthread_machdep.h
  • 将其拷贝至system文件中


【8】‘CrashReporterClient.h’ file not found

导入下载的还是报错,可以通过以下方式解决
- 需要在 Build Settings -> Preprocessor Macros 中加入:LIBC_NO_LIBCRASHREPORTERCLIENT
- 或者下载我给大家的文件CrashReporterClient,这里面我们直接更改了里面的宏信息 #define LIBC_NO_LIBCRASHREPORTERCLIENT

【9】‘objc-shared-cache.h’ file not found

文件路径为:dyld-733.6 --> include --> objc-shared-cache.h


  • 将h文件报备制拷贝到CJLCommon

【10】Mismatch in debug-ness macros

注释掉objc-runtime.mm中的#error mismatch in debug-ness macros


【11】’_simple.h’ file not found

文件路径为:libplatform-220 --> private --> _simple.h


  • 将文件拷贝至CJLCommon
【12】‘kern/restartable.h’ file not found

  • 在CJLCommon中创建kern 文件
  • 找到 h文件,路径为xnu-6153.11.26 --> osfmk --> kern -->restartable.h


【13】‘Block_private.h’ file not found

找到 h 文件,文件路径为libclosure-74 --> Block_private.h



拷贝至CJLCommon目录

【14】libobjc.order 路径问题

问题描述为:can't open order file: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/AppleInternal/OrderFiles/libobjc.order

  • 选择 target -> objc -> Build Settings
  • 在工程的 Order File 中添加搜索路径 $(SRCROOT)/libobjc.order



【14】Xcode 脚本编译问题
问题描述为:/xcodebuild:1:1: SDK "macosx.internal" cannot be located.

选择 target -> objc -> Build Phases -> Run Script(markgc)
把脚本文本 macosx.internal 改成 macosx


编译调试

新建一个target :CJLTest



绑定二进制依赖关系



源码调试

自定义一个CJLPerson类

image

在main.m中 创建 CJLPerson的对象,进行源码调试



作者:style_月月
链接:https://blog.csdn.net/lin1109221208/article/details/108435967

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

现今 Swift 包中的二进制目标

一、目录      1、理解二进制在 Swift 中的演变    2、命令行工具相关    3、结论二、前言    在 iOS 和...
继续阅读 »

一、目录  

    1、理解二进制在 Swift 中的演变
    2、命令行工具相关
    3、结论

二、前言

    在 iOS 和 macOS 开发中, Swift 包现在变得越来越重要。Apple 已经努力推动桥接那些缝隙,并且修复那些阻碍开发者的问题,例如阻碍开发者将他们的库和依赖由其他诸如 Carthage 或 CocoaPods依赖管理工具迁移到 Swift 包依赖管理工具的问题,例如没有能力添加构建步骤的问题。这对任何依赖一些代码生成的库来说都是破坏者,比如,协议和 Swift 生成。


    1、理解二进制在 Swift 中的演变

        为了充分理解 Apple 的 Swift 团队在二进制目标和他们引入的一些新 API 方面采取的一些步骤,我们需要理解它们从何而来。在后续的部分中,我们将调研 Apple 架构的演变,以及为什么二进制目标的 API 在过去几年中逐渐形成的,特别是自 Apple 发布了自己的硅芯片之后。


        胖二进制和 Frameworks 框架

        如果你曾必须处理二进制依赖,或者你曾创建一个属于你自己的可执行文件,你将会对 胖二进制 这个术语感到熟悉。这些被扩展(或增大)的可执行文件,是包含了为多个不同架构原生构建的切片。这允许库的所有者分发一个运行在所有预期的目标架构上的单独的二进制。
        当源码不能被暴露或当处理非常庞大的代码仓库时,预编译库成为可执行文件非常有意义,因为预编译源码以及以二进制文件分发他们,将节省构建程序在他们的应用上的构建时间。
        Pods 是一个非常好的例子,当开发者发现他们自己没必要构建那些非常少改动的依赖。这是一个很共通的问题,它激发了诸如 cocoapods-binary之类的项目,该项目预编译了 pod 依赖项以减少客户端的构建时间。


        Frameworks 框架

        嵌入静态二进制文件可能对应用程序来说已经足够了,但如果需要某些资源(如 assets 或头文件),则需要将这些资源与包含所有切片的 胖二进制文件 捆绑在一起,形成所谓的 frameworks 文件。
这就是诸如 Google Cast[5] 之类的预编译库在过渡到使用 xcframework 进行分发之前所做的事情 —— 下一节将详细介绍这种过渡的原因。
        到目前为止,一切都很好。如果我们要为分发预编译一个库,那么胖二进制文件听起来很理想,对吧?并且,如果我们需要捆绑一些其他资源,我们可以只使用一个 frameworks。一个二进制来统治他们所有!


        XCFrameworks 框架

        好吧,不完全是。胖二进制文件有一个大问题,那就是你不能有两个架构相同但命令/指令不同的切片。这曾经很好,因为设备和模拟器的架构总是不同的,但是随着 Apple Silicon 计算机 (M1) 的推出,模拟器和设备共享相同的架构 (arm64),但具有不同的加载器命令。这与面向未来的二进制目标相结合,正是 Apple 引入 XCFrameworks 的原因。
        XCFrameworks现在允许将多个二进制文件捆绑在一起,解决了 M1 Mac 引入的设备和模拟器冲突架构问题,因为我们现在可以为每个用例提供包含相关切片的二进制文件。事实上,如果我们需要,我们可以走得更远,例如,在同一个 xcframework 中捆绑一个包含 iOS 目标的 UIKit 接口的二进制文件和一个包含 macOS 的 AppKit 接口的二进制文件,然后让 Xcode 基于期望的目标架构决定使用哪一个。
        在 Swift 包中,那先能够以 binaryTarget 被包含进项目的,能够在包中被引入任意其他目标。这相同的操作同样适用于 frameworks。


     2、命令行工具相关

        由于 Swift 5.6 版本中引入了用于 Swift 包管理器的 可扩展构建工具[9] ,因此可以在构建过程中的不同时间执行命令。

        这是 iOS 社区长期以来一直强烈要求的事情,例如格式化源代码、代码生成甚至收集公制代码库的指标。Swift 5.6 中所有这些所谓的 插件最终都需要调用可执行文件来执行特定任务。这是二进制文件再次在 Swift 包中参与的地方。
        在大多数情况下,对于我们 iOS 开发人员来说,这些工具将来自同时支持 macOS 的不同架构切片 —— Apple Silicon 的 arm64 架构和 Intel Mac 的 x86_64 架构。开发者工具如, SwiftLint或 SwiftGen 正是这种案例。在这种情况下,可以使用包含可执行文件(本地或远程)的 .zip 文件的路径创建新的二进制目标。


        Artifact Bundles

        到目前为止,命令行工具所采用的方法仅适用于 macOS 架构。但我们不能忘记,Linux 机器也支持 Swift 包。这意味着如果要同时支持 M1 macs (arm64) 和 Linux arm64 机器,上面的胖二进制方法将不起作用 —— 请记住,二进制不能包含具有相同架构的多个切片。在这个阶段可能有人会想,我们可以不只使用 xcframeworks 吗?不,因为它们在 Linux 操作系统上不受支持!
        Apple 已经考虑到这一点,除了引入 可扩展构建工具[13] 之外,Artifact Bundles和对二进制目标的其他改进也作为 Swift 5.6 的一部分发布。
        工件包(Artifact Bundles) 是包含 工件 的目录。这些工件需要包含支持架构的所有不同二进制文件。二进制文件和支持的架构的路径是使用清单文件 (info.json) 指定的,该文件位于 Artifact Bundle 目录的根目录中。你可以将此清单文件视为一个地图或指南,以帮助 Swift 确定哪些可执行文件可用于哪种架构以及可以在哪里找到它们。


        以 SwiftLint 为例

        SwiftLint 在整个社区中被广泛用作 Swift 代码的静态代码分析工具。由于很多人都非常渴望让这个插件在他们的 SwiftPM 项目中运行,我认为这将是一个很好的例子来展示我们如何将分发的可执行文件从他们的发布页面变成一个与 macOS 架构和 Linux arm64 兼容的工件包。
        让我们从下载两个可执行文件(macOS 和 Linux)开始。
        至此,bundle的结构就可以创建好了。为此,创建一个名为 swiftlint.artifactbundle 的目录并在其根目录添加一个空的 info.json:

        mkdir swiftlint.artifactbundle

        touch swiftlint.artifactbundle/info.json

        现在可以使用 schemaVersion 填充清单文件,这可能会在未来版本的工件包和具有两个变体的工件中发生变化,这将很快定义:

        {

            "schemaVersion": "1.0",
            "artifacts": {
                "swiftlint": {
                    "version": "0.47.0", # The version of SwiftLint being used
                    "type": "executable",
                    "variants": [
                    ]
                },
            }
        }

        需要做的最后一件事是将二进制文件添加到包中,然后将它们作为变体添加到 info.json 文件中。让我们首先创建目录并将二进制文件放入其中(macOS 的一个在 swiftlint-macos/swiftlint,Linux 的一个在 swiftlint-linux/swiftlint)。
        添加这些之后,可以在清单文件中变量:

        {
            "schemaVersion": "1.0",
            "artifacts": {
                "swiftlint": {
                    "version": "0.47.0", # The version of SwiftLint being used
                    "type": "executable",
                    "variants": [
                    {
                        "path": "swiftlint-macos/swiftlint",
                        "supportedTriples": ["x86_64-apple-macosx", "arm64-apple-macosx"]
                    },
                    {
                        "path": "swiftlint-linux/swiftlint",
                        "supportedTriples": ["x86_64-unknown-linux-gnu"]
                    },
                    ]
                },
            }
        }

        为此,需要为每个变量指定二进制文件的相对路径(从工件包目录的根目录)和支持的三元组。如果您不熟悉 目标三元组,它们是一种选择构建二进制文件的架构的方法。请注意,这不是 主机(构建可执行文件的机器)的体系结构,而是 目标 机器(应该运行所述可执行文件的机器)。

        这些三元组具有以下格式: ---- 并非所有字段都是必需的,如果其中一个字段未知并且要使用默认值,则可以省略或替换为 unknown 关键字。
        可执行文件的架构切片可以通过运行 file 找到,这将打印捆绑的任何切片的供应商、系统和架构。在这种情况下,为这两个命令运行它会显示:


        swiftlint-macos/swiftlint


        swiftlint: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64]

        swiftlint (for architecture x86_64): Mach-O 64-bit executable x86_64

        swiftlint (for architecture arm64): Mach-O 64-bit executable arm64


        swiftlint-linux/swiftlint


        -> file swiftlint
        swiftlint: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped


       这带来了上面显示的 macOS 支持的两个三元组(x86_64-apple-macosx、arm64-apple-macosx)和 Linux 支持的一个三元组(x86_64-unknown-linux-gnu)。

        与 XCFrameworks 类似,工件包也可以通过使用 binaryTarget 包含在 Swift 包中。


    3、结论

        简而言之,我们可以总结 2022 年如何在 Swift 包中使用二进制文件的最佳实践,如下所示:

        1、如果你需要为你的 iOS/macOS 项目添加预编译库或可执行文件,您应该使用 XCFramework,并为每个用例(iOS 设备、macOS 设备和 iOS 模拟器)包含单独的二进制文件。
        2、如果你需要创建一个插件并运行一个可执行文件,你应该将其嵌入为一个工件包,其中包含适用于不同支持架构的二进制文件。

收起阅读 »

AFNetworking源码探究 —— UIKit相关之UIProgressView+AFNetworking分类

iOS
下面我们先看一下接口的API/** This category adds methods to the UIKit framework's `UIProgressView` class. The methods in this category provide...
继续阅读 »

接口API

下面我们先看一下接口的API

/**
This category adds methods to the UIKit framework's `UIProgressView` class. The methods in this category provide support for binding the progress to the upload and download progress of a session task.
*/
@interface UIProgressView (AFNetworking)

///------------------------------------
/// @name Setting Session Task Progress
///------------------------------------

/**
Binds the progress to the upload progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated;

/**
Binds the progress to the download progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated;

@end


该类为UIKit框架的UIProgressView类添加方法。 此类别中的方法为将进度绑定到会话任务的上载和下载进度提供了支持。

该接口比较少,其实就是一个上传任务和一个下载任务分别和进度的绑定,可动画。

这里大家还要注意一个关于类的继承的细节。

// 上传
@interface NSURLSessionUploadTask : NSURLSessionDataTask
@interface NSURLSessionDataTask : NSURLSessionTask

// 下载
@interface NSURLSessionDownloadTask : NSURLSessionTask

给大家贴出来就是想让大家注意下这个结构。

runtime获取是否可动画

这里还是用runtime分别绑定下载和上传是否可动画。

- (BOOL)af_uploadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_uploadProgressAnimated)) boolValue];
}

- (void)af_setUploadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_uploadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)af_downloadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_downloadProgressAnimated)) boolValue];
}

- (void)af_setDownloadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_downloadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

这个还算是很好理解的,有了前面的基础,这里就不多说了。

接口的实现

下面我们就看一下接口的实现。

1. 上传任务

static void * AFTaskCountOfBytesSentContext = &AFTaskCountOfBytesSentContext;
static void * AFTaskCountOfBytesReceivedContext = &AFTaskCountOfBytesReceivedContext;
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];
[task addObserver:self forKeyPath:@"countOfBytesSent" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];

[self af_setUploadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesSent,最后就是设置是否可动画的状态。

2. 下载任务

- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];
[task addObserver:self forKeyPath:@"countOfBytesReceived" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];

[self af_setDownloadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesReceived,最后就是设置是否可动画的状态。


KVO观察实现

下面看一下KVO观察的实现,这里也是这个类的精髓所在。

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(__unused NSDictionary *)change
context:(void *)context
{
if (context == AFTaskCountOfBytesSentContext || context == AFTaskCountOfBytesReceivedContext) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) {
if ([object countOfBytesExpectedToSend] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) {
if ([object countOfBytesExpectedToReceive] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self.af_downloadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(state))]) {
if ([(NSURLSessionTask *)object state] == NSURLSessionTaskStateCompleted) {
@try {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(state))];

if (context == AFTaskCountOfBytesSentContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesSent))];
}

if (context == AFTaskCountOfBytesReceivedContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))];
}
}
@catch (NSException * __unused exception) {}
}
}
}
}

这里还是很简单的吧。

  • 如果keyPath是@"countOfBytesSent",那么就获取countOfBytesExpectedToSend,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];得到进度。
  • 如果keyPath是@"countOfBytesReceived",那么就获取countOfBytesExpectedToReceive,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self. af_downloadProgressAnimated];得到进度。
  • 如果keyPath是@"state"并且任务是完成状态NSURLSessionTaskStateCompleted,那么就要移除对这几个keyPath的观察者。

后记

本篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。

转载自:https://cloud.tencent.com/developer/article/1872398





收起阅读 »

AFNetworking源码探究(二十五) —— UIKit相关之UIRefreshControl+AFNetworking分类

iOS
上一篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。这一篇主要分析UIRefreshControl+AFNetworking这个分类。接口API下面我们先看一下接口的API/** This ...
继续阅读 »

回顾

上一篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。这一篇主要分析UIRefreshControl+AFNetworking这个分类。


接口API

下面我们先看一下接口的API

/**
This category adds methods to the UIKit framework's `UIProgressView` class. The methods in this category provide support for binding the progress to the upload and download progress of a session task.
*/
@interface UIProgressView (AFNetworking)

///------------------------------------
/// @name Setting Session Task Progress
///------------------------------------

/**
Binds the progress to the upload progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated;

/**
Binds the progress to the download progress of the specified session task.

@param task The session task.
@param animated `YES` if the change should be animated, `NO` if the change should happen immediately.
*/
- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated;

@end

该类为UIKit框架的UIProgressView类添加方法。 此类别中的方法为将进度绑定到会话任务的上载和下载进度提供了支持。

该接口比较少,其实就是一个上传任务和一个下载任务分别和进度的绑定,可动画。

这里大家还要注意一个关于类的继承的细节。

// 上传
@interface NSURLSessionUploadTask : NSURLSessionDataTask
@interface NSURLSessionDataTask : NSURLSessionTask

// 下载
@interface NSURLSessionDownloadTask : NSURLSessionTask

给大家贴出来就是想让大家注意下这个结构。


runtime获取是否可动画

这里还是用runtime分别绑定下载和上传是否可动画。

- (BOOL)af_uploadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_uploadProgressAnimated)) boolValue];
}

- (void)af_setUploadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_uploadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)af_downloadProgressAnimated {
return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_downloadProgressAnimated)) boolValue];
}

- (void)af_setDownloadProgressAnimated:(BOOL)animated {
objc_setAssociatedObject(self, @selector(af_downloadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

这个还算是很好理解的,有了前面的基础,这里就不多说了。


接口的实现

下面我们就看一下接口的实现。

1. 上传任务

static void * AFTaskCountOfBytesSentContext = &AFTaskCountOfBytesSentContext;
static void * AFTaskCountOfBytesReceivedContext = &AFTaskCountOfBytesReceivedContext;
- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];
[task addObserver:self forKeyPath:@"countOfBytesSent" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext];

[self af_setUploadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesSent,最后就是设置是否可动画的状态。

2. 下载任务

- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task
animated:(BOOL)animated
{
if (task.state == NSURLSessionTaskStateCompleted) {
return;
}

[task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];
[task addObserver:self forKeyPath:@"countOfBytesReceived" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext];

[self af_setDownloadProgressAnimated:animated];
}

这里逻辑很清晰,简单的说一下,如果任务是完成状态,那么就直接return,然后给task添加KVO观察,观察属性是state和countOfBytesReceived,最后就是设置是否可动画的状态。


KVO观察实现

下面看一下KVO观察的实现,这里也是这个类的精髓所在。

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(__unused NSDictionary *)change
context:(void *)context
{
if (context == AFTaskCountOfBytesSentContext || context == AFTaskCountOfBytesReceivedContext) {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) {
if ([object countOfBytesExpectedToSend] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) {
if ([object countOfBytesExpectedToReceive] > 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self.af_downloadProgressAnimated];
});
}
}

if ([keyPath isEqualToString:NSStringFromSelector(@selector(state))]) {
if ([(NSURLSessionTask *)object state] == NSURLSessionTaskStateCompleted) {
@try {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(state))];

if (context == AFTaskCountOfBytesSentContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesSent))];
}

if (context == AFTaskCountOfBytesReceivedContext) {
[object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))];
}
}
@catch (NSException * __unused exception) {}
}
}
}
}

这里还是很简单的吧。

  • 如果keyPath是@"countOfBytesSent",那么就获取countOfBytesExpectedToSend,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated];得到进度。
  • 如果keyPath是@"countOfBytesReceived",那么就获取countOfBytesExpectedToReceive,计算进度百分比,在主线程调用[self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self. af_downloadProgressAnimated];得到进度。
  • 如果keyPath是@"state"并且任务是完成状态NSURLSessionTaskStateCompleted,那么就要移除对这几个keyPath的观察者。

后记

本篇主要分析了UIProgressView+AFNetworking分类,主要实现了上传任务和下载任务与进度之间的绑定。

本文转载自腾讯社区:作者conanma  https://cloud.tencent.com/developer/article/1872401



收起阅读 »

AFNetworking源码探究 —— UIKit相关之AFAutoPurgingImageCache缓存

iOS
回顾上一篇主要讲述了UIRefreshControl+AFNetworking这个分类,将刷新状态和任务状态进行了绑定和同步。这一篇主要讲述AFAutoPurgingImageCache有关的缓存。接口API按照老惯例,我们还是先看一下接口API文档。这个接口...
继续阅读 »

回顾

上一篇主要讲述了UIRefreshControl+AFNetworking这个分类,将刷新状态和任务状态进行了绑定和同步。这一篇主要讲述AFAutoPurgingImageCache有关的缓存。


接口API

按照老惯例,我们还是先看一下接口API文档。这个接口文档包括三个部分,两个协议一个类。

  • 协议AFImageCache
  • 协议AFImageRequestCache
  • AFAutoPurgingImageCache

1. AFImageCache

这个协议包括四个方法

/**
Adds the image to the cache with the given identifier.

@param image The image to cache.
@param identifier The unique identifier for the image in the cache.
*/
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier;

/**
Removes the image from the cache matching the given identifier.

@param identifier The unique identifier for the image in the cache.

@return A BOOL indicating whether or not the image was removed from the cache.
*/
- (BOOL)removeImageWithIdentifier:(NSString *)identifier;

/**
Removes all images from the cache.

@return A BOOL indicating whether or not all images were removed from the cache.
*/
- (BOOL)removeAllImages;

/**
Returns the image in the cache associated with the given identifier.

@param identifier The unique identifier for the image in the cache.

@return An image for the matching identifier, or nil.
*/
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier;

该协议定义了包括加入、移除、获取缓存中的图片。

2. AFImageRequestCache

该协议包含下面几个方法,这里注意这个协议继承自协议AFImageCache

@protocol AFImageRequestCache <AFImageCache>

/**
Asks if the image should be cached using an identifier created from the request and additional identifier.

@param image The image to be cached.
@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.

@return A BOOL indicating whether or not the image should be added to the cache. YES will cache, NO will prevent caching.
*/
- (BOOL)shouldCacheImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

/**
Adds the image to the cache using an identifier created from the request and additional identifier.

@param image The image to cache.
@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.
*/
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

/**
Removes the image from the cache using an identifier created from the request and additional identifier.

@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.

@return A BOOL indicating whether or not all images were removed from the cache.
*/
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

/**
Returns the image from the cache associated with an identifier created from the request and additional identifier.

@param request The unique URL request identifing the image asset.
@param identifier The additional identifier to apply to the URL request to identify the image.

@return An image for the matching request and identifier, or nil.
*/
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;

@end

根据请求和标识对图像进行是否需要缓存、加入到缓存或者移除缓存等进行操作。

3. AFAutoPurgingImageCache

这个是这个类的接口,大家注意下这个类遵循协议AFImageRequestCache

/**
The `AutoPurgingImageCache` in an in-memory image cache used to store images up to a given memory capacity. When the memory capacity is reached, the image cache is sorted by last access date, then the oldest image is continuously purged until the preferred memory usage after purge is met. Each time an image is accessed through the cache, the internal access date of the image is updated.
*/
@interface AFAutoPurgingImageCache : NSObject <AFImageRequestCache>

/**
The total memory capacity of the cache in bytes.
*/
// 内存缓存总的字节数
@property (nonatomic, assign) UInt64 memoryCapacity;

/**
The preferred memory usage after purge in bytes. During a purge, images will be purged until the memory capacity drops below this limit.
*/
// 以字节为单位清除后的首选内存使用情况。 在清除过程中,图像将被清除,直到内存容量降至此限制以下。
@property (nonatomic, assign) UInt64 preferredMemoryUsageAfterPurge;

/**
The current total memory usage in bytes of all images stored within the cache.
*/
// 当前所有图像内存缓存使用的总的字节数
@property (nonatomic, assign, readonly) UInt64 memoryUsage;

/**
Initialies the `AutoPurgingImageCache` instance with default values for memory capacity and preferred memory usage after purge limit. `memoryCapcity` defaults to `100 MB`. `preferredMemoryUsageAfterPurge` defaults to `60 MB`.
// 初始化,memoryCapcity为100M,preferredMemoryUsageAfterPurge为60M

@return The new `AutoPurgingImageCache` instance.
*/
- (instancetype)init;

/**
Initialies the `AutoPurgingImageCache` instance with the given memory capacity and preferred memory usage
after purge limit.

@param memoryCapacity The total memory capacity of the cache in bytes.
@param preferredMemoryCapacity The preferred memory usage after purge in bytes.

@return The new `AutoPurgingImageCache` instance.
*/
- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity;

@end

内存中图像缓存中的AutoPurgingImageCache用于存储图像到给定内存容量。 达到内存容量时,图像缓存按上次访问日期排序,然后最旧的图像不断清除,直到满足清除后的首选内存使用量。 每次通过缓存访问图像时,图像的内部访问日期都会更新。


AFAutoPurgingImageCache接口及初始化

从接口描述中我们可以看出来,类的初始化规定了内存总的使用量以及清楚以后的内存最优大小。

- (instancetype)init {
return [self initWithMemoryCapacity:100 * 1024 * 1024 preferredMemoryCapacity:60 * 1024 * 1024];
}

- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity {
if (self = [super init]) {
self.memoryCapacity = memoryCapacity;
self.preferredMemoryUsageAfterPurge = preferredMemoryCapacity;
self.cachedImages = [[NSMutableDictionary alloc] init];

NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(removeAllImages)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];

}
return self;
}

我们看一下这个初始化方法中都做了什么事情

设置置缓存图像的字典

self.cachedImages = [[NSMutableDictionary alloc] init];

常见和UUID关联的并发队列

NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]];
self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT);

增加移除所有图像的通知

[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(removeAllImages)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
- (BOOL)removeAllImages {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
if (self.cachedImages.count > 0) {
[self.cachedImages removeAllObjects];
self.currentMemoryUsage = 0;
removed = YES;
}
});
return removed;
}

这里就是在上面生成的队列中,清空数组,重置一些属性值。


AFCachedImage接口及初始化

这里我们就看一下AFCachedImage的接口及初始化。

@interface AFCachedImage : NSObject

@property (nonatomic, strong) UIImage *image;
@property (nonatomic, strong) NSString *identifier;
@property (nonatomic, assign) UInt64 totalBytes;
@property (nonatomic, strong) NSDate *lastAccessDate;
@property (nonatomic, assign) UInt64 currentMemoryUsage;

@end

- (instancetype)initWithImage:(UIImage *)image identifier:(NSString *)identifier {
if (self = [self init]) {
self.image = image;
self.identifier = identifier;

CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale);
CGFloat bytesPerPixel = 4.0;
CGFloat bytesPerSize = imageSize.width * imageSize.height;
self.totalBytes = (UInt64)bytesPerPixel * (UInt64)bytesPerSize;
self.lastAccessDate = [NSDate date];
}
return self;
}

这个初始化方法里面初始化图像的字节数,并更新上次获取数据的时间。


协议方法的实现

1. AFImageCache协议的实现

将图像根据标识添加到内存

- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier;
- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier {
dispatch_barrier_async(self.synchronizationQueue, ^{
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];

AFCachedImage *previousCachedImage = self.cachedImages[identifier];
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}

self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});

dispatch_barrier_async(self.synchronizationQueue, ^{
if (self.currentMemoryUsage > self.memoryCapacity) {
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
[sortedImages sortUsingDescriptors:@[sortDescriptor]];

UInt64 bytesPurged = 0;

for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
self.currentMemoryUsage -= bytesPurged;
}
});
}

这里用到了两个阻塞

第一个阻塞

dispatch_barrier_async(self.synchronizationQueue, ^{
AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier];

AFCachedImage *previousCachedImage = self.cachedImages[identifier];
if (previousCachedImage != nil) {
self.currentMemoryUsage -= previousCachedImage.totalBytes;
}

self.cachedImages[identifier] = cacheImage;
self.currentMemoryUsage += cacheImage.totalBytes;
});

这里的作用其实很清楚,就是先根据image和identify实例化AFCachedImage对象。然后在字典中根据identifier查看是否有AFCachedImage对象,如果有的话,那么就减小当前使用内存的值。并将前面实例化的AFCachedImage对象存入字典中,并增加当前使用内存的值。

第二个阻塞

dispatch_barrier_async(self.synchronizationQueue, ^{
if (self.currentMemoryUsage > self.memoryCapacity) {
UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge;
NSMutableArray <AFCachedImage*> *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate"
ascending:YES];
[sortedImages sortUsingDescriptors:@[sortDescriptor]];

UInt64 bytesPurged = 0;

for (AFCachedImage *cachedImage in sortedImages) {
[self.cachedImages removeObjectForKey:cachedImage.identifier];
bytesPurged += cachedImage.totalBytes;
if (bytesPurged >= bytesToPurge) {
break ;
}
}
self.currentMemoryUsage -= bytesPurged;
}
});

这里完成的功能是,首先判断如果当前内存使用量大于内存总量,那么就需要清理了,这里需要计算需要清理多少内存,就是当前内存值 - 最优内存值。然后sortedImages实例化字典中所有的图片,并对这些图片进行按照时间的排序,遍历这个排序后的数组,逐一从字典中移除,终止条件就是移除的字节数大于上面计算的要清除的字节数值。最后,更新下当前内存使用的值。

根据指定标识将图像移出内存

- (BOOL)removeImageWithIdentifier:(NSString *)identifier;
- (BOOL)removeImageWithIdentifier:(NSString *)identifier {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
if (cachedImage != nil) {
[self.cachedImages removeObjectForKey:identifier];
self.currentMemoryUsage -= cachedImage.totalBytes;
removed = YES;
}
});
return removed;
}

这个还是很好理解的,在定义的并行队列中,取出identifier对应的AFCachedImage对象,然后从字典中移除,并更新当前内存的值。

从内存中移除所有的图像

- (BOOL)removeAllImages;
- (BOOL)removeAllImages {
__block BOOL removed = NO;
dispatch_barrier_sync(self.synchronizationQueue, ^{
if (self.cachedImages.count > 0) {
[self.cachedImages removeAllObjects];
self.currentMemoryUsage = 0;
removed = YES;
}
});
return removed;
}

其实就是一句话,清空字典,更新当前内存使用值。

根据指定的标识符从内存中获取图像

- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier;
- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier {
__block UIImage *image = nil;
dispatch_sync(self.synchronizationQueue, ^{
AFCachedImage *cachedImage = self.cachedImages[identifier];
image = [cachedImage accessImage];
});
return image;
}
- (UIImage*)accessImage {
self.lastAccessDate = [NSDate date];
return self.image;
}

其实就是从字典中取值,并更新上次获取图像的时间。

2. AFImageRequestCache协议的实现

根据请求和标识符将图像加入到内存

- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
[self addImage:image withIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}
- (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier {
NSString *key = request.URL.absoluteString;
if (additionalIdentifier != nil) {
key = [key stringByAppendingString:additionalIdentifier];
}
return key;
}

这里其实是调用上面我们讲过的那个根据identifier取出AFCachedImage对象的那个方法。不过下面这个identifier是通过调用下面这个方法生成的。、

根据请求和标识符将图像移出内存

- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
return [self removeImageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}

这个,就是还是利用那个生成indentifier的方法,获取identify,然后调用前面我们讲过的方法移除对应的图像。

根据请求和标识符获取内存中图像

- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier {
return [self imageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]];
}

这个,就是还是利用那个生成indentifier的方法,获取identify,然后调用前面我们讲过的方法获取对应的图像。

是否将图像缓存到内存

- (BOOL)shouldCacheImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier;
- (BOOL)shouldCacheImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier {
return YES;
}

这里就是写死的,默认就是需要进行缓存。

后记

本篇主要讲述了关于图像缓存方面的内容,包括使用标识符或者请求进行图像相关的缓存操作

本文转载自:https://cloud.tencent.com/developer/article/1872403

收起阅读 »

React Native ART

Android自带ART,不用导入。iOS要使用需要使用xcode打开react native 的ios目录,

1、使用xcode中打开react-native中的ios项目,选中‘Libraries’目录 ——> 右键选择‘Add Files to 项目名称’ ——> 'node_modules/react-native/Libraries/ART/ART.xcodeproj' 添加;


2、选中项目根目录 ——> 点击’Build Phases‘ ——> 点击‘Link Binary With Libraries’ ——> 点击左下方‘+’ ——> 选中‘libART.a’添加。


感谢奋斗的orange 提供,转载原文http://blog.csdn.net/u010940770/article/details/71126700

如果要使用svg作为渲染,使用react-native-art-svg

以下是个人记录:

1. svg的设计要使用局中描边;

2. 画扇形

import React from 'react'

import {

View,

ART

} from 'react-native'

const {Surface} = ART;

import Wedge from './Wedge'

export default class Fan extends React.Component{

render(){

return(

outerRadius={50}

startAngle={0}

endAngle={60}

originX={50}

originY={50}

fill="blue"/>

)

}

}



iOS block与__block、weak、__weak、__strong

iOS

首先需要知道:

block,本质是OC对象,对象的内容,是代码块。
封装了函数调用以及函数调用环境。

block也有自己的isa指针,依据block的类别不同,分别指向
__NSGlobalBlock __ ( _NSConcreteGlobalBlock )
__NSStackBlock __ ( _NSConcreteStackBlock )
__NSMallocBlock __ ( _NSConcreteMallocBlock )
需要注意是,ARC下只存在__NSGlobalBlock和__NSMallocBlock。
通常作为参数时,才可能是栈区block,但是由于ARC的copy作用,会将栈区block拷贝到堆上。
通常不管作为属性、参数、局部变量的block,都是__NSGlobalBlock,即使block内部出现了常量、静态变量、全局变量,也是__NSGlobalBlock,
除非block内部出现其他变量,auto变量或者对象属性变量等,就是__NSMallocBlock

为什么block要被拷贝到堆区,变成__NSMallocBlock,可以看如下链接解释:Ios开发-block为什么要用copy修饰

对于基础数据类型,是值传递,修改变量的值,修改的是a所指向的内存空间的值,不会改变a指向的地址。

对于指针(对象)数据类型,修改变量的值,是修改指针变量所指向的对象内存空间的地址,不会改变指针变量本身的地址
简单来说,基础数据类型,只需要考虑值的地址,而指针类型,则需要考虑有指针变量的地址和指针变量指向的对象的地址

以变量a为例

1、基础数据类型,都是指值的地址

1.1无__block修饰,

a=12,地址为A
block内部,a地址变B,不能修改a的值
block外部,a的地址依旧是A,可以修改a的值,与block内部的a互不影响
内外a的地址不一致

1.2有__block修饰

a=12,地址为A
block内部,地址变为B,可以修改a的值,修改后a的地址依旧是B
block外部,地址保持为B,可以修改a的值,修改后a的地址依旧是B

2、指针数据类型

2.1无__block修饰

a=[NSObject new],a指针变量的地址为A,指向的对象地址为B
block内部,a指针变量的地址为C,指向的对象地址为B,不能修改a指向的对象地址
block外部,a指针变量的地址为A,指向的对象地址为B,可以修改a指向的对象地址,
block外部修改后,
外部a指针变量的地址依旧是A,指向的对象地址变为D
内部a指针变量的地址依旧是C,指向的对象地址依旧是B

2.1有__block修饰

a=[NSObject new],a指针变量的地址为A,指向的对象地址为B
block内部,a指针变量的地址为C,指向的对象地址为B,能修改a指向的对象地址
block外部,a指针变量的地址为C,指向的对象地址为B,能修改a指向的对象地址
block内外,或者另一个block中,无论哪里修改,a指针变量地址都保持为C,指向的对象地址保持为修改后的一致

block内修改变量的实质(有__block修饰):

block内部能够修改的值,必须都是存放在堆区的。
1、基础数据类型,__block修饰后,调用block时,会在堆区开辟新的值的存储空间,
指针数据类型,__block修饰后,调用block时,会在堆区开辟新的指针变量地址的存储空间

2、并且无论是基础数据类型还是指针类型,block内和使用block之后,变量的地址所有地址(包括基础数据类型的值的地址,指针类型的指针变量地址,指针指向的对象的地址),都是保持一致的
当然,只有block进行了真实的调用,才会在调用后发生这些地址的变化

另外需要注意的是,如果对一个已存在的对象(变量a),进行__block声明另一个变量b去指向它,
a的指针变量地址为A,b的指针变量会是B,而不是A,
原因很简单,不管有没__block修饰,不同变量名指向即使指向同一个对象,他们的指针变量地址都是不同的。

__weak,__strong

两者本身也都会增加引用计数。
区别在于,__strong声明,会在作用域区间范围增加引用计数1,超过其作用域然后引用计数-1
而__weak声明的变量,只会在其使用的时候(这里使用的时候,指的是一句代码里最终并行使用的次数),临时生成一个__strong引用,引用+次数,一旦使用使用完毕,马上-次数,而不是超出其作用域再-次数

    NSObject *obj = [NSObject new];
NSLog(@"声明时obj:%p, %@, 引用计数:%ld",&obj, obj, CFGetRetainCount((__bridge CFTypeRef)(obj)));
__weak NSObject *weakObj = obj;
NSLog(@"声明时weakObj:%p, %@,%@, %@, 引用计数:%ld",&weakObj, weakObj,weakObj,weakObj, CFGetRetainCount((__bridge CFTypeRef)(weakObj)));
NSLog(@"声明后weakObj引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

声明时obj:0x16daa3968, , 引用计数:1
声明时weakObj:0x16daa3960, ,, , 引用计数:5
声明后weakObj引用计数:2

这个5,是因为obj本来计数是1,

    NSLog(@"声明时weakObj:%p, %@,%@, %@, 引用计数:%ld",&weakObj, weakObj,weakObj,weakObj, CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

这句代码打印5,是因为除去&weakObj(&这个不是使用weakObj指向的对象,而只是取weakObj的指针变量地址,所以不会引起计数+1),另外还使用了4次weakObj,导致引用计数+4

   NSLog(@"声明后weakObj引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

这句打印2,说明上一句使用完毕后,weakObj引用增加的次数会马上清楚,重新变回1,而这句使用了一次weakObj,加上obj的一次引用,就是2了

__weak 与 weak

通常,__weak是单独为某个对象,添加一条弱引用变量的。
weak则是property属性里修饰符。

LGTestBlockObj *testObj = [LGTestBlockObj new];
self.prpertyObj = testObj;
__weak LGTestBlockObj *weakTestObj = testObj;
NSLog(@"testObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(testObj)));
NSLog(@"prpertyObj:%p, %@,%@, %@, 引用计数:%ld",&(_prpertyObj), self.prpertyObj,self.prpertyObj,self.prpertyObj, CFGetRetainCount((__bridge CFTypeRef)(self.prpertyObj)));
NSLog(@"prpertyObj:%p, %@,%@, %@, 引用计数:%ld",&(_prpertyObj), _prpertyObj,_prpertyObj,_prpertyObj, CFGetRetainCount((__bridge CFTypeRef)(_prpertyObj)));
NSLog(@"prpertyObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(_prpertyObj)));
NSLog(@"testObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(testObj)));
NSLog(@"weakTestObj:%p, %@,%@, %@, 引用计数:%ld",&weakTestObj, weakTestObj,weakTestObj,weakTestObj, CFGetRetainCount((__bridge CFTypeRef)(weakTestObj)));

prpertyObj:0x1088017b0, ,, , 引用计数:2
prpertyObj:, 引用计数:2
testObj:, 引用计数:2
weakTestObj:0x16b387958, ,, , 引用计数:6

待补充...

Block常见疑问收录

1、block循环引用

通常,block作为属性,并且block内部直接引用了self,就会出现循环引用,这时就需要__weak来打破循环。

2、__weak为什么能打破循环引用?

一个变量一旦被__weak声明后,这个变量本身就是一个弱引用,只有在使用的那行代码里,才会临时增加引用结束,一旦那句代码执行完毕,引用计数马上-1,所以看起来的效果是,不会增加引用计数,block中也就不会真正持有这个变量了

3、为什么有时候又需要使用__strong来修饰__weak声明的变量?

在block中使用__weak声明的变量,由于block没有对该变量的强引用,block执行的过程中,一旦对象被销毁,该变量就是nil了,会导致block无法继续正常向后执行。
使用__strong,会使得block作用区间,保存一份对该对象的强引用,引用计数+1,一旦block执行完毕,__strong变量就会销毁,引用计数-1
比如block中,代码执行分7步,在执行第二步时,weak变量销毁了,而第五步要用到weak变量。
而在block第一步,可先判断weak变量是否存在,如果存在,加一个__strong引用,这样block执行过程中,就始终存在对weak变量的强引用了,直到block执行完毕

4、看以下代码,obj对象最后打印的引用计数是多少,为什么?

    NSObject *obj = [NSObject new];
void (^testBlock)(void) = ^{
NSLog(@"%@",obj);
};
NSLog(@"引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));

最后的打印的是3
作为一个局部变量的block,由于引用了外部变量(非静态、常量、全局),定义的时候其实是栈区block,但由于ARC机制,使其拷贝到堆上,变成堆block,所以整个函数执行的过程中,实际上该block,存在两份,一个栈区,一个堆区,这就是使得obj引用计数+2了,加上创建obj的引用,就是3了

5、为什么栈区block要copy到堆上

block:我们称代码块,他类似一个方法。而每一个方法都是在被调用的时候从硬盘到内存,然后去执行,执行完就消失,所以,方法的内存不需要我们管理,也就是说,方法是在内存的栈区。所以,block不像OC中的类对象(在堆区),他也是在栈区的。如果我们使用block作为一个对象的属性,我们会使用关键字copy修饰他,因为他在栈区,我们没办法控制他的消亡,当我们用copy修饰的时候,系统会把该 block的实现拷贝一份到堆区,这样我们对应的属性,就拥有的该block的所有权。就可以保证block代码块不会提前消亡。

iOS安全–浅谈关于iOS加固的几种方法

iOS
关于IOS安全这方面呢,能做的安全保护确实要比Android平台下面能做的少很多。 只要你的手机没越狱,基本上来说是比较安全的,当然如果你的手机越狱了,可能也会相应的产生一些安全方面的问题。就比如我在前面几篇博客里面所介绍的一些IOS逆向分析,动态分析以及破...
继续阅读 »

关于IOS安全这方面呢,能做的安全保护确实要比Android平台下面能做的少很多。
只要你的手机没越狱,基本上来说是比较安全的,当然如果你的手机越狱了,可能也会相应的产生一些安全方面的问题。就比如我在前面几篇博客里面所介绍的一些IOS逆向分析,动态分析以及破解方法。
但是尽管这样,对IOS保护这方面来说,需求还不是很乏,所有基于IOS平台的加固产品也不是很多,目前看到几种关于IOS加固的产品也有做的比较好的。
最开始关于爱加密首创的IOS加密,http://www.ijiami.cn/ios 个人感觉这只是一个噱头而已,因为没有看到具体的工具以及加固应用,所以也不知道它的效果怎么样了。
后来在看雪上面看到一个http://www.safengine.com/mobile/ 有关于IOS加密的工具,但是感觉用起来太麻烦了,而且让产品方也不是很放心,要替换xcode默认的编译器。
不久前看到偶然看到一个白盒加密的应用http://kiwisec.com/ 也下下来试用了一下,感觉要比上面两个从使用上方面了许多,而且考虑的东西也是比较多的。
好了,看了别人做的一些工具,这里大概说下都有哪些加固方法以及大概的实现吧,本人也是刚接触这个方面不就,可能分析的深度没有那么深入,大家就随便听听吧。
现在的加固工具总的来说都是从以下几个方面来做的:
一、字符串加密:
现状:对于字符串来说,程序里面的明文字符串给静态分析提供了极大的帮助,比如说根据界面特殊字符串提示信息,从而定义到程序代码块,或者获取程序使用的一些网络接口等等。
加固:对程序中使用到字符串的地方,首先获取到使用到的字符串,当然要注意哪些是能加密,哪些不能加密的,然后对字符串进行加密,并保存加密后的数据,再在使用字符串的地方插入解密算法,这样就很好的保护了明文字符串。
二、类名方法名混淆
现状:目前市面上的IOS应用基本上是没有使用类名方法名混淆的,所以只要我们使用class-dump把应用的类和方法定义dump下来,然后根据方法名就能够判断很多程序的处理函数是在哪。从而进行hook等操作。
加固:对于程序中的类名方法名,自己产生一个随机的字符串来替换这些定义的类名和方法名,但是不是所有类名,方法名都能替换的,要过滤到系统有关的函数以及类,可以参考下开源项目:https://github.com/Polidea/ios-class-guard
三、程序代码混淆
现状:目前的IOS应用找到可执行文件然后拖到Hopper Disassembler或者IDA里面程序的逻辑基本一目了然。
加固:可以基于Xcode使用的编译器clang,然后在中间层也就是IR实现自己的一些混淆处理,比如加入一些无用的逻辑块啊,代码块啊,以及加入各种跳转但是又不影响程序原有的逻辑。可以参考下开源项目:https://github.com/obfuscator-llvm/obfuscator/ 当然开源项目中也是存在一些问题的,还需自己再去做一些优化工作。
四、加入安全SDK
现状:目前大多数IOS应用对于简单的反调试功能都没有,更别说注入检测,以及其它的一些检测了。
加固:加入SDK,包括多处调试检测,注入检测,越狱检测,关键代码加密,防篡改等等功能。并提供接口给开发者处理检测结果。

当然除了这些外,还有很多方面可以做加固保护的,相信大家会慢慢增加对IOS应用安全的意识,保护好自己的APP。

收起阅读 »

iOS10-iOS15主要适配回顾

iOS
ios15适配1、UITabar、NaBar新增scrollEdgeAppearance,来描述滚动视图滚动到bar边缘时的外观,即使没有滚动视图也需要去指定scrollEdgeAppearance,否则可能导致bar的背景设置无效。具体可以参考UIBarAp...
继续阅读 »

ios15适配

  • 1、UITabar、NaBar新增scrollEdgeAppearance,来描述滚动视图滚动到bar边缘时的外观,即使没有滚动视图也需要去指定scrollEdgeAppearance,否则可能导致bar的背景设置无效。具体可以参考UIBarAppearance
  • 2、tableView 增加sectionHeaderTopPadding属性,默认值是UITableViewAutomaticDimension,可能会使tableView sectionHeader多处一段距离,需要设置 为
  • 3、IDFA 请求权限不弹框问题,解决参考iOS15 ATTrackingManager请求权限不弹框
  • 4、iOS15终于迎来了UIButton的这个改动

ios14适配

  • 1、更改了cell布局视图,之前将视图加载在cell上,将会出现contentView遮罩,导致事件无法响应,必须将customView 放在 contentView 上
  • 2、UIDatePicker默认样式不再是以前的,需要设置preferredDatePickerStyle为 UIDatePickerStyleWheels。
  • 3、IDFA必须要用户用户授权处理,否则获取不到IDFA
  • 4、 UIPageControl的变化 具体参考iOS 14 UIPageControl对比、升级与适配

ios13适配

-1、 iOS 13 推出暗黑模式,UIKit 提供新的系统颜色和 api 来适配不同颜色模式,xcassets 对素材适配也做了调整

  • 2、支持第三方登录必须,就必须Sign In with Apple
  • 3、MPMoviePlayerController 废弃
  • 4、iOS 13 DeviceToken有变化
  • 5、模态弹出默认不再是全屏。
  • 6、私有方法 KVC 不允许使用
  • 7、蓝牙权限需要申请
  • 8、LaunchImage 被弃用
  • 9、新出UIBarAppearance统一配置navigation bars、tab bars、 toolbars等bars的外观。之前设置na bar和tab bar外观的方法可能会无效

ios12适配

  • 1、C++ 标准库libstdc++相关的3个库(libstdc++、libstdc++.6、libstdc++6.0.9 )废弃,使用libc++代替
  • 2、短信 验证码自动填充api
if (@available(iOS 12.0, *)) {
codeTextFiled.textContentType = UITextContentTypeOneTimeCode;
}

ios11适配

  • 1、ViewController的automaticallyAdjustsScrollViewInsets属性被废弃,用scrollView的contentInsetAdjustmentBehavior代替。
  • 2、safeAreaLayoutGuide的引入
  • 3、tableView默认开启了Size-self
  • 4、新增的prefersLargeTitles属性
  • 5、改善圆角,layer新增了maskedCorners属性
  • 6、tableView右滑删除新增api
  • 7、导航条的层级发生了变化。
    ios11适配相关

ios10适配

  • 1、通知统一使用UserNotifications.framework框架
  • 2、UICollectionViewCell的的优化,新增加Pre-Fetching预加载机制
  • 3、苹果加强了对隐私数据的保护,要对隐私数据权限做一个适配,iOS10调用相机,访问通讯录,访问相册等都要在info.plist中加入权限访问描述,不然之前你们的项目涉及到这些权限的地方就会直接crash掉。
  • 4、AVPlayer增加了多个属性,timeControlStatus、
    automaticallyWaitsToMinimizeStalling
  • 5、tabar未选中颜色设置 用 unselectedItemTintColor代替
收起阅读 »

大家好啊,新手一枚,请多关照哈

大家好啊,新手一枚,请多关照哈。。。。。。。。。。

大家好啊,新手一枚,请多关照哈。。。。。。。。。。

React-Native iOS 列表(ListView)优化方案

在项目开发中,很多地方用到了列表,而 React-Native 官网中提供的组件 ListView,虽然能够满足我们的需求,但是性能问题并没有很好的解决,对于需要展现大量数据的列表,app 的内存将会非常庞大。针对 React-Native 的列表性能问题,现...
继续阅读 »

在项目开发中,很多地方用到了列表,而 React-Native 官网中提供的组件 ListView,虽然能够满足我们的需求,但是性能问题并没有很好的解决,对于需要展现大量数据的列表,app 的内存将会非常庞大。针对 React-Native 的列表性能问题,现在提供几套可行性方案:

1.利用 Facebook 提供的建议对 ListView 进行优化

Facebook 官方对 ListView 的性能优化做了简单介绍,并提供了以下几个方法:

  • initialListSize
  • 这个属性用来指定我们第一次渲染时,要读取的行数。如果我们想尽可能的快,我们可以设置它为1, 然后可以在后续的帧中,填弃其它的行。每一次读取的行数,由 pageSize 决定.
  • pageSize
  • 在使用了 initialListSize 之后,ListView 根据 pageSize 来决定每一帧读取的行数,默认值为1, 但如果你的的 views 非常的小,并且读取时占的资源很少, 你可以调整这个值,在找到适合你的值。
  • scrollRenderAheadDistance
  • 如果我们的列表有2000个项,而让它一次性读取,它会导致内存和计算资源的耗尽。所以 scrollRenderAhead distance 可以指定,超出当前视图多少,继续宣染。
  • removeClippedSubviews
  • “当它设置为true时,当本地端的superview为offscreen时 ,不在屏幕上显示的子视图offscreen(它的overflow的值为hidden) 会被删除。它可以改善长列表的滚动的性能,默认值为true.
    这对于大的ListViews来说是一个非常重要。在Android, overflow的值通常为hidden. 所以我们并不需要担心它的设置,但是对于iOS来说,你需要设置row Container的样式为overflow: hidden。

在使用了上述方法后,我们可以看到app的内存占用有了一定的下降(加载100张图片时的效果):

使用前


使用后


3.桥接 Native tableview

第二种方法里面,能够比较好的解决屏幕外的 cell 内存问题,但是和 native tableview 相比,并没有 native 的 cell 重用机制完美,那么,我们可以讲 native 的 tableview 桥接到 React-native 中来,让我们可以在 React-Native 中也可以重用 cell

我们创建一些 VirtualView,他只是遵从了 RCTComponent 协议,其实并不是一个真正的 View,我把它形成一个组件,把它 Bridge 到 JS,这就使得,你在写 JSX 的时候,就可以直接用 VirtualView 来去做布局了。在RN里面做布局的时候我们用VirtualView来做布局。但是最终在 insertReactSubview 时,我们把这些 VirtualView 当做数据去处理,通过 VirtualView 和RealView 的对应关系,把它转化成一个真实的 View 对象添加到 TableView 中去。


但是使用这种方法,我们需要将 tableview 的所有常用数据源方法和代理方法都桥接到 React-Native 中来,甚至对于一些 cell 组件,我们也需要自己桥接,并不能像 React-Native 那样使用自己的组件。当我们的需求比较复杂或者需求发生变化时,就需要重新桥接我们的自定义 cell,这样工作量就会比较大。大多数的 cell 里面如果做展示来用的话,Label 和 Image 基本上能够满足大多数的需求了。所以我们现在只是做了 Label 和 Image 的对应工作,但在 RN 的一些官方控件,在这个 view 里面都是没法直接使用的。

4.用 JS 实现一套 cell 重用的逻辑

基于 RN 的 ScrollView,我们也监听 OnScroll(),他往上滑的时候,我们需要把上面的 cellComponent 挪下来,挪到上面去用。但是这个方式最终的效果并不是特别好。

问题在于,如果我们所有的 Cell 都是一样高的,里面的元素不是很多的情况下,性能还相对好一些,我们每次 OnScroll 的时候,他处理的Cell比较少。如果你希望有一个界面滚动能够达到流畅的话,所有的处理都需要在 16ms 内完成,但是这又造成了 onScroll 都要去刷新页面,导致这样的交互会非常非常多,导致你从 JS,到 native 的 bridge 要频繁的通讯,JS 中的很多处理方式都是异步的,使得这个方案的效果没有达到很好的预期。

总结

从上面的几种方案可以看出,方案1、2、3、4都能够比较好的解决列表的性能问题 ,而且各有优缺点,那么,我们在项目开发中该如何应用呢?

  • 当我们在进行列表展示的时候,如果数据量不是特别的庞大(不是无限滚动的),且界面比较复杂的时候,方案1能够比较好的解决性能问题,而且操作起来比较简单,只需要对 listview 的一些属性进行基本设置。
  • 当我们需要展示很多数据的时候(不是无限滚动的),我们可以使用方案2,对那些超出屏幕外的部分,对他进行组件最小化
  • 当我们需要展示大量数据(可以无限滚动的),我们可以通过方案3/4,来达到重用的目的


收起阅读 »

iOS-底层原理 02:alloc & init & new 源码分析

在分析alloc源码之前,先来看看一下3个变量 内存地址 和 指针地址 区别:分别输出3个对象的内容、内存地址、指针地址,下图是打印结果结论:通过上图可以看出,3个对象指向的是同一个内存空间,所以其内容 和 内存地址是相同的,但是对象的指针...
继续阅读 »

在分析alloc源码之前,先来看看一下3个变量 内存地址 和 指针地址 区别:


分别输出3个对象的内容、内存地址、指针地址,下图是打印结果


结论:通过上图可以看出,3个对象指向的是同一个内存空间,所以其内容 和 内存地址相同的,但是对象的指针地址是不同的

%p -> &p1:是对象的指针地址,
%p -> p1: 是对象指针指向的的内存地址

这就是本文需要探索的内容,alloc做了什么?init做了什么?

准备工作

alloc 源码探索

alloc + init 整体源码的探索流程如下


  • 【第一步】首先根据main函数中的LGPerson类的alloc方法进入alloc方法的源码实现(即源码分析开始),

  • 【第二步】跳转至_objc_rootAlloc的源码实现

  • 【第三步】跳转至callAlloc的源码实现

如上所示,在calloc方法中,当我们无法确定实现走到哪步时,可以通过断点调试,判断执行走哪部分逻辑。这里是执行到_objc_rootAllocWithZone

slowpath & fastpath

其中关于slowpathfastpath这里需要简要说明下,这两个都是objc源码中定义的,其定义如下



其中的__builtin_expect指令是由gcc引入的,
1、目的:编译器可以对代码进行优化,以减少指令跳转带来的性能下降。即性能优化
2、作用:允许程序员将最有可能执行的分支告诉编译器。
3、指令的写法为:__builtin_expect(EXP, N)。表示 EXP==N的概率很大。
4、fastpath定义中__builtin_expect((x),1)表示 x 的值为真的可能性更大;即 执行if 里面语句的机会更大
5、slowpath定义中的__builtin_expect((x),0)表示 x 的值为假的可能性更大。即执行 else 里面语句的机会更大
6、在日常的开发中,也可以通过设置来优化编译器,达到性能优化的目的,设置的路径为:Build Setting --> Optimization Level --> Debug --> 将None 改为 fastest 或者 smallest

cls->ISA()->hasCustomAWZ()

其中fastpath中的 cls->ISA()->hasCustomAWZ() 表示判断一个类是否有自定义的 +allocWithZone 实现,这里通过断点调试,是没有自定义的实现,所以会执行到 if 里面的代码,即走到_objc_rootAllocWithZone


【第四步】跳转至_objc_rootAllocWithZone的源码实现


【第五步】跳转至_class_createInstanceFromZone的源码实现,这部分是alloc源码的核心操作,由下面的流程图及源码可知,该方法的实现主要分为三部分
cls->instanceSize:计算需要开辟的内存空间大小
calloc:申请内存,返回地址指针
obj->initInstanceIsa:将 类 与 isa 关联


根据源码分析,得出其实现流程图如下所示:


alloc 核心操作

核心操作都位于calloc方法中

cls->instanceSize:计算所需内存大小

计算需要开辟内存的大小的执行流程如下所示


  • 1、跳转至instanceSize的源码实现

通过断点调试,会执行到cache.fastInstanceSize方法,快速计算内存大小

  • 2、跳转至fastInstanceSize的源码实现,通过断点调试,会执行到align16

  • 3、跳转至align16的源码实现,这个方法是16字节对齐算法

内存字节对齐原则

在解释为什么需要16字节对齐之前,首先需要了解内存字节对齐的原则,主要有以下三点

数据成员对齐规则:struct 或者 union 的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如数据、结构体等)的整数倍开始(例如int在32位机中是4字节,则要从4的整数倍地址开始存储)
数据成员为结构体:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(例如:struct a里面存有struct b,b里面有char、int、double等元素,则b应该从8的整数倍开始存储)
结构体的整体对齐规则:结构体的总大小,即sizeof的结果,必须是其内部做大成员的整数倍,不足的要补齐
为什么需要16字节对齐

需要字节对齐的原因,有以下几点:

通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块为单位存取,块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数来降低cpu的开销
16字节对齐,是由于在一个对象中,第一个属性isa占8字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱
16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况
字节对齐-总结

在字节对齐算法中,对齐的主要是对象,而对象的本质则是一个 struct objc_object的结构体,
结构体在内存中是连续存放的,所以可以利用这点对结构体进行强转。
苹果早期是8字节对齐,现在是16字节对齐
下面以align(8) 为例,图解16字节对齐算法的计算过程,如下所示

首先将原始的内存 8 与 size_t(15)相加,得到 8 + 15 = 23
将 size_t(15) 即 15进行~(取反)操作,~(取反)的规则是:1变为0,0变为1
最后将 23 与 15的取反结果 进行 &(与)操作,&(与)的规则是:都是1为1,反之为0,最后的结果为 16,即内存的大小是以16的倍数增加的

calloc:申请内存,返回地址指针

通过instanceSize计算的内存大小,向内存中申请 大小 为 size的内存,并赋值给obj,因此 obj是指向内存地址的指针


这里我们可以通过断点来印证上述的说法,在未执行calloc时,po objnil,执行后,再po obj法线,返回了一个16进制的地址


在平常的开发中,一般一个对象的打印的格式都是类似于这样的<LGPerson: 0x01111111f>(是一个指针)。为什么这里不是呢?

  • 主要是因为objc 地址 还没有与传入 的 cls进行关联,
  • 同时印证了 alloc的根本作用就是 开辟内存
obj->initInstanceIsa:类与isa关联

经过calloc可知,内存已经申请好了,类也已经出入进来了,接下来就需要将 类与 地址指针 即isa指针进行关联,其关联的流程图如下所示


主要过程就是初始化一个isa指针,并将isa指针指向申请的内存地址,在将指针与cls类进行 关联

同样也可以通过断点调试来印证上面的说法,在执行完initInstanceIsa后,在通过po obj可以得出一个对象指针


总结

  • 通过对alloc源码的分析,可以得知alloc的主要目的就是开辟内存,而且开辟的内存需要使用16字节对齐算法,现在开辟的内存的大小基本上都是16的整数倍
  • 开辟内存的核心步骤有3步:计算 -- 申请 -- 关联

init 源码探索

alloc源码探索完了,接下来探索init源码,通过源码可知,inti的源码实现有以下两种

类方法 init


这里的init是一个构造方法 ,是通过工厂设计(工厂方法模式),主要是用于给用户提供构造方法入口。这里能使用id强转的原因,主要还是因为 内存字节对齐后,可以使用类型强转为你所需的类型

实例方法 init

  • 通过以下代码进行探索实例方法 init

  • 通过main中的init跳转至init的源码实现

  • 跳转至_objc_rootInit的源码实现

有上述代码可以,返回的是传入的self本身。

new 源码探索

一般在开发中,初始化除了init,还可以使用new,两者本质上并没有什么区别,以下是objc中new的源码实一般在开发中,初始化除了init,还可以使用new,两者本质上并没有什么区别,以下是objc中new的源码实现,通过源码可以得知,new函数中直接调用了callAlloc函数(即alloc中分析的函数),且调用了init函数,所以可以得出new 其实就等价于 [alloc init]的结论


但是一般开发中并不建议使用new,主要是因为有时会重写init方法做一些自定义的操作,例如 initWithXXX,会在这个方法中调用[super init],用new初始化可能会无法走到自定义的initWithXXX部分。

例如,在CJLPerson中有两个初始化方法,一个是重写的父类的init,一个是自定义的initWithXXX方法,如下图所示


使用 alloc + init 初始化时,打印的情况如下


使用new 初始化时,打印的情况如下

总结

如果子类没有重写父类的init,new会调用父类的init方法
如果子类重写了父类的init,new会调用子类重写的init方法
如果使用 alloc + 自定义的init,可以帮助我们自定义初始化操作,例如传入一些子类所需参数等,最终也会走到父类的init,相比new而言,扩展性更好,更灵活。

补充

【问题】为什么无法断点到obj->initInstanceIsa(cls, hasCxxDtor);

主要是因为断点断住的不是 自定义类的流程,而是系统级别的


作者:style_月月
链接:https://blog.csdn.net/lin1109221208/article/details/108427260

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

iOS底层原理01:源码探索的三种方式

iOS
本文主要介绍下源码探索的三种方法1、符号断点直接跟流程2、通过按住control+step into3、汇编跟流程下面详细讲下这三种方法是如何查找到函数所在的源码库,以alloc为例1、符号断点直接跟流程通过下alloc的符号断点选择断点Symbolic Br...
继续阅读 »

本文主要介绍下源码探索的三种方法

  • 1、符号断点直接跟流程
  • 2、通过按住control+step into
  • 3、汇编跟流程

下面详细讲下这三种方法是如何查找到函数所在的源码库,以alloc为例

1、符号断点直接跟流程

  • 通过下alloc的符号断点

    • 选择断点Symbolic Breakpoint


符号断点中输入 alloc


main中的CJLPerson处 加一个断点
在走到这部分断点之前,需要关闭上面新增的符号断点,原因是因为alloc的调用有很多,如果开启了就不能准确的定位到CJLPerson的alloc方法


以下为符号断点的关闭状态


运行程序, 断在CJLPerson部分

  • 打开 alloc符号断点 ,断点状态为


    继续执行


    以下为alloc符号断点断住的堆栈调用情况,从下图可以看出 alloc 的源码位于libobjc.A.dylib库(需要去Apple 相应的开源网址下载 objc源码进行更深入的探索)


    2、通过按住control+step into

    • main中的CJLPerson处 加一个断点,运行程序,会断在CJLPerson位置


  • 按住 control键,选择 step into ⬇️键


进去后,显示为以下内容


再下一个objc_alloc符号断点,符号断点后显示了 objc_alloc所在的源码库
(需要去Apple 相应的开源网址下载 objc源码进行更深入的探索)


3、汇编跟流程

main中的CJLPerson处 加一个断点,运行程序,会断在CJLPerson位置


xcode 工具栏 选择 Debug --> Debug Workflow --> Always Show Disassembly,这个 选项表示 始终显示反汇编 ,即 通过汇编 跟流程


按住control,点击 step into ⬇️键,执行到下图的callq ,对应 objc_alloc


  • 按住control,点击 step into ⬇️键进入,看到断点断在objc_alloc部分


  • 同样通过objc_alloc的符号断点,得知源码所在库
    (需要去Apple 相应的开源网址下载 objc源码进行更深入的探索)



作者:style_月月
链接:https://blog.csdn.net/lin1109221208/article/details/108425742
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

苹果:App自6月30日起支持删除账号,开发者相关问题都在这里了

今晨,苹果正式宣布自 2022 年 6 月 30 日起,提交至 App Store 且支持账号创建的应用,必须允许用户在应用内删除账号。6 月 30 日起,App 必须允许用户删除账号从 2022 年 6 月 30 日开始,App Store 内支持账号创建的...
继续阅读 »

今晨,苹果正式宣布自 2022 年 6 月 30 日起,提交至 App Store 且支持账号创建的应用,必须允许用户在应用内删除账号。

6 月 30 日起,App 必须允许用户删除账号

从 2022 年 6 月 30 日开始,App Store 内支持账号创建的应用,必须提供删除账号的功能。

1653473320(1).jpg

出海痛点很多?点击这里解决



开发者如需更新应用程序以完善删除账号功能,需要注意以下几点:

1)用户能在应用中快速找到删除账号的入口,一般可在账户设置中找到;

2)如果用户是通过 Apple ID 登录,需要在删除账号时使用 Sign in with Apple REST API 来撤销用户令牌;

发.png

3)用户删除账号不仅是暂时停用或禁用账号,苹果要求在应用内,所有与该账号相关的个人数据都可以被删除,以帮助用户更好地管理隐私数据;

4)受高度监管的应用可能需要提供额外的客户服务流程,以跟进账号删除过程;

5)遵守有关存储和保留用户账号信息以及处理账号删除的适用法律要求,包括遵守不同国家或地区的当地法律。

此外,如果用户需要访问网站以指引如何删除账号,开发者也需提供相关链接。

若删除账号需要额外的时间,或删除时应用购买问题需要另外解决,开发者也应告知用户。

App 删除账号功能相关问题

Q:开发者可以将用户引导到客户服务流程以完成账号删除吗?

A:受高度监管的应用,如中应用商店审查指南 5.1.1(ix)所述,可能会使用额外的客户服务流程来确认和促进账号删除过程。

不在高度监管的行业中运行的应用程序不应要求用户拨打电话、发送电子邮件或通过其他支持流程完成账号删除。

Q:开发者是否可以要求重新认证,或添加确认步骤以确保账号不会被意外删除或被账号持有人以外的人删除?

A:可以,确保删除动作是用户期望进行的。

开发者可以添加步骤来验证用户身份,并确认他们想要删除该账号(如通过输入已与该账号关联的电子邮件或电话号码)。

但是,给用户删除账号增加不必要的困难将不会通过审核。

Q:如应用使用 Sign in with Apple 为用户提供账号创建和身份验证,需要进行哪些更改?

A:支持 Sign in with Apple 的应用需要使用 Sign in with Apple REST API 来撤销用户令牌。更多信息,请查看苹果官方文档和设计建议。

Q:如果开发者的应用链接到默认网络浏览器以创建账号,是否仍需要在应用内提供账号删除功能?

A:是的。但请注意链接到默认 Web 浏览器进行登录或注册账号,会影响用户体验,具体可查看应用商店审查指南 4。

Q:应用会自动为用户创建一个账号,是否需要提供进行账号删除的选项?

A:是的。用户应该可以选择删除自动生成的账号(包括访客账号)以及与这些账号关联的数据。

同时,开发者需要确保应用中的任何账号创建都符合当地法律。

Q:账号删除是否必须立即自动完成?

A:不是,可以接受手动删除账号,并花费一些时间。

开发者需要通知用户删除账号需要多长时间,并在删除完成后提供确认,并确保删除账号所用的时间。

Q:删除账号后,用户产生的内容是否需要在共享的应用中删除?

A:是的。用户删除账号时,将删除与其账号关联的所有数据,包括与他人一起生成的内容,如照片、视频、文字帖子和评论等。

如果当地法律要求开发者维护某些数据,请另外告知用户。

Q:是否允许应用只在某些地方根据 CCPA、GDPR 或其他当地法律删除账号?

A:不可以。应该允许所有用户删除他们的账号,无论他们身在何处,开发者的账号删除流程也需要提供给所有用户。

Q:如何管理自动续订的用户,以免在用户删除账号后意外收费?

A:告知用户管理订阅,后续计费将通过 Apple 继续,并提醒用户在下一次收费前取消订阅。

开发者使用 App Store 自动续订的 Server Notifications,可以实时查看用户的订阅状态,或者使用订阅状态 API 进行识别。

同时,开发者可以提供 Apple 支持链接(https: //support.apple.com/en-us/HT204084),帮助用户提交退款请求。

此外,开发者还可以提供一个选项,即设置账号删除日期与订阅到期时间一致,但仍需提供可立即删除账号的选项。

应用更新过程中的更多常见问题,可访问以下网站了解:

https://developer.apple.com/support/offering-account-deletion-in-your-app

据悉,苹果去年就已宣布调整 App Store 的指导方针,要求应用允许用户删除自己的账户,但由于功能实现较复杂,苹果两度推迟实行。如今正式推行,预计未来一段时间内或将有大量应用进行更新。

收起阅读 »

KTV歌词解析, 音准评分组件

iOS
KTV歌词解析, 音准评分组件介绍支持XML歌词解析, LRC歌词解析, 解决了多行歌词进度渲染的问题, 评分根据人声实时计算评分欢迎各位大佬提交PR, 有问题提issue, 我会不定时fixGithub使用方法初始化    private...
继续阅读 »

KTV歌词解析, 音准评分组件

KTV歌词解析, 音准评分组件

介绍

支持XML歌词解析, LRC歌词解析, 解决了多行歌词进度渲染的问题, 评分根据人声实时计算评分

欢迎各位大佬提交PR, 有问题提issue, 我会不定时fix

Github

使用方法

初始化

    private lazy var lrcScoreView: AgoraLrcScoreView = {
       let lrcScoreView = AgoraLrcScoreView(delegate: self)
       lrcScoreView.config.scoreConfig.scoreViewHeight = 100
       lrcScoreView.config.scoreConfig.emitterColors = [.systemPink]
       lrcScoreView.config.lrcConfig.lrcFontSize = .systemFont(ofSize: 15)
       return lrcScoreView
   }()

配置属性

组件base配置
    /// 评分组件配置
   public var scoreConfig: AgoraScoreItemConfigModel = .init()
   /// 歌词组件配置
   public var lrcConfig: AgoraLrcConfigModel = .init()
   /// 是否隐藏评分组件
   public var isHiddenScoreView: Bool = false
   /// 背景图
   public var backgroundImageView: UIImageView?
   /// 评分组件和歌词组件之间的间距 默认: 0
   public var spacing: CGFloat = 0
歌词配置
    /// 无歌词提示文案
   public var tipsString: String = "纯音乐,无歌词"
   /// 提示文字颜色
   public var tipsColor: UIColor = .black
   /// 提示文字大小
   public var tipsFont: UIFont = .systemFont(ofSize: 17)
   /// 分割线的颜色
   public var separatorLineColor: UIColor = .lightGray
   /// 是否隐藏分割线
   public var isHiddenSeparator: Bool = false
   /// 默认歌词背景色
   public var lrcNormalColor: UIColor = .gray
   /// 高亮歌词背景色
   public var lrcHighlightColor: UIColor = .white
   /// 实时绘制的歌词颜色
   public var lrcDrawingColor: UIColor = .orange
   /// 歌词文字大小 默认: 15
   public var lrcFontSize: UIFont = .systemFont(ofSize: 15)
   /// 歌词高亮文字缩放大小 默认: 1.1
   public var lrcHighlightScaleSize: Double = 1.1
   /// 歌词左右两边间距
   public var lrcLeftAndRightMargin: CGFloat = 15
   /// 等待开始圆点背景色 默认: 灰色
   public var waitingViewBgColor: UIColor? = .gray
   /// 等待开始圆点大小 默认: 10
   public var waitingViewSize: CGFloat = 10
   /// 是否可以拖动歌词 默认: true
   public var isDrag: Bool = true
评分配置
    /// 评分视图高度 默认:100
   public var scoreViewHeight: CGFloat = 100
   /// 圆的起始位置: 默认: 100
   public var innerMargin: CGFloat = 100
   /// 线的高度 默认:10
   public var lineHeight: CGFloat = 10
   /// 线的宽度 默认: 120
   public var lineWidht: CGFloat = 120
   /// 默认线的背景色
   public var normalColor: UIColor = .gray
   /// 匹配后线的背景色
   public var highlightColor: UIColor = .orange
   /// 分割线的颜色
   public var separatorLineColor: UIColor = .systemPink
   /// 是否隐藏垂直分割线
   public var isHiddenVerticalSeparatorLine: Bool = false
   /// 是否隐藏上下分割线
   public var isHiddenSeparatorLine: Bool = false
   /// 游标背景色
   public var cursorColor: UIColor = .systemPink
   /// 游标的宽
   public var cursorWidth: CGFloat = 20
   /// 游标的高
   public var cursorHeight: CGFloat = 20
   /// 是否隐藏粒子动画效果
   public var isHiddenEmitterView: Bool = false
   /// 使用图片创建粒子动画
   public var emitterImages: [UIImage]?
   /// emitterImages为空默认使用颜色创建粒子动画
   public var emitterColors: [UIColor] = [.red]
   /// 尾部动画图片
   public var tailAnimateImage: UIImage?
   /// 尾部动画颜色
   public var tailAnimateColor: UIColor? = .yellow
   /// 评分默认分数: 50
   public var defaultScore: Double = 50

事件回调

歌词Delegate
weak var delegate: AgoraLrcViewDelegate?

protocol AgoraLrcViewDelegate {
   /// 当前播放器的时间 单位: 秒
   func getPlayerCurrentTime() -> TimeInterval
   /// 获取歌曲总时长
   func getTotalTime() -> TimeInterval

   /// 设置播放器时间
   @objc
   optional func seekToTime(time: TimeInterval)
   /// 当前正在播放的歌词和进度
   @objc
   optional func currentPlayerLrc(lrc: String, progress: CGFloat)
}
歌词下载Delegate
weak var downloadDelegate: AgoraLrcDownloadDelegate?

protocol AgoraLrcDownloadDelegate {
   /// 开始下载
   @objc
   optional func beginDownloadLrc(url: String)
   /// 下载完成
   @objc
   optional func downloadLrcFinished(url: String)
   /// 下载进度
   @objc
   optional func downloadLrcProgress(url: String, progress: Double)
   /// 下载失败
   @objc
   optional func downloadLrcError(url: String, error: Error?)
   /// 下载取消
   @objc
   optional func downloadLrcCanceld(url: String)
   /// 开始解析歌词
   @objc
   optional func beginParseLrc()
   /// 解析歌词结束
   @objc
   optional func parseLrcFinished()
}
评分Delegate
weak var scoreDelegate: AgoraKaraokeScoreDelegate?

protocol AgoraKaraokeScoreDelegate {
   /// 分数实时回调
   /// score: 每次增加的分数
   /// cumulativeScore: 累加分数
   /// totalScore: 总分
   @objc optional func agoraKaraokeScore(score: Double, cumulativeScore: Double, totalScore: Double)
}

集成方式

本地pod引入,暂时使用本地pod 待后续发布cocoapods

把 'AgoraLrcScoreView' 复制到根目录, 执行pod

pod 'AgoraLrcScore', :path => "AgoraLrcScoreView"

作者:莫烦恼
来源:https://juejin.cn/post/7054443857928257550

收起阅读 »

swift 苹果登录

iOS
- 苹果登录的前期工作: - 1.开发者账号中增加苹果登录的选项- 2.xcode中配置苹果登录 //swift版本的代码逻辑 //头文件 import AuthenticationServices //按钮加载 苹果登录 对于按钮有一定的要求,具体查看...
继续阅读 »
苹果登录
项目中继承第三方登录时,需增加上苹果登录即可上架
苹果登录需要iOS系统 13以上支持
详细的内容阅读苹果官方的网址
url:https://developer.apple.com/documentation/authenticationservices/implementing_user_authentication_with_sign_in_with_apple
- 苹果登录的前期工作:
- 1.开发者账号中增加苹果登录的选项


1.1  可能会造成证书无法使用,重新编辑一下保存下载即可!


- 2.xcode中配置苹果登录


前期的配置基本上完成
剩下的就是代码逻辑
- 3.代码中增加苹果登录的逻辑
//swift版本的代码逻辑
//头文件
import AuthenticationServices

//按钮加载 苹果登录 对于按钮有一定的要求,具体查看上方的连接
// 此处使用了一个临时的
if #available(iOS 13.0, *) {
let authorizationButton = ASAuthorizationAppleIDButton()
authorizationButton.frame = CGRect(x: (KScreenWidth - 300) / 2, y: kScreenHeight - 50, width: 300, height: 30)
authorizationButton.addTarget(self, action: #selector(handleAuthorizationAppleIDButtonPress), for: .touchUpInside)
self.view.addSubview(authorizationButton)
} else {
// Fallback on earlier versions
}


//MARK: 点击苹果登陆按钮
@objc
func handleAuthorizationAppleIDButtonPress() {

if #available(iOS 13.0, *) {
/**
- 点击 苹果登录的按钮跳出苹果登录的界面
- 跳转出系统界面
*/

let appleIDProvider = ASAuthorizationAppleIDProvider()
let request = appleIDProvider.createRequest()
request.requestedScopes = [.fullName, .email]

let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self as? ASAuthorizationControllerPresentationContextProviding
authorizationController.performRequests()

} else {
// Fallback on earlier versions
}

}

//MARK: - 授权成功
@available(iOS 13.0, *)
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
if #available(iOS 13.0, *) {
switch authorization.credential {
case let appleIDCredential as ASAuthorizationAppleIDCredential:
/**
- 首次注册 能够那去到的参数分别是:
1. user
2.state
3.authorizedScopes
4.authorizationCode
5.identityToken
6.email
7.fullName
8.realUserStatus
*/

// Create an account in your system.
let userIdentifier = appleIDCredential.user
let fullName = appleIDCredential.fullName
let email = appleIDCredential.email
let code = appleIDCredential.authorizationCode
// For the purpose of this demo app, store the `userIdentifier` in the keychain.
self.saveUserInKeychain(userIdentifier)

// For the purpose of this demo app, show the Apple ID credential information in the `ResultViewController`.
self.showResultViewController(userIdentifier: userIdentifier, fullName: fullName, email: email)
BPLog.lmhInfo("userID:\(userIdentifier),fullName:\(fullName),userEmail:\(email),code:\(code)")
case let passwordCredential as ASPasswordCredential:

// Sign in using an existing iCloud Keychain credential.
let username = passwordCredential.user
let password = passwordCredential.password

// For the purpose of this demo app, show the password credential as an alert.
DispatchQueue.main.async {
self.showPasswordCredentialAlert(username: username, password: password)
}

default:
break
}
} else {
// Fallback on earlier versions
}
}
收起阅读 »

SwiftUI版通知栏应用开发(4) ——多语言本地化适配

iOS
开发多语言版本的 APP,估计是大家希望的,尤其对于 iOS/Mac APP 的开发,上线 App Store 多希望在其它地区也能使用,所以今天主要想学习怎么基于 SwiftUI 做一些文本和字符串文字多语言化。相信市面上不少这样的文章可供参考Project...
继续阅读 »

开发多语言版本的 APP,估计是大家希望的,尤其对于 iOS/Mac APP 的开发,上线 App Store 多希望在其它地区也能使用,所以今天主要想学习怎么基于 SwiftUI 做一些文本和字符串文字多语言化。

相信市面上不少这样的文章可供参考

Project 配置

首先,在 Project Info 选项中,选择 Localization 增加一个「中文」本地化配置,如果你需要其他语言,可以相对应的添加:

接下来,新建对应的 Group 文件夹,如本文的两个 Groupen.lprojzh-Hans.lproj

在这两个 Group 里,同时创建同名的 Strings 文件:Localizable

代码编写

创建完成之后,我们分别创建一个 Preferences demo:

在我们的主 View 上引入变量 locale

popOver.contentViewController?.view = NSHostingView(rootView: MainView().environment(\.locale, .init(identifier: "en")))

然后创建一个 Text

Text("Preferences")
.font(.customf(22))
.padding(.bottom, 10.0)

刚开始我们定义的是 en,执行看看效果:

如果我们改为 zh-Hans

MainView().environment(\.locale, .init(identifier: "zh-Hans"))

结果也就不一样了:

Locale 变化功能

基本功能实现了,接下来就是设置一个开关来变化 Locale 了。

首先,创建一个 Picker

Section(header: Text("localization")) {
Picker("", selection: $localeViewModel.localeString) {
ForEach(LocaleStrings.allCases) { localeString in
Text(localeString.rawValue)
.tag(localeString.suggestedLocalication)
}
}
.pickerStyle(SegmentedPickerStyle())
}

其中,我定义两个 enum 来做选择的类型:

enum Localication: String, CaseIterable, Identifiable {
case zh_Hans = "zh-Hans"
case en = "en"
var id: String { self.rawValue }
}

enum LocaleStrings: String, CaseIterable, Identifiable {
case zh_Hans = "中文"
case en = "English"
var id: String { self.rawValue }
}

extension LocaleStrings {
var suggestedLocalication: Localication {
switch self {
case .zh_Hans: return .zh_Hans
case .en: return .en
}
}
}

这个好理解,因为显示的 String 和提供给 locale 的字符串不一致,如显示的是「中文」,提供给 locale 的是 zh-Hans,这里我借助 suggestedLocalication 做桥梁转换。

最后,我们创建一个 Combine ViewModel 变量 localeString,以供实时变化改变本地化字符串内容。

class LocaleViewModel: ObservableObject {
@Published var localeString: Localication
}

最后,只需要在具体的 View 里加入 ViewModel 订阅变量 localeString 的更新:

// ContentView.swift

import SwiftUI

struct ContentView: View {
@ObservedObject private var timerViewModel: TimerViewModel
@ObservedObject private var localeViewModel: LocaleViewModel
init(timerViewModel: TimerViewModel, localeViewModel: LocaleViewModel) {
self.timerViewModel = timerViewModel
self.localeViewModel = localeViewModel
}

var body: some View {
Text("localization")
.font(.customf(14))
.padding()
.environment(\.locale, .init(identifier: localeViewModel.localeString.rawValue))
}
}

好了,代码编写完毕,我们运行看效果:

localechange2

小结

基本跑通本地化多语言适配流程,接下来就是不断增加新的语言和翻译工作。

未完待续

收起阅读 »

[译] SwiftUI 2 应用生命周期的终极指导

原文地址:The Ultimate Guide to the SwiftUI 2 Application Life Cycle原文作者:Peter Friese译文出自:掘金翻译计划本文永久链接:github.com/xitu/gold-m…译者:zhuzil...
继续阅读 »

在很长一段时间里,iOS 开发者们都是使用 AppDelegate 作为应用的主要入口。随着 SwiftUI 2 在 WWDC 2020 上发布,苹果公司引入了一个新的应用生命周期。新的生命周期几乎(几乎)完全与 AppDelegate 无关,为类 DSL 方法铺平了道路。

在本文中,我会讨论引入新的生命周期的原因,以及你该如何在已有的应用或新的应用中使用它。

指定应用入口

我们的第一个问题是,该如何告诉编译器哪里是应用的入口呢?SE-0281 详述了**基于类型的程序入口(Type-Based Program Entry Points)**的工作方式:

Swift 编译器将识别标注了 @main 属性的类型为程序的入口。标有 @main 的类型有一个隐式要求:类型内部需要声明一个静态 main() 方法。

创建新的 SwiftUI 应用时,应用的主类(main class)如下所示:

import SwiftUI

@main
struct SwiftUIAppLifeCycleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

那么 SE-0281 提到的静态 main() 函数在哪儿呢?

实际上,框架可以(并且应该)为用户提供方便的默认实现。你会从上面的代码片段注意到 SwiftUIAppLifeCycleApp 遵循 App 协议。对于 App 协议,苹果提供了如下协议扩展:

@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension App {

/// 初始化并运行应用。
///
/// 如果你在你的 ``SwiftUI/App`` 的实现类(conformer)的声明前加上了
/// [@main](https://docs.swift.org/swift-book/ReferenceManual/Attributes.html#ID626)
/// 属性,系统会调用这个实现类的 `main()` 方法来启动应用。
/// SwiftUI 提供了该方法的默认实现,从而能以适合平台的方式处理应用启动流程。
public static func main()
}

这下你就懂了吧 —— 这个协议扩展提供了处理应用启动的默认的实现。

由于 SwiftUI 框架不是开源的,所以我们看不到苹果是如何实现此功能的,但是 Swift Argument Parser 是开源的,并且也用了这个办法。查看 ParsableCommand 的源码,就能了解它是如何用协议扩展来提供静态 main 函数的默认实现,并将其用作程序入口的:

extension ParsableCommand {
...
public static func main(_ arguments: [String]?) {
do {
var command = try parseAsRoot(arguments)
try command.run()
} catch {
exit(withError: error)
}
}

public static func main() {
self.main(nil)
}
}

如果上述这些听起来有点复杂,好消息是实际上在创建新的 SwiftUI 应用程序时你不必关心它:只需确保在 Life Cycle 下拉菜单中选择 SwiftUI App 来创建你的应用程序就行了:

创建一个新的 SwiftUI 项目

让我们来看一些常见的情况。

初始化资源 / 你最喜欢的 SDK 或框架

大多数应用程序需要在启动时执行这些步骤:获取一些配置值,连接数据库或者初始化框架或第三方 SDK。

通常,您可以在 ApplicationDelegate 的 application(_:didFinishLaunchingWithOptions:) 方法中进行这些操作。由于已经没有应用委托了,我们需要找到其他方法来初始化我们的应用程序。根据您的特定需求,有以下策略:

  • 为你的主类实现一个构造函数(initializer)(详见文档
  • 为存储属性设置初始值(详见文档
  • 用闭包设置属性的默认值(详见文档
@main
struct ColorsApp: App {
init() {
print("Colors application is starting up. App initialiser.")
}

var body: some Scene {
WindowGroup {
ContentView()
}
}

如果上述几种策略都无法满足你的需求,你可能还是需要一个 AppDelegate。后文会介绍如果能在应用中加入一个 AppDelegate。

处理你的应用的生命周期

了解你的应用程序处于哪种状态有时很有用。例如,你可能希望应用处于活动状态时立即获取新数据,或者在应用程序变为非活动状态并转换到后台后清除所有缓存。

通常,您可以在你的 ApplicationDelegate 上实现 applicationDidBecomeActiveapplicationWillResignActive 或 applicationDidEnterBackground

从 iOS 14.0 起,苹果提供了新的 API,该 API 允许以更优雅,更易维护的方式跟踪应用程序状态:[ScenePhase](https://developer.apple.com/documentation/swiftui/scenephase)。你的项目可以有多个场景(scene),不过有时只有一个场景。这些场景将由 [WindowGroup](https://developer.apple.com/documentation/swiftui/windowgroup) 展示。

SwiftUI 追踪环境中场景的状态,你可以使用 @Environment 属性包装器来获取 scenePhase 的值,然后使用 onChange(of:) modifier 来监听该值的变化:

@main
struct SwiftUIAppLifeCycleApp: App {
@Environment(\.scenePhase) var scenePhase

var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { newScenePhase in
switch newScenePhase {
case .active:
print("App is active")
case .inactive:
print("App is inactive")
case .background:
print("App is in background")
@unknown default:
print("Oh - interesting: I received an unexpected new value.")
}
}
}
}

值得注意的是,你可以从应用中的其他位置读取该值。当在应用的顶层读取该值时(如上面的代码片段所示),你将获得应用程序中所有阶段(phase)的汇总。.inactive 表示你应用中的所有场景均未激活。当在视图中读取 scenePhase 时,你将收到包含该视图的阶段值。请记住,你的应用程序在在同一时刻可能包含在不同阶段的多个场景。想了解有关场景阶段的更多详细信息,请阅读苹果的[文档](developer.apple.com/documentati…

处理深层链接(Deeplink)

之前,在处理深层链接时,你需要实现 application(_:open:options:),并将传入的 URL 转给最合适的处理程序。

新的应用生命周期模型可以更容易地处理深层链接。在最顶层的场景上添加 onOpenURL 就可以处理传入的 URL 了:

@main
struct SwiftUIAppLifeCycleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
print("Received URL: \(url)")
}
}
}
}

真正酷的是:你可以在整个应用程序中装上多个 URL 处理程序 —— 让进行深层链接变得很轻松,因为你可以在最合适的位置处理传入的链接。

可能的话,你应该使用 universal links(或者 Firebase Dynamic Links,它使用了 universal links for iOS apps),因为它们使用了关联域(associated domain)来创建网站和你的应用之间的链接 —— 这会让你可以安全地共享数据。

不过,你仍可以使用自定义 URL scheme 来链接应用内部的内容。

无论哪种方式,触发应用中的深层链接的一种简单方法是在开发计算机上使用以下命令:

xcrun simctl openurl booted <your url>

Demo: Opening deep links and continuing user activities

继续用户 activity

如果你的应用使用 NSUserActivity 来集成 Siri、Handoff 或 Spotlight,你需要处理用户继续进行的 activity。

同样,新的应用生命周期模型通过提供两个 modifier 使你更容易实现这一点。这些 modifier 使你可以声明 activity 并让用户可以继续进行它们。

下面是一个展现如何声明 activity 的代码片段。在一个具体的视图里:

struct ColorDetailsView: View {
var color: String

var body: some View {
Image(color)
// ...
.userActivity("showColor") { activity in
activity.title = color
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
// ...
}
}
}

为了允许继续进行这个 activity,你可以在最顶层的导航视图中注册 onContinueUserActivity 闭包,如下所示:

import SwiftUI

struct ContentView: View {
var colors = ["Red", "Green", "Yellow", "Blue", "Pink", "Purple"]

@State var selectedColor: String? = nil

var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(colors, id: \.self) { color in
NavigationLink(destination: ColorDetailsView(color: color),
tag: color,
selection: $selectedColor) {
Image(color)
}
}
}
.onContinueUserActivity("showColor") { userActivity in
if let color = userActivity.userInfo?["colorName"] as? String {
selectedColor = color
}
}
}
}
}
}

请帮帮我 —— 上述的那些对我都不管用!

新的应用声明周期(截止当前)并非支持 AppDelegate 的所有回调函数。如果上述这些都不满足你的需求,你可能还是需要一个 AppDelegate

另一个需要 AppDelegate 的原因是你使用的第三方 SDK 会使用 method swizzling 来把它们注入应用生命周期。Firebase 就是一个典型的例子

为了帮助上述情况中的你摆脱困境,Swift 提供了一种将 AppDelegate 的一个实现类与你的 App 实现相连接的方法:@UIApplicationDelegateAdaptor。使用方法如下:

class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("Colors application is starting up. ApplicationDelegate didFinishLaunchingWithOptions.")
return true
}
}

@main
struct ColorsApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

var body: some Scene {
WindowGroup {
ContentView()
}
}
}

如果你是在复制现有的 AppDelegate 实现,不要忘记删除 @main 属性 —— 不然,编译器该向你抱怨存在多个应用入口了。

总结

至此,让我们讨论一下苹果为什么要进行这些改变。我觉得有以下的几个原因:

SE-0281 explicitly states that one of the design goals was “to offer a more general purpose and lightweight mechanism for delegating a program’s entry point to a designated type.”

苹果选择的基于 DSL 来处理应用生命周期的方法和 SwiftUI 的声明式 UI 搭建方法相契合。两者采用相同的概念可以更方便新加入的开发者们理解。

声明式方法的主要好处是:框架/平台将替代开发者承受实现特定功能的负担。如果需要进行任何更改,这种模式可以在不破坏许多开发人员的应用的情况下进行发布,这也使发布更改变得更容易 —— 理想情况下,开发人员无需更改其实现,因为框架将把一切都搞定。

总体而言,新的应用生命周期模型使实现应用程序的启动更加简单。你的代码将变得更加简洁,更易于维护 —— 要我说,这总是一件好事。

我希望本文能帮你了解新的应用生命周期的来龙去脉。如果你有关于本文的任何疑问或评论,欢迎在 Twitter 上关注并私信我,或者在 GitHub 上的样例项目中提 issue。

感谢你的阅读!

扩展阅读

想了解更多,请查看下面的这些资料:

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

收起阅读 »

SwiftUI 实现侧滑菜单 Side Menu

SwiftUI 实现侧滑菜单 Side Menu 效果 代码 代码里都有相关注释 源码 github 链接:gist.github.com/RandyWei/05… // // ContentView.swift // SiderMenuDemo01 ...
继续阅读 »

SwiftUI 实现侧滑菜单 Side Menu


效果


iShot2021-09-08 09.43.45.gif


代码


代码里都有相关注释


源码 github 链接:gist.github.com/RandyWei/05…



//
// ContentView.swift
// SiderMenuDemo01
//
// Created by RandyWei on 2021/9/7.
//

import SwiftUI

struct ContentView: View {

//划动偏移量
@GestureState var offset:CGFloat = 0

//滑动应该停留在某个点
//停留点: 屏幕宽度的3/5
let maxOffset:CGFloat = UIScreen.main.bounds.width * 3 / 5

//滑动展开之后的 offset
@State var expandOffset:CGFloat = 0

//回弹点:最大停留点/2
private var springOffset:CGFloat{
maxOffset / 2
}
//缩放比例,默认是1
@State private var scaleRatio:CGFloat = 1

//最小 可缩放值
let minScale:CGFloat = 0.9


private var dragGesture: some Gesture {
DragGesture()
.updating($offset, body: { value, out, _ in
//判断是否反向滑动,如果是展开状态需要反向滑动
if value.translation.width >= 0 || expandOffset != 0 {
out = value.translation.width
}
})
.onChanged { value in
//为了顺畅给缩放增加过渡
if value.translation.width >= 0 {
//对缩放比例进行计算:缩放值 = 划动比例 * 可缩放值(1-minScale)
//因为是往小了缩,所以是1-缩放值
scaleRatio = 1 - (value.translation.width / maxOffset) * (1 - minScale)
} else {
//反向value.translation.width是负数 ,所以+maxOffset变为正值
scaleRatio = 1 - ((maxOffset + value.translation.width) / maxOffset) * (1 - minScale)
}
}
.onEnded { value in
//需要判断滑动是否超过某个点来决定是重置还是停留
if value.translation.width >= springOffset {
expandOffset = maxOffset
//停止后,缩小 到0.9
scaleRatio = minScale
} else {
expandOffset = 0
scaleRatio = 1
}
}
}

var body: some View {

ZStack{

//侧边菜单层
SideMenuView()

//功能区域
FeatureView()
.offset(x: offset + expandOffset)
.scaleEffect(scaleRatio)
.animation(.easeInOut(duration: 0.05))
.gesture(dragGesture)


}

}
}

struct FeatureView:View {

var body: some View{

GeometryReader{proxy in
VStack{
HStack{
Image(systemName: "list.dash")
.resizable()
.frame(width: 20, height: 20, alignment: .center)

Text("功能区域")
.font(.title)

Spacer()
}

ScrollView(.vertical, showsIndicators: false, content: {

VStack{

ForEach(0..<50){_ in

HStack{

Image(systemName: "person")
.resizable()
.frame(width: 80, height: 80, alignment: .center)

VStack(alignment: .leading){
Text("titletitletitletitletitle")
.font(.title)

Spacer()

Text("bodybodybodybodybodybody")
.font(.body)
}

}

}.redacted(reason: .placeholder)
}

})
}
.padding(.horizontal)
.padding(.top, 8 + proxy.safeAreaInsets.top)
.frame(maxWidth:.infinity,maxHeight: .infinity,alignment: .topLeading)
.background(Color.white)
.cornerRadius(30)
.shadow(radius: 10)
.ignoresSafeArea()
}

}
}

struct SideMenuView:View {
var body: some View{

GeometryReader{proxy in
VStack(alignment:.leading){
//祖传头像
Image("avatar")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100, alignment: .center)
.clipShape(Circle())

Text("韦爵爷")
.font(.title)

Text("这个人很懒,什么都没留下")

//菜单

HStack{
Image(systemName: "archivebox")
Text("菜单一")
}
.padding(.top)

HStack{
Image(systemName: "note.text")
Text("菜单二")
}
.padding(.top)


HStack{
Image(systemName: "gearshape")
Text("个人设置")
}
.padding(.top)

Spacer()

HStack{
Image(systemName: "signature")
Text("退出登录")
}
.padding(.top)

}
.foregroundColor(.white)
.padding(.horizontal)
.padding(.top, 8 + proxy.safeAreaInsets.top)
.padding(.bottom, 8 + proxy.safeAreaInsets.bottom)
.frame(maxWidth:.infinity,maxHeight: .infinity,alignment: .topLeading)
.background(Color.orange)
.ignoresSafeArea()
}

}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

相关视频

Swift UI侧滑菜单Side Menu-哔哩哔哩


作者:RandyWei
链接:https://juejin.cn/post/7005374220360220702

收起阅读 »

聊聊 Combine 和 async/await 之间的合作

iOS
在 Xcode 13.2 中,苹果完成了 async/await 的向前部署(Back-deploying)工作,将最低的系统要求降低到了 iOS 13(macOS Catalina),这一举动鼓舞了越来越多的人开始尝试使用 async/await 进行开发。...
继续阅读 »

在 Xcode 13.2 中,苹果完成了 async/await 的向前部署(Back-deploying)工作,将最低的系统要求降低到了 iOS 13(macOS Catalina),这一举动鼓舞了越来越多的人开始尝试使用 async/await 进行开发。当大家在接触了异步序列(AsyncSequence)后,会发现它同 Combine 的表现有些接近,尤其结合近两年 Combine 框架几乎没有什么变化,不少人都提出了疑问:苹果是否打算使用 AsyncSequence 和 AsyncStream 替代 Combine。

恰巧我在最近的开发中碰到了一个可能需要结合 Combine 和 async/await 的使用场景,通过本文来聊聊 Combine 和 async/await 它们之间各自的优势、是否可以合作以及如何合作等问题。

原文发表在我的博客 wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】

需要解决的问题

在最近的开发中,我碰到了这样一个需求:

  • 在 app 的生命周期中,会不定期的产生一系列事件,事件的发生频率不定、产生的途径不定
  • 对每个事件的处理都需要消耗不小的系统资源,且需要调用系统提供的 async/await 版本的 API
  • app 对事件的处理结果时效性要求不高
  • 需要限制事件处理的系统消耗,避免同时处理多个事件
  • 不考虑使用 GCD 或 OperationQueue

对上述的需求稍加分析,很快就可以确立解决问题的方向:

  • Combine 在观察和接收事件方面表现的非常出色,应该是解决需求第一点的不二人选
  • 在解决方案中必然会使用到 async/await 的编程模式

需要解决的问题就只剩下两个:

  • 如何将事件处理串行化(必须处理完一个事件后才能处理下一个事件)
  • 如何将 Combine 和 async/await 结合使用

Combine 和 AsyncSequence 之间的比较

由于 Combine 同 AsyncSequence 之间存在不少相似之处,有不少开发者会认为 AsyncSequence 可能取代 Combine,例如:

  • 两者都允许通过异步的方式处理未来的值
  • 两者都允许开发者使用例如 map、flatMap 等函数对值进行操作
  • 当发生错误时,两者都会结束数据流

但事实上,它们之间还是有相当的区别。

事件的观察与接收

Combine 是为响应式编程而生的工具,从名称上就可以看出,它非常擅长将不同的事件流进行变形和合并,生成新的事件流。Combine 关注于对变化的响应。当一个属性发生变化,一个用户点击了按钮,或者通过 NotificationCenter 发送了一个通知,开发者都可以通过 Combine 提供了的内置工具做出及时处理。

通过 Combine 提供的 Subject(PassthroughSubject、CurrentValueSubject),开发者可以非常方便的向数据流中注入值,当你的代码是以命令式风格编写的时候,Subject 就尤为显得有价值。

在 async/await 中,通过 AsyncSequence,我们可以观察并接收网络流、文件、Notification 等方面的数据,但相较于 Combine,仍缺乏数据绑定以及类似 Subject 的数据注入能力。

在对事件的观察与接收方面,Combine 占有较大优势。

关于数据处理、变形的能力

仅从用于数据处理、变形的方法数量上来看,AsyncSequence 相较 Combine 还是有不小的差距。但 AsyncSequence 也提供了一些 Combine 尚未提供,且非常实用的方法和变量,例如:characters、lines 等。

由于侧重点不同,即使随着时间的推移两者增加了更多的内置方法,在数据处理和变形方面也不会趋于一致,更大的可能性是不断地在各自擅长的领域进行扩展。

错误处理方式

在 Combine 中,明确地规定了错误值 Failure 的类型,在数据处理链条中,除了要求 Output 数据值类型一致外,还要求错误值的类型也要相互匹配。为了实现这一目标,Combine 提供了大量的用于处理错误类型的操作方法,例如:mapError、setFailureType、retry 等。

使用上述方法处理错误,可以获得编译器级别的保证优势,但在另一方面,对于一个逻辑复杂的数据处理链,上述的错误处理方式也将导致代码的可读性显著下降,对开发者在错误处理方面的掌握要求也比较高。

async/await 则采用了开发者最为熟悉的 throw-catch 方式来进行错误处理。基本没有学习难度,代码也更符合大多数人的阅读习惯。

两者在错误处理上功能没有太大区别,主要体现在处理风格不同。

生命周期的管理

在 Combine 中,从订阅开始,到取消订阅,开发者通过代码可以对数据链的生命周期做清晰的定义。当使用 AsyncSequence 时,异步序列生命周期的表述则没有那么的明确。

调度与组织

在 Combine 中,开发者不仅可以通过指定调度器(scheduler),显式地组织异步事件的行为和地点,而且 Combine 还提供了控制管道数量、调整处理频率等多维度的处理手段。

AsyncSequence 则缺乏对于数据流的处理地点、频率、并发数量等控制能力。

下文中,我们将尝试解决前文中提出的需求,每个解决方案均采用了 Combine + async/await 融合的方式。

方案一

在 Combine 中,可以使用两种手段来限制数据的并发处理能力,一种是通过设定 flatMap 的 maxPublishers,另一种则是通过自定义 Subscriber。本方案中,我们将采用 flatMap 的方式来将事件的处理串行化。

在 Combine 中调用异步 API,目前官方提供的方法是将上游数据包装成 Future Publisher,并通过 flatMap 进行切换。

在方案一中,通过将 flatMap、Deferred(确保只有在订阅后 Future 才执行)、Future 结合到一起,创建一个新的 Operator,以实现我们的需求。

public extension Publisher {
func task<T>(maxPublishers: Subscribers.Demand = .unlimited,
_ transform: @escaping (Output) async -> T) -> Publishers.FlatMap<Deferred<Future<T, Never>>, Self> {
flatMap(maxPublishers: maxPublishers) { value in
Deferred {
Future { promise in
Task {
let output = await transform(value)
promise(.success(output))
}
}
}
}
}
}

public extension Publisher where Self.Failure == Never {
func emptySink() -> AnyCancellable {
sink(receiveValue: { _ in })
}
}

鉴于篇幅,完整的代码(支持 Error、SetFailureType)版本,请访问 Gist,本方案的代码参考了 Sundell 的 文章

使用方法如下:

var cancellables = Set<AnyCancellable>()

func asyncPrint(value: String) async {
print("hello \(value)")
try? await Task.sleep(nanoseconds: 1000000000)
}

["abc","sdg","353"].publisher
.task(maxPublishers:.max(1)){ value in
await asyncPrint(value:value)
}
.emptySink()
.store(in: &cancellables)
// Output
// hello abc
// 等待 1 秒
// hello sdg
// 等待 1 秒
// hello 353

假如将将上述代码中的["abc","sdg","353"].publisher更换成 PassthoughSubject 或 Notification ,会出现数据遗漏的情况。这个状况是因为我们限制了数据的并行处理数量,从而导致数据的消耗时间超过了数据的生成时间。需要在 Publisher 的后面添加 buffer,对数据进行缓冲。

let publisher = PassthroughSubject<String, Never>()
publisher
.buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest) // 缓存数量和策略根据业务的具体情况确定
.task(maxPublishers: .max(1)) { value in
await asyncPrint(value:value)
}
.emptySink()
.store(in: &cancellables)

publisher.send("fat")
publisher.send("bob")
publisher.send("man")

方案二

在方案二中,我们将采用的自定义 Subscriber 的方式来限制并行处理的数量,并尝试在 Subscriber 中调用 async/await 方法。

创建自定义 Subscriber:

extension Subscribers {
public class OneByOneSink<Input, Failure: Error>: Subscriber, Cancellable {
let receiveValue: (Input) -> Void
let receiveCompletion: (Subscribers.Completion<Failure>) -> Void

var subscription: Subscription?

public init(receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Input) -> Void) {
self.receiveCompletion = receiveCompletion
self.receiveValue = receiveValue
}

public func receive(subscription: Subscription) {
self.subscription = subscription
subscription.request(.max(1)) // 订阅时申请数据量
}

public func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
return .max(1) // 数据处理结束后,再此申请的数据量
}

public func receive(completion: Subscribers.Completion<Failure>) {
receiveCompletion(completion)
}

public func cancel() {
subscription?.cancel()
subscription = nil
}
}
}

receive(subscription: Subscription)中,使用subscription.request(.max(1))设定了订阅者订阅时请求的数据量,在receive(_ input: Input)中,使用return .max(1)设定了每次执行完receiveValue方法后请求的数据量。通过上述方式,我们创建了一个每次申请一个值,逐个处理的订阅者。

但当我们在receiveValue方法中使用 Task 调用 async/await 代码时会发现,由于没有提供回调机制,订阅者将无视异步代码执行完成与否,调用后直接会申请下一个值,这与我们的需求不符。

在 Subscriber 中可以通过多种方式来实现回调机制,例如回调方法、Notification、@Published 等。下面的代码中我们使用 Notification 进行回调通知。

public extension Subscribers {
class OneByOneSink<Input, Failure: Error>: Subscriber, Cancellable {
let receiveValue: (Input) -> Void
let receiveCompletion: (Subscribers.Completion<Failure>) -> Void

var subscription: Subscription?
var cancellable: AnyCancellable?

public init(notificationName: Notification.Name,
receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Input) -> Void) {
self.receiveCompletion = receiveCompletion
self.receiveValue = receiveValue
cancellable = NotificationCenter.default.publisher(for: notificationName, object: nil)
.sink(receiveValue: { [weak self] _ in self?.resume() })
// 在收到回调通知后,继续向 Publisher 申请新值
}

public func receive(subscription: Subscription) {
self.subscription = subscription
subscription.request(.max(1))
}

public func receive(_ input: Input) -> Subscribers.Demand {
receiveValue(input)
return .none // 调用函数后不继续申请新值
}

public func receive(completion: Subscribers.Completion<Failure>) {
receiveCompletion(completion)
}

public func cancel() {
subscription?.cancel()
subscription = nil
}

private func resume() {
subscription?.request(.max(1))
}
}
}

public extension Publisher {
func oneByOneSink(
_ notificationName: Notification.Name,
receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Output) -> Void
) -> Cancellable {
let sink = Subscribers.OneByOneSink<Output, Failure>(
notificationName: notificationName,
receiveCompletion: receiveCompletion,
receiveValue: receiveValue
)
self.subscribe(sink)
return sink
}
}

public extension Publisher where Failure == Never {
func oneByOneSink(
_ notificationName: Notification.Name,
receiveValue: @escaping (Output) -> Void
) -> Cancellable where Failure == Never {
let sink = Subscribers.OneByOneSink<Output, Failure>(
notificationName: notificationName,
receiveCompletion: { _ in },
receiveValue: receiveValue
)
self.subscribe(sink)
return sink
}
}

调用:

let resumeNotification = Notification.Name("resume")

publisher
.buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest)
.oneByOneSink(
resumeNotification,
receiveValue: { value in
Task {
await asyncPrint(value: value)
NotificationCenter.default.post(name: resumeNotification, object: nil)
}
}
)
.store(in: &cancellables)

由于需要回调才能完成整个处理逻辑,针对本文需求,方案一相较方案二明显更优雅。

方案二中,数据处理链是可暂停的,很适合用于需要触发某种条件才可继续执行的场景。

方案三

在前文中提到过,苹果已经为 Notification 提供了 AsyncSequence 的支持。如果我们只通过 NotificationCenter 来发送事件,下面的代码就直接可以满足我们的需求:

let n = Notification.Name("event")
Task {
for await value in NotificationCenter.default.notifications(named: n, object: nil) {
if let str = value.object as? String {
await asyncPrint(value: str)
}
}
}

NotificationCenter.default.post(name: n, object: "event1")
NotificationCenter.default.post(name: n, object: "event2")
NotificationCenter.default.post(name: n, object: "event3")

简单的难以想象是吗?

遗憾的是,Combine 的 Subject 和其他的 Publishe 并没有直接遵循 AsyncSequence 协议。

但今年的 Combine 为 Publisher 增加了一个非常小但非常重要的功能——values。

values 的类型为 AsyncPublisher,其符合 AsyncSequence 协议。设计的目的就是将 Publisher 转换成 AsyncSequence。使用下面的代码便可以满足各种 Publisher 类型的需求:

let publisher = PassthroughSubject<String, Never>()
let p = publisher
.buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest)
Task {
for await value in p.values {
await asyncPrint(value: value)
}
}

因为 AsyncSequence 只能对数据逐个处理,因此我们无需再考虑数据的串行问题。

将 Publisher 转换成 AsyncSequence 的原理并不复杂,创建一个符合 AsyncSequence 的结构,将从 Publihser 中获取的数据通过 AsyncStream 转送出去,并将迭代器指向 AsyncStream 的迭代器即可。

我们可以用代码自己实现上面的 values 功能。下面我们创建了一个 sequence,功能表现同 values 类似。

public struct CombineAsyncPublsiher<P>: AsyncSequence, AsyncIteratorProtocol where P: Publisher, P.Failure == Never {
public typealias Element = P.Output
public typealias AsyncIterator = CombineAsyncPublsiher<P>

public func makeAsyncIterator() -> Self {
return self
}

private let stream: AsyncStream<P.Output>
private var iterator: AsyncStream<P.Output>.Iterator
private var cancellable: AnyCancellable?

public init(_ upstream: P, bufferingPolicy limit: AsyncStream<Element>.Continuation.BufferingPolicy = .unbounded) {
var subscription: AnyCancellable?
stream = AsyncStream<P.Output>(P.Output.self, bufferingPolicy: limit) { continuation in
subscription = upstream
.sink(receiveValue: { value in
continuation.yield(value)
})
}
cancellable = subscription
iterator = stream.makeAsyncIterator()
}

public mutating func next() async -> P.Output? {
await iterator.next()
}
}

public extension Publisher where Self.Failure == Never {
var sequence: CombineAsyncPublsiher<Self> {
CombineAsyncPublsiher(self)
}
}

完整代码,请参阅 Gist,本例的代码参考了 Marin Todorov 的 文章

sequence 在实现上和 values 还是有微小的不同的,如果感兴趣的朋友可以使用下面的代码,分析一下它们的不同点。

let p = publisher
.print() // 观察订阅器的请求情况。 values 的实现同方案二一样。
// sequence 使用了 AsyncStream 的 buffer,因此无需再设定 buffer

for await value in p.sequence {
await asyncPrint(value: value)
}

总结

在可以预见的未来,苹果一定会为 Combine 和 async/await 提供更多的预置融合手段。或许明后年,前两种方案就可以直接使用官方提供的 API 了。

希望本文能够对你有所帮助。

原文发表在我的博客 wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】

收起阅读 »

[翻译]你不可错过的 10 个 Xcode 技巧和快捷键

iOS
原文地址:10 Tips and Shortcuts You Should Be Using Right Now in Xcode 原文作者:Mike Pesate 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m… 译者:F...
继续阅读 »


你不可错过的 10 个 Xcode 技巧和快捷键


Image source: Author


在我作为 iOS 开发人员的职业生涯中,养成了一些使得工作变得更加轻松快捷的 Xcode 习惯。很多好用的快捷键一直都存在,只是我们没有发现而已。


所以我收集了一些我最喜欢的,在这里和大家分享。


我们开始吧!


1. 快速自动缩进


当你的代码没有对齐时,这个快捷键非常有用。



control + i / ⌃ + i



它会自动缩进光标所在的行。如果你选中了一些代码,甚至整个文件,这个快捷键就会调整选中部分的缩进。


Demo of ⌃ + i


这对及时保持代码整洁非常有帮助。


2. 在所有作用域中修改


假设你发现某个方法或变量名有错误,你想要修复它。当然你不会一个个去修改,因为你知道有重构(Refactor)功能可以批量重命名,但有时候 Xcode 的重构功能可能不太靠谱。


此时你可以使用以下快捷键,选中当前文件中所有用到该变量的位置。



command + control + e / ⌘ + ⌃ + e



这将选中所有用到这个变量的位置,让你可以非常方便地更改变量名。


Demo of ⌘ + ⌃ + e


3. 查找下一个


现在,假设你不想在所有作用域中修改变量名称,而只想找到下一处;或者只想在一个函数中重命名,而不是整个类中,或者其他类似情况。有一个(和上面)非常相似的快捷键。



option + control + e / ⌥ + ⌃ + e



Demo of ⌥ + ⌃ + e


当你选中某个字符串,按下这个快捷键,Xcode 将选中下一个出现该字符串的位置。但这意味着,如果某些变量和函数同名,则下一个选中的,也许和你预期的不一样。(译注:这里指的是,并不判断是否真的是同一个变量,只是单纯的字符串匹配)。


4. 查找上一个


上面我们介绍了“查找下一个”,再多按一个键,则变成了“查找上一个”。



shift + option + control + e / ⇧ + ⌥ + ⌃ + e



Demo of ⇧ + ⌥ + ⌃ + e


5. 整行向上或向下移动


我们可能会对代码进行一些顺序调整,当然可以用经典的“剪切粘贴”,但如果我们只想将代码向上移动一行或向下移动一行,那么以下快捷键肯定会对你有所帮助。


向上移动:



option + command + [ / ⌥ + ⌘ + [



向下移动:



option + command + ] / ⌥ + ⌘ + ]



Demo of ⌥ + ⌘ + [ and ⌥ + ⌘ + ]


额外提示!你可以移动多行


如果选中多行之后再使用前面的快捷键,那么这些行将作为一个整体进行移动。


Demo of previous shortcut moving several lines as block


6. 多行光标(使用鼠标)


有时你需要在文件的不同部分中写入相同的内容,你很烦恼,因为你必须编写一次并复制粘贴几次。好吧,别再烦了。你可以使用一个快捷键同时写入多行。



shift + control + click / ⇧ + ⌃ + click



Demo of ⇧ + ⌃ + click


7. 多行光标(使用键盘)


此快捷键与上一个基本相同,但是我们不是使用鼠标来选择光标的位置,而是使用箭头向上或向下来移动光标。



shift + control + up or down /⇧ + ⌃ + ↑ or ↓




8. 快速创建带有多个参数的初始化(init)函数


上面的快捷键,我最喜欢用法之一,就是快速创建一个初始化函数,比之前的任何方法都快。



通过使用多行光标,配合其他一些快捷键,例如复制粘贴或选中整行,我们可以快速创建初始化函数。这只是这个按键的几种用途之一。


8.1 另一种方式


还有一个编辑功能,可以让你轻松地生成 “成员初始化器”(Memberwise Initializer)。你可以将光标放在类的名称上,然后找到 Editor > Refactor > Generate Memberwise Initializer。


但是,由于本文介绍快捷键,所以这里给一个小提示:可以进入 Preferences > Key Bindings,再查找对应命令,并添加快捷键。


这是操作示例:


How to add a key binding


9. 返回光标之前所在的位置


有时候你需要处理很大的文件,向上滚动查看某些内容之后,可能很难找到原来位置。有了这个快捷键,只要我们没有将光标移开,我们就可以快速跳回之前的位置。



option + command + L / ⌥ + ⌘ + L



Demo of ⌥ + ⌘ + L


10. 跳到某一行


和上一条相关,如果我们知道要跳转的那一行的行号,那么使用此快捷键,我们可以直接跳到该行。



command + L / ⌘ + L



Demo of ⌘ + L


最后的想法


这些就是我每天用来高效使用 Xcode 的十个快捷键和技巧。他们经常会派上用场。


我希望他们对你也一样有用。


如果你已经知道了这些快捷键,或者还不知道,都可以与我交流,我会很高兴。也欢迎和我分享你用到的其他有用的快捷键。


小贴士


理想情况下,你可以使用同样的快捷键,来实现前面提到的所有技巧。但是也可能取决于你的操作系统语言设置,其中一些可能略有不同。


你可以在 Xcode > Preferences… > Key Bindings 中查看特定快捷键的按键组合。


额外提示! 快速打开偏好设置(Preferences)



command + , / ⌘ + ,




如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。







作者:FranzWang
收起阅读 »

Xcode 13 更新了哪些内容

iOS
直接进入主题。外观对比 Xcode 12,风格和显示都发生了变化:去掉了文件拓展名图标也可以识别文件类型自动调整了导航栏布局重新进行了分布和调整右下角增加了光标所在行列数文件拓展名设置:打开 设置 - 通用 选择 Fil...
继续阅读 »

直接进入主题。

外观

005XGWPvly1gun3djnsn4j627y1cku0x02

对比 Xcode 12,风格和显示都发生了变化:

  • 去掉了文件拓展名
  • 图标也可以识别文件类型自动调整了
  • 导航栏布局重新进行了分布和调整
  • 右下角增加了光标所在行列数
文件拓展名设置:

打开 设置 - 通用 选择 File Extensions

image-20210920152905766

文件拓展名的显示隐藏控制,选项有三种:

image-20210920153816172

  • Hide All:隐藏全部拓展名

  • Show All:显示全部拓展名

  • Show Only:自定义显示拓展名 ↓↓↓↓

    QQ20210921-024658-HD

    问题提醒设置:

    在 设置 - 通用 里还多了一个 Xcode 12没有的选项:Issues,对应的子选项为:Show InlineShow Minimized

    Show InlineShow Minimized
    image-20210920155859979image-20210920155918641

    对比 Show InlineShow Minimized 把问题提醒最小化到了右侧,当开发者点击对应的问题时,会显示出来。

    优点一目了然,界面整洁,没有一堆提示文字和红蓝色。

    缺点则是无法直接的查看问题原因,即使是点击出来,也没有像前者那样直接的把问题精准的定位具体代码中。

    image-20210920160709412

    不过这个也不能够算是缺点,只是说提示的没有那么的明显,这点根据个人喜好选择就行。

    info.plist

    info.plist 文件内容减少,甚至使用 SwiftUI 创建项目,已经移除了 info.plist 文件,真是把简洁做到了极致

    Storyboard 创建SwiftUI 创建
    image-20210921172421609image-20210921173002487

    当然,只是当前 info.plist 文件没有显示之前的内容,在 Project - Target - Info 下,对应的信息还是存在的,且如果你在 info.plist 文件内新增了的话,依旧会在 Project - Target - Info 下显示出来的。

    至于 SwiftUI 下没有 info.plist 文件,开发者可以自行创建,具体方式可以看这里

    我不能接受

    有一个地方的改变我不能接受,那就是:编译成功失败的提醒框没有啦

    Xcode 12Xcode13
    005XGWPvly1gun4lqd89kg60ig0b80vi02005XGWPvly1gun4lsjs0jg60ig0b841m02

    Xcode 13 中,不管是编译还是运行,都没有了最后的提示框。在设置中也没有找到对应的选项。

    对于我这种经常写 Bug 的人来说,看不到弹出来的 Build Succeeeded,简直是要命。苹果你赶紧给我改回来...

更新:

评论区小伙伴给出解决方案:通知栏会提示编译成功或者失败的提示。

感谢指出,Xcode 13 版本之前也有这个提示, 我一直都忽略了这个地方,平时都把大多数应用的通知都给关了。

让我意外的是:我自己的笔记本 设置 - 通知 里面竟然没有找到 Xcode 这个应用。。。我又不会玩了~

自动补全

import

在开发过程中,经常会出现没有导入头文件就开始直接调用文件,这个时候就会比较尴尬,特别是当代码行数比较多的时候,要先回到顶部导入头文件,再回来继续写,有时候甚至都找不到刚才的位置在哪了...

Xcode 13 解决了这个问题,当你使用一个没有导入头文件的库时,会智能帮助你导入对应的头文件,非常 nice

QQ20210920-163257-HD

switch

Xcode 13 以前,使用 switch 调用枚举的时候,如果想快速调出全部的 case,就只能输入代码后等着 Xcode 给你提示 Switch must be exhaustive 然后 Fix 加载全部的 case

QQ20210920-172130-HD

Xcode 13 中,你是需要正常输入代码,就会自动的显示出来了

QQ20210920-171833-HD

摸鱼的时间又增加了

不过并不是所有的情况都支持,在使用接口请求的时候,回调的Result 类型目前就无法自动补全,只能手动输入。不知道是苹果故意为之还是。。。。

QQ20210920-172656-HD

if / guard let

Xcode 13 中,使用 if 、guard 判断一个 Optional 参数的时候,也会同名自动补全。就很舒服

QQ20210920-173543-HD

for

使用 for...in 循环语句遍历一个数组的时候,Xcode 13 会根据数组名自动生成子元素名自动补全循环

QQ20210920-175028-HD

当然,即使你输入的数组名不是那么的标准,Xcode 也还是会根据它自动识别的进行补全,比如:如果你的数组名是 number 而不是 numbers 的时候,Xcode 的自动补全依旧是 for number in number。所以,还是尽量保证代码命名的正确性吧。

列断点

Swift 链式语法在开发过程中会使代码变得非常美观和整洁,与之带来的部分问题也会出现,就是无法直观的看到每块代码的具体值,每次想查看的时候只能通过声明一个新的变量来赋值查看,这很不Swift

Xcode 13 可以使用给每行代码的任意位置设置断点,通过打印日志来查看详细内容。

可能对于这个 列断点 描述的不是太清楚。可以通过具体操作来了解。

首先创建 列断点:再所选代码位置右键 - Show Code Actions - Create Column Breakpoint

QQ20210920-182540-HD

列断点 跟之前的 行断点 一样,可以 单击、双击、和拖拽。对应的功能也一样。

运行代码,在断点位置处,通过打印日志查看:

QQ20210920-183129-HD

这个功能增加的蛮不错的。

vim

现在你可以从 Xcode 13 中使用 vim 模式来编写代码了。

beta 5 版本中,通过 Editor - Vim Mode 来开启和关闭 vim 模式了。

开启后,Xcode 底部会有对应的快捷键提示。非常友好

image-20210921022842159

其他

除了以上这些之外,Xcode 13 还增加和完善了很多的功能,比如:优化了版本控制功能、新增了 Xcode Cloud 和可以直接在 Xcode 中构件展示官方文档了等等..

image-20210921024217111

更多更详细的内容就需要各位开发者自己亲自去研究和探索了。

总结

相对于之前的版本来说,Xcode 13 看起来让人感觉更加的舒服了,不管是文件风格还是展示形式都显得干净简洁。

当然安装包也还是那么大、还是那么的吃内存。无解~

目前使用起来还是比较顺手的,就是赶紧把编译提醒框退回来,不然每次 command + B 后都要网上看,多别扭。

收起阅读 »

升级到xcode13碰到的问题

iOS
经过了半个月的时间, xcode 没有暴露出来大的 BUG , 可以安心的升级了 然后问题来了, 各种适配问题, 开始撸起来 问题 : The Legacy Build System will be removed in a future release...
继续阅读 »

经过了半个月的时间, xcode 没有暴露出来大的 BUG , 可以安心的升级了


Xcode版本


然后问题来了, 各种适配问题, 开始撸起来



  1. 问题


: The Legacy Build System will be removed in a future release. You can configure the selected build system and this deprecation message in File > Workspace Settings.


错误详情


解决方案:



菜单栏 File->Workspace Settings-> BuildSystem

选择使用 New Buile System(Default)


错误详情



  1. 问题, 文件引入问题(Xcode12之前没有问题)



Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/PEDat_wb.bundle':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/WBCloudReflectionFaceVerify_framework/Resources/PEDat_wb.bundle' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/PEDat_wb.bundle'

2) That command depends on command in Target 'dudu' (project 'dudu'): script phase “[CP] Copy Pods Resources”

Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/WBCloudReflectionFaceVerify.bundle':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/WBCloudReflectionFaceVerify_framework/Resources/WBCloudReflectionFaceVerify.bundle' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/WBCloudReflectionFaceVerify.bundle'

2) That command depends on command in Target 'dudu' (project 'dudu'): script phase “[CP] Copy Pods Resources”

Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/detector_wb.bundle':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/WBCloudReflectionFaceVerify_framework/Resources/detector_wb.bundle' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/detector_wb.bundle'

2) That command depends on command in Target 'dudu' (project 'dudu'): script phase “[CP] Copy Pods Resources”

Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/ufa_wb.bundle':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/WBCloudReflectionFaceVerify_framework/Resources/ufa_wb.bundle' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/ufa_wb.bundle'

2) That command depends on command in Target 'dudu' (project 'dudu'): script phase “[CP] Copy Pods Resources”



解决方案:



target->Build Phases

具体如图, 报错的文件就行了



错误详情



  1. 问题: info.plist 文件问题, 由于项目中使用的库手动拉进来的,



Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/Libs/AliyunOSSiOS/Info.plist' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'

2) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/SupportingFiles/Info.plist' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'

3) Target 'dudu' (project 'dudu') has process command with output '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'



解决方案:



直接删除手动拉进来的库里面的 `info.plist` 文件




  1. 项目里面有个 VERSION 的文件, 想不明白这个为啥



Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/VERSION':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/Libs/ST_Mobile/SenseArSourceService/VERSION' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/VERSION'

2) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/Libs/ST_Mobile/VERSION' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/VERSION'



解决方案:



修改一下 `VERSION` 的文件名称: 或者删除文件




  1. 处理了上面的问题, 依旧还是有问题, 接着修改



Multiple commands produce '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist':

1) Target 'dudu' (project 'dudu') has copy command from '/Users/imac/Desktop/dudu_dev/dudu/dudu/SupportingFiles/Info.plist' to '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'

2) Target 'dudu' (project 'dudu') has process command with output '/Users/imac/Library/Developer/Xcode/DerivedData/dudu-awglbcqfapsmfngrajrwibtvgoph/Build/Products/Debug-iphoneos/dudu.app/Info.plist'



解决方案:



1. 找到 Products 文件夹下的 项目, 邮件 show in finder, 然后向上找, 找到 DerivedData 文件下, 删除对用的文件

到这里这个问题应该已经修复了, 如果还有问题, 继续

2. Build Settings 里面 找到 info.plist 文件夹的位置, (先复制一下路径) 删除, 编译一下, 然后再添加上路径

如果还是不行:

3 Build Phases 里面 Copy Bundle Resources 删除 info.plist 文件



错误详情


好了, 到现在, 我的项目已经能正常运行了,


在翻阅的时候发现 JWAutumn 同学的文章 Xcode 13 更新了哪些内容 这个文章已经更新的比较全了, 可以参考一下


不知道有没有碰到其他问题的, 可以私信我一下, 给加在上面, 供大家参考




  1. Xcode 编译结果的提示, 中间的 Build Successed 和 小锤子不出来了, 没找到在哪里能修改的, 只能在软件上面提示, 感觉习惯了小锤子的提示, 这个修改感觉很不友好




  2. XcodeSnippets (就是自定义的代码块) 不自动提示了, 需要将自定义的名称打全才出来提示 , 比如: 设置的 mark 只有全部打出来才提示代码块, 不然不提示, 也是不友好, 目前没发现在哪里可以提示的




  3. 以前修改的文件, 当前文件的名字变灰, 知道编译后有哪些文件修改过了, 现在直接没了, 没找到哪里设置的 (这个修改很不友好)







作者:Keya
链接:https://juejin.cn/post/7016195057266999304

收起阅读 »

Xcode调试技巧总结

iOS
前言 本来觉得调试是一件很简单的事情,但是看了很多介绍调试方法的文章,发现有些技巧并不知道,有必要对常用的Xcode调试技巧做一个总结,提高工作效率。 一、调试面板 上方:断点开关、继续执行、单步执行、单步步入、单步步过等命令; 左边:watch窗口,负责变...
继续阅读 »

前言


本来觉得调试是一件很简单的事情,但是看了很多介绍调试方法的文章,发现有些技巧并不知道,有必要对常用的Xcode调试技巧做一个总结,提高工作效率。


一、调试面板


image.png


上方:断点开关、继续执行、单步执行、单步步入、单步步过等命令;

左边:watch窗口,负责变量信息显示,如果想查看寄存器的内容,可以将左下角的Auto切换为All

右边:日志窗口,接受和显示程序日志,左下角可以选择All/Debugger/Target output


二、断点


1- 普通断点


找到下断点的代码行,可以通过下面3种方式下断点:

(1)导航栏:Debug->Breakpoint->Add Breakpoint at Current Line

(2)快捷键:command +

(3)鼠标:直接在编辑区域左边行号的地方左键


大部分情况普通断点就满足需求了,但是对于一些特殊的调试情况,还需要掌握一些其他类型的断点。


2- 条件断点


适用场景

(1)一个函数重复多次被调用,但是只需要调试其中某一次的情况时;

(2)对于一些因为异常数据导致的bug调试也很实用;

下断点: 右键普通断点 -> Edit Breakpoint,条件断点和普通断点相比只是多了一个条件判断而已,和我们手动在断点代码加一个if条件判断效果一样,只有满足条件的情况才会断下来;


image.png


3- 符号断点


符号断点: 其实就是对一个特定的函数名下断点,这里的方法可以是OC方法或者C++函数名;

适用场景: 调试一些没有源码的模块时比较有用,比如调试一个第三方提供的Lib库,或者系统模块,可以给相应函数下断点,调试程序的运行流程,查看一些参数信息;

下断点 :断点Tab页 -> 点击下面+号 -> Symbolic Breakpoint


image.png


image.png


设置符号断点可以输入类名+函数名,也可以只输入函数名,xcode会自动匹配不同类中同名的方法进行断点,如下DJTPerson和DJTAnimal都有-(void)djt_run方法,会自动生成两个断点,一旦被调用就会命中断点:


image.png


4- 异常断点


适用场景: 异常断点用来调试程序抛出异常而导致退出,下个异常断点很快就能定位运行到那行代码出了问题;

下断点: 断点Tab -> 左下角+号 -> Exception Breakpoint

Exception Breakpoint也是可以编辑的,可以选择Exception类型,也可以选择在抛出异常或者捕获异常的时候断点等;


image.png


注:有的程序会使用异常来组织程序逻辑,比如微信扫一扫,所以如果Exception选了All,那么异常断点会频繁触发,所以这种情况可以只选择Objective-C异常。


下面是一个异常断点,在DJTPerson类中只有djt_run方法的声明没有实现,触发断点:


image.png


5- watch断点


顾名思义:watch断点就是当某个变量发生改变时触发的断点;

适用场景: watch断点对于要跟踪某个变量或者某个状态的变化时非常有用的,可以方便的跟踪到哪些地方改变了变量的值。

下断点: 在xcode的watch窗口 -> 右键需要watch的变量 -> watch "Xxx":


image.png


如例子中,当_name变量发生变化时调试器会自动断下来,同时输出变化信息:
image.png


6- 线程断点


线程断点: 线程断点适用在调试多线程代码的时候,一段代码可能会被多个线程同时执行,如果下普通断点,那么你会在不同线程之间切来切去,最后自己都迷糊了,这个时候可以使用多线程断点。

下断点: 调试区域右边控制台输出 -> breakpoint set –f 文件名 –l 行号 –t 线程id


下面例子在28行设置普通断点,就可以在控制台打印 thread-id,控制台输入:


thread info

获取当前线程id,在控制台通过命令行给32行设置线程断点:


breakpoint set -f ViewController.m -l 32 -t 0x331854

image.png


7- 断点后的Action


断点后的Action: 当断点被触发可以执行的一些操作;

下断点: 右键断点 -> Edit Breakpoint -> Add action


action类型很多,有调试命令、apple script、shell script等:

image.png


下边是在运行到断点后po一下person.name,直接打印了name的值:
image.png


如果觉得仅仅输出对象信息不够,还想加一些自己指定的内容,可以使用Log Message。


image.png


三、常用命令


1- p命令


p命令:查看基本数据类型的值

po命令:查看oc对象

简单查看一个变量或者OC对象的值在watch窗口完全可以满足,但是如果需要查看一个oc对象的属性,或者一个oc对象方法的返回值怎么办呢?p和po命令后面都可以接相应的表达式,如:


image.png


2- expr命令


expr命令:全称expression,可以在调试时动态修改变量的值,同时打印出结果。使用expr命令动态修改变量的值,可以在调试的时候覆盖一些异常路径,对调试异常处理的代码很有用。


image.png


3- call命令


call命令用来动态调用函数,可以在不增加代码不重新编译的情况下动态调用一个方法。下例动态将view1从父view移除:


image.png


4- image命令


image命令可以列出当前app中的所有模块,可以查找一个地址对应的代码位置,在调试越狱插件时,可以用image list命令查看越狱插件是否注入自己的App。当遇到crash时,查看线程栈只能看到栈帧的地址,使用image lookup -address 地址命令可以方便的定位这个地址对应的代码行。


5- bt命令


bt命令可以查看线程的堆栈信息,该信息也可以在导航区的Debug Navigator看到;

bt:打印当前线程栈
bt all:打印所有线程栈


image.png


分割线:上边介绍了基本的调试技巧,下面是一些不同场景下的调试经验


四、多线程


场景:在调试的时候bug不出现,一旦关闭调试直接运行bug就出现:这种问题大部分是因为多线程bug,而调试影响了多线程的执行顺序。

技巧:这种问题可以在关键点输出log,之前介绍的断点action中的Log Message就派上用场,这样的好处是不需要在代码中添加冗余的log即可调试;在调试多线程问题时,合理使用线程断点和条件断点也是很有帮助的;


五、UI调试


1-控件信息


查看控件信息除了使用p和po命令,还可以使用expr命令修改控件属性,如内容、坐标、大小等,这样可以不重启程序看到界面的变化;


2-界面结构


查看界面结构:po [view recursiveDescription],该命令可以打印出view的所有子view的结构关系,对于调试界面层级关系很有用;


3-快速预览


xcode支持在调试时对变量进行快速预览,调试时将鼠标放在变量上,然后点击快速预览按钮即可看到控件的显示。


image.png


4-符号断点跟踪UI变化


对于一些系统控件的信息,如果发现最终显示和自己设置的不一样,可以使用符号断点,在一些设置函数下断点,这样就可以很清晰的看到是从哪里改变了这个属性的值。比如一个UIButton的title在显示的时候和设置的不一样,只需要符号断点设置setTitle就可以跟踪哪里改了值;



作者:D___
链接:https://juejin.cn/post/6950852311346315271

收起阅读 »

黑科技- iOS静态cell和动态cell结合使用

iOS
1. 什么是静态Cell。 静态cell,可以直接布局cell样式的、group、insert group等直接拖@IBOutlet 布局简单,实用,比如我们同一类型的登陆、密码、设置、WIFI等页面 2. 怎么使用静态Cell。 必须使用StoryBo...
继续阅读 »

1. 什么是静态Cell。



  • 静态cell,可以直接布局cell样式的、group、insert group等直接拖@IBOutlet

  • 布局简单,实用,比如我们同一类型的登陆、密码、设置、WIFI等页面


2. 怎么使用静态Cell。



  • 必须使用StoryBoard来创建UITableViewController

  • image-20210823133103767.png

  • 然后你就可以直接使用cell的布局,运行出来就是StoryBoard的布局

  • image-20210823133226364.png

  • 运行以后的效果

  • image-20210823133337424.png


3. 和动态Cell结合。



  • 如例子:Wi-Fi的截图,我的网络和其他网络可以用动态cell来创建、其他的都可以直接用静态cell来创建


image-20210823132756571.png


使用步骤:




  1. 先在StoryBoard创建静态的cell,需要复用的cell留出一个位置即可

  2. 复用的cell必须单独创建(或者使用单独的xib文件)

  3. 这使用的时候,必须注册cell

  4. 动态的cell必须实现UITableViewDelegate的indentationLevelForRowAt这个方法

  5. 在这个方法里indentationLevelForRowAt返回第一个这StoryBoard留出位置的cell的indexPath


详情请看下列代码实现;



image-20210823133226364.png


第二个section 留白的就是给动态cell实现的


image-20210823134208316.png


创建一个xib的动态cell实现(可复用)


// 注册Cell
tableView.register(UINib(nibName: "CostomTableViewCell", bundle: nil), forCellReuseIdentifier: "CostomTableViewCell")


override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
   if indexPath.section == 1 {
       return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1))
   }
   return super.tableView(tableView, indentationLevelForRowAt: indexPath)
}


实现indentationLevelForRowAt方法,返回IndexPath(row: 0, section: 1)第一section 的第一个row,其他不需要复用的自己返回父类即可


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   if indexPath.section == 1 {
       let cell = tableView.dequeueReusableCell(withIdentifier: "CostomTableViewCell", for: indexPath) as! CostomTableViewCell
       cell.dynamic.text = "dynamicRow:(dynamicRowArray[indexPath.row])"
       return cell
   }
   return  super.tableView(tableView, cellForRowAt: indexPath)
}

在cellForRowAt复用里写已经要实现的复用的cell,其他静态cell直接返回父类即可


最终实现的效果


image-20210823134654515.png


4. Row、Section使用的技巧,以及常出现的问题。



  • 如果复用的是row,直接实现indentationLevelForRowAt这个方法和cellForRowAt方法即可 必须实现,不然会崩溃

  • 但是如果是复用的section,就必须实现UITableViewDataSource的和数据源相关的方法以下的几个方法


// 自定义动态section 的时候,以下方法必须实现,否则会崩溃
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
   nil
}

override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
   nil
}

override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
   nil
}

override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
   nil
}

override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
   5
}

override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
   .leastNonzeroMagnitude
}



  • 默认情况下会调用父类的数据,静态cell是不实现这些方法是越界的


注意事项:



  1. indentationLevelForRowAt 这个方法是动态和静态结合必须实现的方法

  2. Row、Section所需要实现的方法有差别,当是Section的时候需要实现与section相关的代理和数据源,例如sectionHeaderView、Footer等

  3. 动态cell一定要在storyboard里留白,自定义需要复用的cell必须使用xib、或者自定义,不能在原有的storyboard里创建

  4. 如果遇到崩溃,大多数是因为数据越界,数据源的问题,如果以上都实现,基本是没有问题的


Demo地址


作者:芭菲猫
链接:https://juejin.cn/post/6999504422065831943

收起阅读 »

std::out_of_range异常

iOS
使用C++容器类访问成员时由于使用问题可能会遇到"terminate called after throwing an instance of 'std::out_of_range'"或者"Abort message: 'terminating with un...
继续阅读 »

使用C++容器类访问成员时由于使用问题可能会遇到"terminate called after throwing an instance of 'std::out_of_range'"或者"Abort message: 'terminating with uncaught exception of type std::out_of_range"。问题的大概意思是:访问越界了。没有捕获std::out_of_range类型的异常终止。

通常在使用vector、map这样的C++容器类型时会遇到,这里我们以map类型为例,加以说明。

std::out_of_range异常的描述

假设我们定义了一个map类型的变量g_mapIsDestroyedRefCount,要访问容器中的数据项有多种方式。例如,获取g_mapIsDestroyedRefCount中key值为cameraId的值,可以这样:

  1. g_mapIsDestroyedRefCount[cameraId]
  2. g_mapIsDestroyedRefCount.at(cameraId)

两种写法都可以获取key为cameraId的value,一般效果看不出来差别,但是当g_mapIsDestroyedRefCount中不存在key为cameraId的<key, value>时就会出现“std::out_of_range”访问越界问题。

导致std::out_of_range的原因

容器类型访问方法使用有问题

对于std::map::at官方声明:

  mapped_type& at (const key_type& k);
const mapped_type& at (const key_type& k) const;

对于std::map::at使用有如下说明: Access element        访问元素

Returns a reference to the mapped value of the element identified with key k.      返回元素键为k的映射值的引用,即Key为k的元素的对应value值。
If k does not match the key of any element in the container, the function throws an out_of_range exception.   如果容器中没有匹配的k键,该函数将抛出一个out_of_range异常

 

std::map::at的使用

  • 正确使用
  • 错误使用

1.std::map::at的正确使用

 

#include <iostream>
#include <string>
#include <map>

std
::map<int, int> g_mapIsDestroyedRefCount;

int main()
{
int cameraId = 1;
cout
<< "Let's try"<< endl;

//向map中添加测试数据
g_mapIsDestroyedRefCount
.insert(std::pair<int, int>(0, 2))'
cout << "cameraId:"<< cameraId<< "count:";
try {
cout<< g_mapIsDestroyedRefCount.at(cameraId) <<endl;
} catch (const std::out_of_range& oor) {
std::cerr << "\nOut of range error:" << oor.what()<< endl;
}
cout << "try done"<< endl;
return 0;
}

 

运行结果:

 

2.std::map::at错误使用

#include <iostream>
#include <string>
#include <map>
using namespace std;

std
::map<int, int> g_mapIsDestroyedRefCount;

int main()
{
int cameraId = 2;

cout
<< "Let's try"<< endl;
g_mapIsDestroyedRefCount
.insert(std::pair<int, int>(0, 2));
cout
<< "cameraId:"<< cameraId<< "count:";

//介绍中说的方法一,可以访问
cout
<< g_mapIsDestroyedRefCount[cameraId]<< endl;
//方法二,异常
cameraId = 2;
count
<< g_mapIsDestroyedRefCount.at(cameraId)<< endl;
cout<< "try done"<< endl;
}

运行结果:(程序异常退出)


收起阅读 »

SwiftUI开发小技巧总结(不定期更新)

iOS
目前SwiftUI还不完善,而且实际使用还会存在一些缺陷。网上的教程目前还很少,有也是收费的。因此特地整理一些平时开发中遇到的问题,免费提供给读者。 (注:本文主要面向对SwiftUI有一定基础的读者。) 调整状态栏样式 StatusBarStyle 尝试In...
继续阅读 »

目前SwiftUI还不完善,而且实际使用还会存在一些缺陷。网上的教程目前还很少,有也是收费的。因此特地整理一些平时开发中遇到的问题,免费提供给读者。


(注:本文主要面向对SwiftUI有一定基础的读者。)


调整状态栏样式 StatusBarStyle


尝试Info.plistUIApplication.statusBarStyle方法无效。如果有UIViewController作为根视图,重写方法preferredStatusBarStyle,这样可以控制全局;如果要设置单个页面的样式用preferredColorScheme(.light),但测试似乎设置无效。还有另一个方法:stackoverflow.com/questions/5…


调整导航栏样式 NavigationBar


let naviAppearance = UINavigationBarAppearance()
naviAppearance.configureWithOpaqueBackground() // 不透明背景样式
naviAppearance.backgroundColor = UIColor.whiteColor // 背景色
naviAppearance.shadowColor = UIColor.whiteColor // 阴影色
naviAppearance.titleTextAttributes = [:] // 标题样式
naviAppearance.largeTitleTextAttributes = [:] // 大标题样式
UINavigationBar.appearance().standardAppearance = naviAppearance
UINavigationBar.appearance().compactAppearance = naviAppearance
UINavigationBar.appearance().scrollEdgeAppearance = naviAppearance
UINavigationBar.appearance().tintColor = UIColor.blackColor // 导航栏按钮颜色


注意configureWithOpaqueBackground()需要在其它属性设置之前调用,除此之外还有透明背景configureWithTransparentBackground(),设置背景模糊效果backgroundEffect(),背景和阴影图片等,以及导航栏按钮样式也可修改。


调整标签栏样式 TabBar


let itemAppearance = UITabBarItemAppearance()
itemAppearance.normal.iconColor = UIColor.whiteColor // 正常状态的图标颜色
itemAppearance.normal.titleTextAttributes = [:] // 正常状态的文字样式
itemAppearance.selected.iconColor = UIColor.whiteColor // 选中状态的图标颜色
itemAppearance.selected.titleTextAttributes = [:] // 选中状态的文字样式
let tabBarAppearance = UITabBarAppearance()
tabBarAppearance.configureWithOpaqueBackground() // 不透明背景样式
tabBarAppearance.stackedLayoutAppearance = itemAppearance
tabBarAppearance.backgroundColor = UIColor.whiteColor // 背景色
tabBarAppearance.shadowColor = UIColor.clear // 阴影色
UITabBar.appearance().standardAppearance = tabBarAppearance


注意configureWithOpaqueBackground()同样需要在其它属性设置之前调用,和UINavigationBarAppearance一样有同样的设置,除此之外还可以为每个标签项设置指示器外观。


标签视图 TabView


设置默认选中页面:方法如下,同时每个标签项需要设置索引值tag()


TabView(selection: $selectIndex, content: {})
复制代码

控制底部标签栏显示和隐藏:


UITabBar.appearance().isHidden = true


NavigationView与TabView结合使用时,进入子页面TabBar不消失问题:不用默认的TabBar,将其隐藏,自己手动实现一个TabBar,放在根视图中。


键盘


输入框获得焦点(弹出键盘):在iOS15上增加了方法focused(),注意这个方法在视图初始化时是无效的,需要在onAppear()中延迟一定时间调用才可以。在此之前的系统只能自定义控件的方法实现参考这个:stackoverflow.com/questions/5…


关闭键盘,两种方法都可以:


UIApplication.shared.keyWindow?.endEditing(true)
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)


添加键盘工具栏:


.toolbar()


手势


获得按下和松开的状态:


.simultaneousGesture(
DragGesture(minimumDistance: 0)
    .onChanged({ _ in })
    .onEnded({ _ in })
)


通过代码滚动ScrollView到指定位置:借助ScrollViewReader可以获取位置,在onAppear()中设置位置scrollTo(),我们实际使用发现,需要做个延迟执行才会有效,可以把执行放在DispatchQueue.main.async()中执行。


TextEditor


修改背景色:


UITextView.appearance().backgroundColor


处理Return键结束编辑:


.onChange(of: text) { value in
if value.last == "\n" {
UIApplication.shared.keyWindow?.endEditing(true)
}
}


Text文本内部对齐方式


multilineTextAlignment(.center)


页面跳转


容易出错的情况:开发中会经常遇到这样的需求,列表中选择一项,进入子页面。点击按钮返回上一页。此时再次点击列表中的某一项,会发现显示的页面内容是错误的。如果你是用NavigationLink做页面跳转,并传递了isActive参数,那么是会遇到这样的问题。原因在于多个页面的使用的是同一个isActive参数。解决办法是,列表中每一项都用独立的变量控制。NavigationView也尽量不要写在TabView外面,可能会导致莫名其妙的问题。


属性包装器在init中的初始化


init方法中直接赋值会发现无法成功,应该用属性包装器自身的方法包装起来,赋值的属性名前加_,例如:


_value = State<Int>(initialValue: 1)
_value = Binding<Bool>.constant(true) // 也可以使用Swift语法特性直接写成.constant(true)


View如何忽略触摸事件


allowsHitTesting(false)

作者:iOS技术小组
链接:https://juejin.cn/post/7037780197076123685

收起阅读 »

设计一套完整的日志系统

iOS
需求日志对于线上排查问题是非常重要的,很多问题其实是很偶现的,同样的系统版本,同样的设备,可能就是用户的复现,而开发通过相同的操作和设备就是不复现。但是这个问题也不能一直不解决,所以可以通过日志的方式排查问题。可能是后台导致的问题,也可能是客户端逻辑问题,在关...
继续阅读 »

需求

日志对于线上排查问题是非常重要的,很多问题其实是很偶现的,同样的系统版本,同样的设备,可能就是用户的复现,而开发通过相同的操作和设备就是不复现。但是这个问题也不能一直不解决,所以可以通过日志的方式排查问题。可能是后台导致的问题,也可能是客户端逻辑问题,在关键点记录日志可以快速定位问题。

假设我们的用户量是一百万日活,其中有1%的用户使用出现问题,即使这个问题并不是崩溃,就是业务上或播放出现问题。那这部分用户就是一万的用户,一万的用户数量是很庞大的。而且大多数用户在遇到问题后,并不会主动去联系客服,而是转到其他平台上。

虽然我们现在有Kibana网络监控,但是只能排查网络请求是否有问题,用户是否在某个时间请求了服务器,服务器下发的数据是否正确,但是如果定位业务逻辑的问题,还是要客户端记录日志。

现状

我们项目中之前有日志系统,但是从业务和技术的角度来说,存在两个问题。现有的日志系统从业务层角度,需要用户手动导出并发给客服,对用户有不必要的打扰。而且大多数用户并不会答应客服的请求,不会导出日志给客服。从技术的角度,现有的日志系统代码很乱,而且性能很差,导致线上不敢持续记录日志,会导致播放器卡顿。

而且现有的日志系统仅限于debug环境开启主动记录,线上是不开启的,线上出问题后需要用户手动打开,并且记录时长只有三分钟。正是由于现在存在的诸多问题,所以大家对日志的使用并不是很积极,线上排查问题就比较困难。

方案设计

思路

正是针对现在存在的问题,我准备做一套新的日志系统,来替代现有的日志系统。新的日志系统定位很简单,就是纯粹的记录业务日志。Crash、埋点这些,我们都不记录在里面,这些可以当做以后的扩展。日志系统就记录三种日志,业务日志、网络日志、播放器日志。

日志收集我们采用的主动回捞策略,在日志平台上填写用户的uid,通过uid对指定设备下发回捞指令,回捞指令通过长连接的方式下发。客户端收到回捞指令后,根据筛选条件对日志进行筛选,随后以天为单位写入到不同的文件中,压缩后上传到后端。

在日志平台可以根据指定的条件进行搜索,并下载文件查看日志。为了便于开发者查看日志,从数据库取出的日志都会写成.txt形式,并上传此文件。

API设计

对于调用的API设计,应该足够简单,业务层使用时就像调用NSLog一样。所以对于API的设计方案,我采用的是宏定义的方式,调用方法和NSLog一样,调用很简单。

#if DEBUG
#define SVLogDebug(frmt, ...) [[SVLogManager sharedInstance] mobileLogContent:(frmt), ##__VA_ARGS__]
#else
#define SVLogDebug(frmt, ...) NSLog(frmt, ...)
#endif

日志总共分为三种类型,业务日志、播放器日志、网络日志,对于三种日志分别对应着不同的宏定义。不同的宏定义,写入数据库的类型也不一样,可以用户日志筛选。

  • 业务日志:SVLogDebug
  • 播放器日志:SVLogDebugPlayer
  • 网络日子:SVLogDebugQUIC

淘汰策略

不光是要往数据库里写,还需要考虑淘汰策略。淘汰策略需要平衡记录的日志数量,以及时效性的问题,日志数量尽量够排查问题,并且还不会占用过多的磁盘空间。所以,在日志上传之后会将已上传日志删除掉,除此之外日志淘汰策略有以下两种。

  1. 日志最多只保存三天,三天以前的日志都会被删掉。在应用启动后进行检查,并后台线程执行这个过程。
  2. 日志增加一个最大阈值,超过阈值的日志部分,以时间为序,从前往后删除。我们定义的阈值大小为200MB,一般不会超过这个大小。

记录基础信息

在排查问题时一些关键信息也很重要,例如用户当时的网络环境,以及一些配置项,这些因素对代码的执行都会有一些影响。对于这个问题,我们也会记录一些用户的配置信息及网络环境,方便排查问题,但不会涉及用户经纬度等隐私信息。

数据库

旧方案

之前的日志方案是通过DDLog实现的,这种方案有很严重的性能问题。其写入日志的方式,是通过NSData来实现的,在沙盒创建一个txt文件,通过一个句柄来向本地写文件,每次写完之后把句柄seek到文件末尾,下次直接在文件末尾继续写入日志。日志是以NSData的方式进行处理的,相当于一直在频繁的进行本地文件写入操作,还要在内存中维持一个或者多个句柄对象。

这种方式还有个问题在于,因为是直接进行二进制写入,在本地存储的是txt文件。这种方式是没有办法做筛选之类的操作的,扩展性很差,所以新的日志方案我们打算采用数据库来实现。

方案选择

我对比了一下iOS平台主流的数据库,发现WCDB是综合性能最好的,某些方面比FMDB都要好,而且由于是C++实现的代码,所以从代码执行的层面来讲,也不会有OC的消息发送和转发的额外消耗。

根据WCDB官网的统计数据,WCDBFMDB进行对比,FMDB是对SQLite进行简单封装的框架,和直接用SQLite差别不是很大。而WCDB则在sqlcipher的基础上进行的深度优化,综合性能比FMDB要高,以下是性能对比,数据来自WCDB官方文档。

单次读操作WCDB要比FMDB5%左右,在for循环内一直读。

15906481049447.jpg

单次写操作WCDB要比FMDB28%,一个for循环一直写。

15906481114970.jpg

批量写操作比较明显,WCDB要比FMDB180%,一个批量任务写入一批数据。

15906481277664.jpg

从数据可以看出,WCDB在写操作这块性能要比FMDB要快很多,而本地日志最频繁的就是写操作,所以这正好符合我们的需求,所以选择WCDB作为新的数据库方案是最合适的。而且项目中曝光模块已经用过WCDB,证明这个方案是可行并且性能很好的。

表设计

我们数据库的表设计很简单,就下面四个字段,不同类型的日志用type做区分。如果想增加新的日志类型,也可以在项目中扩展。因为使用的是数据库,所以扩展性很好。

  • index:主键,用来做索引。
  • content:日志内容,记录日志内容。
  • createTime:创建时间,日志入库的时间。
  • type:日志类型,用来区分三种类型。

数据库优化

我们是视频类应用,会涉及播放、下载、上传等主要功能,这些功能都会大量记录日志,来方便排查线上问题。所以,避免数据库太大就成了我在设计日志系统时,比较看重的一点。

根据日志规模,我对播放、下载、上传三个模块进行了大量测试,播放一天两夜、下载40集电视剧、上传多个高清视频,累计记录的日志数量大概五万多条。我发现数据库文件夹已经到200MB+的大小,这个大小已经是比较大的,所以需要对数据库进行优化。

我观察了一下数据库文件夹,有三个文件,dbshmwal,主要是数据库的日志文件太大,db文件反而并不大。所以需要调用sqlite3_wal_checkpointwal内容写入到数据库中,这样可以减少walshm文件的大小。但WCDB并没有提供直接checkpoint的方法,所以经过调研发现,执行database的关闭操作时,可以触发checkpoint

我在应用程序退出时,监听了terminal通知,并且把处理实际尽量靠后。这样可以保证日志不被遗漏,而且还可以在程序退出时关闭数据库。经过验证,优化后的数据库磁盘占用很小。143,987条数据库,数据库文件大小为34.8MB,压缩后的日志大小为1.4MB,解压后的日志大小为13.6MB

wal模式

这里顺带讲一下wal模式,以方便对数据库有更深入的了解。SQLite3.7版本加入了wal模式,但默认是不开启的,iOS版的WCDBwal模式自动开启,并且做了一些优化。

wal文件负责优化多线程下的并发操作,如果没有wal文件,在传统的delete模式下,数据库的读写操作是互斥的,为了防止写到一半的数据被读到,会等到写操作执行完成后,再执行读操作。而wal文件就是为了解决并发读写的情况,shm文件是对wal文件进行索引的。

SQLite比较常用的deletewal两种模式,这两种模式各有优势。delete是直接读写db-page,读写操作的都是同一份文件,所以读写是互斥的,不支持并发操作。而walappend新的db-page,这样写入速度比较快,而且可以支持并发操作,在写入的同时不读取正在操作的db-page即可。

由于delete模式操作的db-page是离散的,所以在执行批量写操作时,delete模式的性能会差很多,这也就是为什么WCDB的批量写入性能比较好的原因。而wal模式读操作会读取dbwal两个文件,这样会一定程度影响读数据的性能,所以wal的查询性能相对delete模式要差。

使用wal模式需要控制wal文件的db-page数量,如果page数量太大,会导致文件大小不受控制。wal文件并不是一直增加的,根据SQLite的设计,通过checkpoint操作可以将wal文件合并到db文件中。但同步的时机会导致查询操作被阻塞,所以不能频繁执行checkpoint。在WCDB中设置了一个1000的阈值,当page达到1000后才会执行一次checkpoint

这个1000是微信团队的一个经验值,太大会影响读写性能,而且占用过多的磁盘空间。太小会频繁执行checkpoint,导致读写受阻。

# define SQLITE_DEFAULT_WAL_AUTOCHECKPOINT  1000

sqlite3_wal_autocheckpoint(db, SQLITE_DEFAULT_WAL_AUTOCHECKPOINT);

int sqlite3_wal_autocheckpoint(sqlite3 *db, int nFrame){
#ifdef SQLITE_OMIT_WAL
UNUSED_PARAMETER(db);
UNUSED_PARAMETER(nFrame);
#else
#ifdef SQLITE_ENABLE_API_ARMOR
if( !sqlite3SafetyCheckOk(db) ) return SQLITE_MISUSE_BKPT;
#endif
if( nFrame>0 ){
sqlite3_wal_hook(db, sqlite3WalDefaultHook, SQLITE_INT_TO_PTR(nFrame));
}else{
sqlite3_wal_hook(db, 0, 0);
}
#endif
return SQLITE_OK;
}

也可以设置日志文件的大小限制,默认是-1,也就是没限制,journalSizeLimit的意思是,超出的部分会被覆写。尽量不要修改这个文件,可能会导致wal文件损坏。

i64 sqlite3PagerJournalSizeLimit(Pager *pPager, i64 iLimit){
if( iLimit>=-1 ){
pPager->journalSizeLimit = iLimit;
sqlite3WalLimit(pPager->pWal, iLimit);
}
return pPager->journalSizeLimit;
}

下发指令

日志平台

日志上报应该做到用户无感知,不需要用户主动配合即可进行日志的自动上传。而且并不是所有的用户日志都需要上报,只有出问题的用户日志才是我们需要的,这样也可以避免服务端的存储资源浪费。对于这些问题,我们开发了日志平台,通过下发上传指令的方式告知客户端上传日志。

037C8667-914E-43A7-8B6D-7B6EDD80E3A5.png

我们的日志平台做的比较简单,输入uid对指定的用户下发上传指令,客户端上传日志之后,也可以通过uid进行查询。如上图,下发指令时可以选择下面的日志类型和时间区间,客户端收到指令后会根据这些参数做筛选,如果没选择则是默认参数。搜索时也可以使用这三个参数。

日志平台对应一个服务,点击按钮下发上传指令时,服务会给长连接通道下发一个jsonjson中包含上面的参数,以后也可以用来扩展其他字段。上传日志是以天为单位的,所以在这里可以根据天为单位进行搜索,点击下载可以直接预览日志内容。

长连接通道

指令下发这块我们利用了现有的长连接,当用户反馈问题后,我们会记录下用户的uid,如果技术需要日志进行排查问题时,我们会通过日志平台下发指令。

指令会发送到公共的长连接服务后台,服务会通过长连接通道下发指令,如果指令下发到客户端之后,客户端会回复一个ack消息回复,告知通道已经收到指令,通道会将这条指令从队列中移除。如果此时用户未打开App,则这条指令会在下次用户打开App,和长连接通道建立连接时重新下发。

未完成的上传指令会在队列中,但最多不超过三天,因为超过三天的指令就已经失去其时效性,问题当时可能已经通过其他途径解决。

静默push

用户如果打开App时,日志指令的下发可以通过长连接通道下发。还有一种场景,也是最多的一种场景,用户未打开App怎么解决日志上报的问题,这块我们还在探索中。

当时也调研了美团的日志回捞,美团的方案中包含了静默push的策略,但是经过我们调研之后,发现静默push基本意义不大,只能覆盖一些很小的场景。例如用户App被系统kill掉,或者在后台被挂起等,这种场景对于我们来说并不多见。另外和push组的人也沟通了一下,push组反馈说静默push的到达率有些问题,所以就没采用静默push的策略。

日志上传

分片上传

进行方案设计的时候,由于后端不支持直接展示日志,只能以文件的方式下载下来。所以当时和后端约定的是以天为单位上传日志文件,例如回捞的时间点是,开始时间4月21日19:00,结束时间4月23日21:00。对于这种情况会被分割为三个文件,即每天为一个文件,第一天的文件只包含19:00开始的日志,最后一天的文件只包含到21:00的日志。

这种方案也是分片上传的一种策略,上传时以每个日志文件压缩一个zip文件后上传。这样一方面是保证上传成功率,文件太大会导致成功率下降,另一方面是为了做文件分割。经过观察,每个文件压缩成zip后,文件大小可以控制在500kb以内,500kb这个是我们之前做视频切片上传时的一个经验值,这个经验值是上传成功率和分片数量的一个平衡点。

日志命名使用时间戳为组合,时间单位应该精确到分钟,以方便服务端做时间筛选操作。上传以表单的方式进行提交,上传完成后会删除对应的本地日志。如果上传失败则使用重试策略,每个分片最多上传三次,如果三次都上传失败,则这次上传失败,在其他时机再重新上传。

安全性

为了保证日志的数据安全性,日志上传的请求我们通过https进行传输,但这还是不够的,https依然可以通过其他方式获取到SSL管道的明文信息。所以对于传输的内容,也需要进行加密,选择加密策略时,考虑到性能问题,加密方式采用的对称加密策略。

但对称加密的秘钥是通过非对称加密的方式下发的,并且每个上传指令对应一个唯一的秘钥。客户端先对文件进行压缩,对压缩后的文件进行加密,加密后分片再上传。服务端收到加密文件后,通过秘钥解密得到zip并解压缩。

主动上报

新的日志系统上线后,我们发现回捞成功率只有40%,因为有的用户反馈问题后就失去联系,或者反馈问题后一直没有打开App。对于这个问题,我分析用户反馈问题的途径主要有两大类,一种是用户从系统设置里进去反馈问题,并且和客服沟通后,技术介入排查问题。另一种是用户发生问题后,通过反馈群、App Store评论区、运营等渠道反馈的问题。

这两种方式都适用于日志回捞,但第一种由于有特定的触发条件,也就是用户点击进入反馈界面。所以,对于这种场景反馈问题的用户,我们增加了主动上报的方式。即用户点击反馈时,主动上报以当前时间为结束点,三天内的日志。这样可以把日志上报的成功率提升到90%左右,成功率上来后也会推动更多人接入日志模块,方便排查线上问题。

手动导出

日志上传的方式还包含手动导出,手动导出就是通过某种方式进入调试页面,在调试页面选择对应的日志分享,并且调起系统分享面板,通过对应的渠道分享出去。在新的日志系统之前,就是通过这种方式让用户手动导出日志给客服的,可想而知对用户的打扰有多严重吧。

现在手动导出的方式依然存在,但只用于debug阶段测试和开发同学,手动导出日志来排查问题,线上是不需要用户手动操作的。

dsasdlalfjsdafas.png


作者:刘小壮
链接:https://juejin.cn/post/7028229305050071071

收起阅读 »

iOS-组件化

iOS
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动 通过问题看本质!!! 组件化目的: 组件化可以明确业务模块职责及边界,降低模块之间的耦合以减少复杂依赖,提高代码可维护性,提高业务模块调度的规范性、灵活性,后续也可进一步优化编译速度。 那什么时候要做组...
继续阅读 »

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动


通过问题看本质!!!


组件化目的:


组件化可以明确业务模块职责及边界,降低模块之间的耦合以减少复杂依赖,提高代码可维护性,提高业务模块调度的规范性、灵活性,后续也可进一步优化编译速度。


那什么时候要做组件化呢?随着项目功能的复杂提升,各个业务代码耦合越来越多。这个时候就可以开始考虑组件化了。


通俗的讲,就好比你去宿舍附近的便利店买东西,直接走过去就到了。就没有必要打车了,打车效率还更低了呢。


如果你去公司(车程半小时),就有必要打车或者公交车了,走路那得多慢啊,等你走到了,估计都矿工几小时了。


组件化方案


1. URL路由方案;


2. runtime反射调用(简单反射及二次封装Target-action)


3. Target-action(category及动态调度);


4. protocol方案;


5. notification方案;


组件化的方案有很多种,没有哪种最好,只有哪种最合适。常见的是url-block、protocol-class、target-action方案。所以参考了网上的一些文章,对这3种方案做了一下简单的对比。


url-block 蘑菇街


路由中心维护一张路由表,url为key,block为value。


优点:

1、统一iOS、安卓的平台差异性


缺点:

1、url参数收到限制,只能传常规的字符串参数,无法传递data、image参数;


2、无法区分本地和远程情况;


3、组件本身依赖中间件,且分散注册的耦合较多;


4、启动时提供注册服务,保存在内存中。


protocol-class


优点:

1、扩展了本地调用的功能;


2、通过实现接口来提供服务,只是中间加了一层wrapper;


3、通过protocol-class做一个映射,在内存中保存一张映射表;


缺点:

还是存在内存中维护注册表的问题


target-action


使用target-action方式实现组件间的解耦,本身功能完全独立,不依赖中间件。


1、通过runtime进行反射,直接调用。


2、生成方法签名,通过invocation对象,直接执行invoke方法。


3、通过组件包装一层wrapper来给外界提供服务,不会对原组件代码造成入侵。


4、中间件是通过runtime来调用组件服务的,中间件的catergory提供服务给调度者。


5、使用者只需要依赖中间件,中间件又不需要依赖组件。


作者:龙在掘金62077
链接:https://juejin.cn/post/7023972006957678599
收起阅读 »

Swift 重构:通过预设视图样式,缩减代码量

iOS
通过预设常用视图基础属性,缩减每次创建时需要声明的属性行数(之后创建时不需要再重复声明),项目越大收益越高; 🌰🌰: { func application(_ application: UIApplication, didFinishLaunchin...
继续阅读 »

通过预设常用视图基础属性,缩减每次创建时需要声明的属性行数(之后创建时不需要再重复声明),项目越大收益越高;



🌰🌰:


{
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

UIApplication.setupAppearance(.white, barTintColor: .systemBlue)
}


源码:


@objc public extension UIApplication{

/// 配置 app 外观主题色
static func setupAppearance(_ tintColor: UIColor, barTintColor: UIColor) {
_ = {
$0.barTintColor = barTintColor
$0.tintColor = tintColor
$0.titleTextAttributes = [NSAttributedString.Key.foregroundColor: tintColor,]
}(UINavigationBar.appearance())


_ = {
$0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.black], for: .normal)
}(UIBarButtonItem.appearance(whenContainedInInstancesOf: [UIImagePickerController.self]))


_ = {
$0.setTitleColor(tintColor, for: .normal)
$0.titleLabel?.adjustsFontSizeToFitWidth = true;
$0.titleLabel?.minimumScaleFactor = 1.0;
$0.imageView?.contentMode = .scaleAspectFit
$0.isExclusiveTouch = true
$0.adjustsImageWhenHighlighted = false
}(UIButton.appearance(whenContainedInInstancesOf: [UINavigationBar.self]))


_ = {
$0.tintColor = tintColor

$0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: tintColor,
], for: .normal)
$0.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: barTintColor,
], for: .selected)
}(UISegmentedControl.appearance(whenContainedInInstancesOf: [UINavigationBar.self]))


_ = {
$0.tintColor = tintColor
}(UISegmentedControl.appearance())


_ = {
$0.autoresizingMask = [.flexibleWidth, .flexibleHeight]
$0.showsHorizontalScrollIndicator = false
$0.keyboardDismissMode = .onDrag;
if #available(iOS 11.0, *) {
$0.contentInsetAdjustmentBehavior = .never;
}
}(UIScrollView.appearance())


_ = {
$0.separatorInset = .zero
$0.separatorStyle = .singleLine
$0.rowHeight = 60
$0.backgroundColor = .groupTableViewBackground
if #available(iOS 11.0, *) {
$0.estimatedRowHeight = 0.0;
$0.estimatedSectionHeaderHeight = 0.0;
$0.estimatedSectionFooterHeight = 0.0;
}
}(UITableView.appearance())


_ = {
$0.layoutMargins = .zero
$0.separatorInset = .zero
$0.selectionStyle = .none
$0.backgroundColor = .white
}(UITableViewCell.appearance())


_ = {
$0.scrollsToTop = false
$0.isPagingEnabled = true
$0.bounces = false
}(UICollectionView.appearance())


_ = {
$0.layoutMargins = .zero
$0.backgroundColor = .white
}(UICollectionViewCell.appearance())


_ = {
$0.titleLabel?.adjustsFontSizeToFitWidth = true;
$0.titleLabel?.minimumScaleFactor = 1.0;
$0.imageView?.contentMode = .scaleAspectFit
$0.isExclusiveTouch = true
$0.adjustsImageWhenHighlighted = false
}(UIButton.appearance())


_ = {
$0.isUserInteractionEnabled = true;
}(UIImageView.appearance())


_ = {
$0.isUserInteractionEnabled = true;
}(UILabel.appearance())


_ = {
$0.pageIndicatorTintColor = barTintColor
$0.currentPageIndicatorTintColor = tintColor
$0.isUserInteractionEnabled = true;
$0.hidesForSinglePage = true;
}(UIPageControl.appearance())


_ = {
$0.progressTintColor = barTintColor
$0.trackTintColor = .clear
}(UIProgressView.appearance())


_ = {
$0.datePickerMode = .date;
$0.locale = Locale(identifier: "zh_CN");
$0.backgroundColor = .white;
if #available(iOS 13.4, *) {
$0.preferredDatePickerStyle = .wheels
}
}(UIDatePicker.appearance())


_ = {
$0.minimumTrackTintColor = tintColor
$0.autoresizingMask = .flexibleWidth
}(UISlider.appearance())


_ = {
$0.onTintColor = tintColor
$0.autoresizingMask = .flexibleWidth
}(UISwitch.appearance())

}
}

作者:SoaringHeart
链接:https://juejin.cn/post/6974338640784654350

收起阅读 »

iOS Reachability

iOS
大多数App都严重依赖于网络,一款用户体验良好的的app是必须要考虑网络状态变化的。为了更好的用户体验,我们会在无网络时展现本地或者缓存的内容,并对用户进行合适的提示。对于网络状态的检测,苹果提供了Reachability,由此也衍生出各种 Reachabil...
继续阅读 »

大多数App都严重依赖于网络,一款用户体验良好的的app是必须要考虑网络状态变化的。

为了更好的用户体验,我们会在无网络时展现本地或者缓存的内容,并对用户进行合适的提示。对于网络状态的检测,苹果提供了Reachability,由此也衍生出各种 Reachability 框架,比较著名的有Github上的 tonymillion/Reachability 以及 AFNetworking 中的 AFNetworkReachabilityManager 模块,它们的实现原理基本上都是对苹果公司的SCNetworkReachability API进行的封装。

1、SCNetworkReachability (SystemConfiguration.framework)

0.png

获取网络状态:

@property (nonatomic, strong) dispatch_source_t timer;
@property (nonatomic, assign) SCNetworkReachabilityRef reachability;
@property (nonatomic, strong) dispatch_queue_t serialQueue;

-(void)dealloc {
if (_reachability != NULL) {
CFRelease(_reachability);
_reachability = NULL;
}
}

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.

//创建零地址,0.0.0.0地址表示查询本机的网络连接状态
struct sockaddr_in zeroAddress;
bzero(&zeroAddress, sizeof(zeroAddress));
zeroAddress.sin_len = sizeof(zeroAddress);
zeroAddress.sin_family = AF_INET;

_reachability = SCNetworkReachabilityCreateWithAddress(NULL, (struct sockaddr *)&zeroAddress);
_serialQueue = dispatch_queue_create("com.xmy.serialQueue", DISPATCH_QUEUE_SERIAL);

__weak __typeof(self) weakSelf = self;
_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"连接状态: %d", [strongSelf isConnectionAvailable]);
});
dispatch_resume(_timer);

[self startMonitor];
}

- (BOOL)isConnectionAvailable
{
SCNetworkReachabilityFlags flags;
//获取连接的标志
BOOL didRetrieveFlags = SCNetworkReachabilityGetFlags(_reachability, &flags);

//如果不能获取连接标志,则不能进行网络连接,直接返回
if (!didRetrieveFlags) {
NSLog(@"Error. Could not recover network reachability flags");
return NO;
}

//根据连接标志进行判断
BOOL isReachable = ((flags & kSCNetworkFlagsReachable) != 0);
BOOL needConnection = ((flags & kSCNetworkFlagsConnectionRequired) != 0);

return (isReachable && !needConnection) ? YES : NO;
}

- (void)startMonitor
{
SCNetworkReachabilityContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
if (SCNetworkReachabilitySetCallback(_reachability, ReachabilityCallback, &context)) {
// Schedules the given target with the given run loop and mode.
// SCNetworkReachabilityScheduleWithRunLoop(_reachability, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);

// Schedule or unschedule callbacks for the given target on the given dispatch queue.
SCNetworkReachabilitySetDispatchQueue(_reachability, _serialQueue);
}
}

- (void)stopMonitor
{
SCNetworkReachabilitySetCallback(_reachability, NULL, NULL);

// Unschedules the given target from the given run loop and mode.
// SCNetworkReachabilityUnscheduleFromRunLoop(_reachability, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
SCNetworkReachabilitySetDispatchQueue(_reachability, NULL);
}

static void ReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info)
{
NSLog(@"%@, %d, %@", target, flags, info);
}

优点:

  • 使用简单,只有一个类,官方还有Demo,容易上手
  • 灵敏度高,基本网络一有变化,基本马上就能判断出来

缺点:

  • 现在很流行的公用wifi,需要网页鉴权,鉴权之前无法上网,但本地连接已经建立
  • 存在本地网络连接,但信号很差,实际无法连接到服务器情况
  • 能否连接到指定服务器,比如国内访问墙外的服务器

苹果的Reachability有如下说明,告诉我们其能力受限于此:
The SCNetworkReachability programming interface allows an application to determine the status of a system's current network configuration and the reachability of a target host.
A remote host is considered reachable when a data packet, sent by an application into the network stack, can leave the local device. Reachability does not guarantee that the data packet will actually be received by the host.
当应用程序发送到网络堆栈的数据包可以离开本地设备时,就可以认为远程主机是可访问的,不能保证主机是否实际接收到数据包。

2、SimplePing

ping 是 Windows、Unix 、Linux和macOS 等系统下一个常用的命令,利用 ping 命令可以用来测试数据包能否通过IP 协议到达特定主机,并收到主机的应答,以检查网络是否连通和网络连接速度,帮助我们分析和判定网络故障。

SimplePing是苹果封装好的ping的功能,它利用resolve host,create socket(send&recv data),解析ICMP 包验证 checksum 等实现了 ping功能。并且支持iPv4 和 iPv6。

ping 功能使用是 ICMP 协议(Internet Control Message Protocol),ICMP 协议定义了一组错误信息,当路由器或者主机无法成功处理一个IP 封包的时候,能够将错误信息回送给来源主机:

1.png ICMP用途:差错通知、信息查询、重定向等

2.png [1]给送信者的错误通知;[2]送信者的信息查询。

[1]是到IP 数据包被对方的计算机处理的过程中,发生了什么错误时被使用。不仅传送发生了错误这个事实,也传送错误原因等消息。

[2]的信息询问是在送信方的计算机向对方计算机询问信息时被使用。被询问内容的种类非常丰富,他们有目标IP 地址的机器是否存在这种基本确认,调查自己网络的子网掩码,取得对方机器的时间信息等。

Ping实现:

3.png Ping超时原因:

  • 目标服务器不存在
  • 花在数据包交流上的时间太长ping命令认为超时
  • 目标服务器不回答ping命令

SimplePing实现:

4.png SimplePing初始化:

let hostName = "www.baidu.com"
var pinger: SimplePing?
var sendTimer: NSTimer?

/// Called by the table view selection delegate callback to start the ping.
func start(forceIPv4 forceIPv4: Bool, forceIPv6: Bool) {
let pinger = SimplePing(hostName: self.hostName)
self.pinger = pinger

// By default we use the first IP address we get back from host resolution (.Any)
// but these flags let the user override that.
if (forceIPv4 && !forceIPv6) {
pinger.addressStyle = .ICMPv4
} else if (forceIPv6 && !forceIPv4) {
pinger.addressStyle = .ICMPv6
}

pinger.delegate = self
pinger.start()
}

/// Called by the table view selection delegate callback to stop the ping.
func stop() {
self.pinger?.stop()
self.pinger = nil

self.sendTimer?.invalidate()
self.sendTimer = nil

self.pingerDidStop()
}

/// Sends a ping.
/// Called to send a ping, both directly (as soon as the SimplePing object starts up) and
/// via a timer (to continue sending pings periodically).
func sendPing() {
self.pinger!.sendPingWithData(nil)
}

代理方法:

/// pinger.start()成功之后,解析HostName拿到ip地址后的回调
func simplePing(pinger: SimplePing, didStartWithAddress address: NSData) {
NSLog("pinging %@", MainViewController.displayAddressForAddress(address))

// Send the first ping straight away.
self.sendPing()

// And start a timer to send the subsequent pings.
assert(self.sendTimer == nil)
self.sendTimer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(MainViewController.sendPing), userInfo: nil, repeats: true)
}

/// pinger.start()功能启动失败的回调
func simplePing(pinger: SimplePing, didFailWithError error: NSError) {
NSLog("failed: %@", MainViewController.shortErrorFromError(error))

self.stop()
}

/// sendPingWithData发送数据成功
func simplePing(pinger: SimplePing, didSendPacket packet: NSData, sequenceNumber: UInt16) {
NSLog("#%u sent", sequenceNumber)
}

/// sendPingWithData发送数据失败,并返回错误信息
func simplePing(pinger: SimplePing, didFailToSendPacket packet: NSData, sequenceNumber: UInt16, error: NSError) {
NSLog("#%u send failed: %@", sequenceNumber, MainViewController.shortErrorFromError(error))
}

/// ping发送后收到响应
func simplePing(pinger: SimplePing, didReceivePingResponsePacket packet: NSData, sequenceNumber: UInt16) {
NSLog("#%u received, size=%zu", sequenceNumber, packet.length)
}

/// ping接收响应封包发生异常
func simplePing(pinger: SimplePing, didReceiveUnexpectedPacket packet: NSData) {
NSLog("unexpected packet, size=%zu", packet.length)
}

如代码所示,每隔一段时间就ping下host,看看是否畅通无阻,因此ping不可能做到及时判断网络变化,会有一定的延迟:
利用Reachability判断当前设备是否联网,利用SimplePing来检查服务器是否连通。

3、RealReachability (Star: 3k)

5.png

4、扩展:traceroute

由于ping命令不一定能判断对方是否存在,为了查看主机及目标主机之间的路由路径,我们使用traceroute 命令。它与ping 并列,也是ICMP 的典型实现之一。

traceroute是利用增加存活时间(TTL)值来实现功能的。每当一个icmp包经过一个路由器时,其存活时间值就会减1,当其存活时间为0时,路由器便会取消包发送,并发送一个ICMP TTL超时封包给原封包发出者。

6.png

7.png 命令行测试:

测试1
> traceroute -I baidu.com
traceroute: Warning: baidu.com has multiple addresses; using 220.181.38.148
traceroute to baidu.com (220.181.38.148), 64 hops max, 72 byte packets
1 172.25.62.254 (172.25.62.254) 2.198 ms 1.690 ms 1.437 ms
2 172.25.100.17 (172.25.100.17) 2.175 ms 1.795 ms 1.769 ms
3 * * *
4 * * *
5 * * *
6 * * *
7 * * *
8 * * *
9 * * *
10 * * *
11 * * *
12 * * *
13 * * *
14 * * *
15 * * *
16 220.181.38.148 (220.181.38.148) 29.700 ms 29.135 ms 29.127 ms

测试2
> traceroute -I baidu.com
traceroute: Warning: baidu.com has multiple addresses; using 39.156.69.79
traceroute to baidu.com (39.156.69.79), 64 hops max, 72 byte packets
1 172.25.62.254 (172.25.62.254) 3.339 ms 1.993 ms 4.845 ms
2 172.25.100.17 (172.25.100.17) 2.146 ms 1.792 ms 1.971 ms
3 * * *
4 * * *
5 * * *
6 * * *
7 * * *
8 * * *
9 * * *
10 * * *
11 * * *
12 * * *
13 * * *
14 * * *
15 * * *
16 * * *
17 * * *
18 39.156.69.79 (39.156.69.79) 29.015 ms 27.569 ms 28.232 ms

net-diagnosis (Star: 0.3k)
通过集成net-diagnosis,您可以轻松地在iOS上实现ping / traceroute /移动公共网络信息/端口扫描等网络诊断相关的功能。

8.png

9.png


收起阅读 »

iOS开发Crash之内存暴涨

iOS
今天遇到了一个线上的Crash,线上包,用户打开APP后就一直闪退,但是我们开发和测试都没有这样的问题,后面等到Bugly上报后,看到问题,找到了相对应的测试包开始复现,同事在某一个tf上的build版本QA测试成功出了这个Crash.找到对应的组件分支,全m...
继续阅读 »

今天遇到了一个线上的Crash,线上包,用户打开APP后就一直闪退,但是我们开发和测试都没有这样的问题,后面等到Bugly上报后,看到问题,找到了相对应的测试包开始复现,同事在某一个tf上的build版本QA测试成功出了这个Crash.找到对应的组件分支,全master指定版本真机测试.


全master真机运行出现的结果为


image.png


Message from debugger:Terminated due to memory issue
复制代码

看见这个错误大概就知道是内存暴涨被看门狗杀死了


关键是如何排查


使用instrument中的leaks工具来查看,但是我们并没有这样排查,我们发现一进APP过了main以后就被杀死了,那么可以初步确定是工作台发生的错误.


那么工作台的主要功能就是加载相应权限,然后配置菜单等功能,想到会不会是因为这个客户的数据异常,我们把网络关了,然后进入工作台,不会Crash了。


然后我们把工作台所用到的几个请求一一排查下来,发现在工作台有一个功能卡片组件,组件里面使用了富文本,而富文本是根据后端来的数据来进行对应的加载。


后端接口返回有一个字段,是否采用富文本,然后哪一段启用富文本,字体,颜色,样式都是由后端决定。


因为是一个tableView的Cell,里面又嵌套了for循环,for循环里面又嵌套了for来展示item,item又记录的一条条的富文本还有图片,犹豫cell每次数据源都会addSubviews,没有remove掉,再加上后端返回了N条,同事写的时候没有限制,我们这边只显示4条,显示的图片占用内存很大,从而导致内存暴涨,这一块因为是很老的代码了,需要重构一下,为了线上先不崩溃,跟后端商量图片压缩以及返回条数限制


最后记录一下crash日志
image.png


作者:小肥肥
链接:https://juejin.cn/post/7039598306267234318
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS整体框架介绍

iOS
这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战iOS整体框架通常我们称iOS的框架为cocoa框架. 话不多说,官方的整体框架图如下:简单解释一下:Cocoa (Application) Layer(触摸层)Media Layer ...
继续阅读 »

这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

iOS整体框架

通常我们称iOS的框架为cocoa框架. 话不多说,官方的整体框架图如下:

image.png

简单解释一下:

  • Cocoa (Application) Layer(触摸层)
  • Media Layer (媒体层)
  • Core Services Layer(核心服务层)
  • Core OS Layer (核心系统操作层)
  • The Kernel and Device Drivers layer(内核和驱动层)

注:Cocoa (Application) Layer(触摸层)其实包含cocoa Touch layer(触摸层) 和Application Layer (应用层).应用层原本在触摸层上面,因为应用层是开发者自己实现,所以和触摸层合在一起.

其实每一层都包含多个子框架, 如下图:

image.png

简单解释下(瞄一眼就得了):

  • Cocoa Touch Layer:触摸层提供应用基础的关键技术支持和应用的外观。如NotificationCenter的本地通知和远程推送服务,iAd广告框架,GameKit游戏工具框架,消息UI框架,图片UI框架,地图框架,连接手表框架,UIKit框架、自动适配等等

  • Media Layer:媒体层提供应用中视听方面的技术,如图形图像相关的CoreGraphics,CoreImage,GLKit,OpenGL ES,CoreText,ImageIO等等。声音技术相关的CoreAudio,OpenAL,AVFoundation,视频相关的CoreMedia,Media Player框架,音视频传输的AirPlay框架等等

  • Core Services Layer:系统服务层提供给应用所需要的基础的系统服务。如Accounts账户框架,广告框架,数据存储框架,网络连接框架,地理位置框架,运动框架等等。这些服务中的最核心的是CoreFoundationFoundation框架,定义了所有应用使用的数据类型。CoreFoundation是基于C的一组接口,Foundation是对CoreFoundation的OC封装

  • Core OS Layer:系统核心层包含大多数低级别接近硬件的功能,它所包含的框架常常被其它框架所使用。Accelerate框架包含数字信号,线性代数,图像处理的接口。针对所有的iOS设备硬件之间的差异做优化,保证写一次代码在所有iOS设备上高效运行。CoreBluetooth框架利用蓝牙和外设交互,包括扫描连接蓝牙设备,保存连接状态,断开连接,获取外设的数据或者给外设传输数据等等。Security框架提供管理证书,公钥和私钥信任策略,keychain,hash认证数字签名等等与安全相关的解决方案。

想看更详细的可以移步:iOS总体框架介绍和详尽说明

我们只需要知道其中重要的框架就是UIKit和Function框架.下面说说这两个框架.

Function框架

Foundation框架为所有应用程序提供基本的系统服务。应用程序以及 UIKit和其他框架,都是建立在 Foundation 框架的基础结构之上。 Foundation框架提供许多基本的对象类和数据类型,使其成为应用程序开发的基础。

话不多说,我们先来看看Foundation框架,三个图,包括了Foundation所以的类,图中灰色的是iOS不支持的,灰色部分是OS X系统的。

image.png

image.png

image.png

这里只需要知道绝大部分Function框架的类都继承NSObject, 小部分继承NSProxy

对于Foundation框架中的一些基本类的使用方法详情参见:iOS开发系列—Objective-C之Foundation框架

UIKit框架

UIKit框架提供一系列的Class(类)来建立和管理iOS应用程序的用户界面( UI )接口、应用程序对象、事件控制、绘图模型、窗口、视图和用于控制触摸屏等的接口。

UIKit框架的类继承体系图如下图所示:

image.png

在图中可以看出,responder 类是图中最大分支的根类,UIResponder为处理响应事件和响应链定义了界面和默认行为。当用户用手指滚动列表或者在虚拟键盘上输入时,UIKit就生成事件传送给UIResponder响应链,直到链中有对象处理这个事件。相应的核心对象,比如:UIApplication ,UIWindowUIView都直接或间接的从UIResponder继承 。

这里需要知道一点:UIKit框架所有的类都继承NSObject

UIKit框架的各个类的简单介绍戳后面的链接:UIKit框架各个类的简要说明 

收起阅读 »

阿里二面:什么是mmap?

iOS
平时在面试中你肯定会经常碰见的问题就是:RocketMQ为什么快?Kafka为什么快?什么是mmap?这一类的问题都逃不过的一个点就是零拷贝,虽然还有一些其他的原因,但是今天我们的话题主要就是零拷贝。传统IO在开始谈零拷贝之前,首先要对传统的IO方式有一个概念...
继续阅读 »

平时在面试中你肯定会经常碰见的问题就是:RocketMQ为什么快?Kafka为什么快?什么是mmap?

这一类的问题都逃不过的一个点就是零拷贝,虽然还有一些其他的原因,但是今天我们的话题主要就是零拷贝。

传统IO

在开始谈零拷贝之前,首先要对传统的IO方式有一个概念。

基于传统的IO方式,底层实际上通过调用read()write()来实现。

通过read()把数据从硬盘读取到内核缓冲区,再复制到用户缓冲区;然后再通过write()写入到socket缓冲区,最后写入网卡设备。

整个过程发生了4次用户态和内核态的上下文切换4次拷贝,具体流程如下:

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发公众号:编程大鑫,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!

  1. 用户进程通过read()方法向操作系统发起调用,此时上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. CPU把读缓冲区数据拷贝到应用缓冲区,上下文从内核态转为用户态,read()返回
  4. 用户进程通过write()方法发起调用,上下文从用户态转为内核态
  5. CPU将应用缓冲区中数据拷贝到socket缓冲区
  6. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write()返回

那么,这里指的用户态内核态指的是什么?上下文切换又是什么?

简单来说,用户空间指的就是用户进程的运行空间,内核空间就是内核的运行空间。

如果进程运行在内核空间就是内核态,运行在用户空间就是用户态。

为了安全起见,他们之间是互相隔离的,而在用户态和内核态之间的上下文切换也是比较耗时的。

从上面我们可以看到,一次简单的IO过程产生了4次上下文切换,这个无疑在高并发场景下会对性能产生较大的影响。

那么什么又是DMA拷贝呢?

因为对于一个IO操作而言,都是通过CPU发出对应的指令来完成,但是相比CPU来说,IO的速度太慢了,CPU有大量的时间处于等待IO的状态。

因此就产生了DMA(Direct Memory Access)直接内存访问技术,本质上来说他就是一块主板上独立的芯片,通过它来进行内存和IO设备的数据传输,从而减少CPU的等待时间。

但是无论谁来拷贝,频繁的拷贝耗时也是对性能的影响。

零拷贝

零拷贝技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域,这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。

那么对于零拷贝而言,并非真的是完全没有数据拷贝的过程,只不过是减少用户态和内核态的切换次数以及CPU拷贝的次数。

这里,仅仅有针对性的来谈谈几种常见的零拷贝技术。

mmap+write

mmap+write简单来说就是使用mmap替换了read+write中的read操作,减少了一次CPU的拷贝。

mmap主要实现方式是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次CPU拷贝。

整个过程发生了4次用户态和内核态的上下文切换3次拷贝,具体流程如下:

  1. 用户进程通过mmap()方法向操作系统发起调用,上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. 上下文从内核态转为用户态,mmap调用返回
  4. 用户进程通过write()方法发起调用,上下文从用户态转为内核态
  5. CPU将读缓冲区中数据拷贝到socket缓冲区
  6. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write()返回

mmap的方式节省了一次CPU拷贝,同时由于用户进程中的内存是虚拟的,只是映射到内核的读缓冲区,所以可以节省一半的内存空间,比较适合大文件的传输。

sendfile

相比mmap来说,sendfile同样减少了一次CPU拷贝,而且还减少了2次上下文切换。

sendfile是Linux2.1内核版本后引入的一个系统调用函数,通过使用sendfile数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间的拷贝,同时由于使用sendfile替代了read+write从而节省了一次系统调用,也就是2次上下文切换。

整个过程发生了2次用户态和内核态的上下文切换3次拷贝,具体流程如下:

  1. 用户进程通过sendfile()方法向操作系统发起调用,上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. CPU将读缓冲区中数据拷贝到socket缓冲区
  4. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,sendfile调用返回

sendfile方法IO数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比如静态文件服务器。

sendfile+DMA Scatter/Gather

Linux2.4内核版本之后对sendfile做了进一步优化,通过引入新的硬件支持,这个方式叫做DMA Scatter/Gather 分散/收集功能。

它将读缓冲区中的数据描述信息--内存地址和偏移量记录到socket缓冲区,由 DMA 根据这些将数据从读缓冲区拷贝到网卡,相比之前版本减少了一次CPU拷贝的过程

整个过程发生了2次用户态和内核态的上下文切换2次拷贝,其中更重要的是完全没有CPU拷贝,具体流程如下:

  1. 用户进程通过sendfile()方法向操作系统发起调用,上下文从用户态转向内核态
  2. DMA控制器利用scatter把数据从硬盘中拷贝到读缓冲区离散存储
  3. CPU把读缓冲区中的文件描述符和数据长度发送到socket缓冲区
  4. DMA控制器根据文件描述符和数据长度,使用scatter/gather把数据从内核缓冲区拷贝到网卡
  5. sendfile()调用返回,上下文从内核态切换回用户态

DMA gathersendfile一样数据对用户空间不可见,而且需要硬件支持,同时输入文件描述符只能是文件,但是过程中完全没有CPU拷贝过程,极大提升了性能。

应用场景

对于文章开头说的两个场景:RocketMQ和Kafka都使用到了零拷贝的技术。

对于MQ而言,无非就是生产者发送数据到MQ然后持久化到磁盘,之后消费者从MQ读取数据。

对于RocketMQ来说这两个步骤使用的是mmap+write,而Kafka则是使用mmap+write持久化数据,发送数据使用sendfile

总结

由于CPU和IO速度的差异问题,产生了DMA技术,通过DMA搬运来减少CPU的等待时间。

传统的IOread+write方式会产生2次DMA拷贝+2次CPU拷贝,同时有4次上下文切换。

而通过mmap+write方式则产生2次DMA拷贝+1次CPU拷贝,4次上下文切换,通过内存映射减少了一次CPU拷贝,可以减少内存使用,适合大文件的传输。

sendfile方式是新增的一个系统调用函数,产生2次DMA拷贝+1次CPU拷贝,但是只有2次上下文切换。因为只有一次调用,减少了上下文的切换,但是用户空间对IO数据不可见,适用于静态文件服务器。

sendfile+DMA gather方式产生2次DMA拷贝,没有CPU拷贝,而且也只有2次上下文切换。虽然极大地提升了性能,但是需要依赖新的硬件设备支持。

收起阅读 »