马蜂窝 iOS App 启动治理:回归用户体验

From:https://mp.weixin.qq.com/s?__biz=Mzg5MTA4Mzg5NA==&mid=2247483816&idx=1&sn=588981334dd8c797ce2e4395db6ca68d&utm_source=tuicool&utm_medium=referral

增长、活跃、留存是移动 App 的常见核心指标,直接反映一款 App 甚至一个互联网公司运行的健康程度和发展动能。启动流程的体验决定了用户的第一印象,在一定程度上影响了用户活跃度和留存率。因此,确保启动流程的良好体验至关重要。

「马蜂窝旅游」App 是马蜂窝为用户提供服务的主要阵地,其承载的 业务模块不断丰富和完善,产品功能日趋复杂, 已经逐渐成长为一个集合旅行信息、出行决策、自由行产品及服务交易的一站式移动平台。

「马蜂窝旅游」iOS App 历经几十个版本的开发迭代,在启动流程上积累了一定的技术债务。为了带给用户更流畅的使用体验,我们团队实施了数月的专项治理,也总结出一些 iOS 启动治理方面的实践经验,借由本文和大家分享。

0X0

如何定义「启动」

要分析和解决启动问题,我们首先需要界定启动的内涵和边界,从哪开始、到哪结束,中间经历了哪些阶段和过程。以不同视角去观察时,可以得出不同结论。

技术视角

App 启动原本就是程序启动的技术过程。作为开发人员,我们很自然地更愿意从技术阶段去看待和定义启动的流程。

App 启动的方式分为 冷启动 和 热启动 两种。简单来说,冷启动发生时后台是没有这个应用的进程的,程序需要从头开始,经过漫长的准备和加载过程,最终运行起来。而热启动则是在后台已有该应用进程的情况下发生的,系统不需要重新创建和初始化。因此,从技术视角讨论启动治理时,主要针对冷启动。

从技术视角出发,分析 iOS 的启动过程,主要分为两个阶段:

pre-main: main() 函数是程序执行入口,从进程创建到进入 main 函数称为 premain 阶段, 主要包括了环境准备、资源加载等操作;

post-main: main() 函数到-didFinishLaunchWithOptions:方法执行结束。该阶段已获得代码执行控制权,是我们治理的主要部分。

 <premain>                  <postmain>

+----------------X------------------------------------X--------->

start main -didFinishLaunchWithOptions:

用户视角

iOS App 是面向终端用户的产品,因此衡量启动的最终标准还是要从用户视角出发。

从用户视角定义启动,主要以用户主观视觉为依据,以页面流程为标准。这样看来,常见的 App 启动可以分为三个阶段:

T1:闪屏页
闪屏页是启动过程中的静态展示页。在冷启动的过程中,App 还没有运行起来,需要经历环境准备和初始化的过程。这个过渡阶段需要展示一些视图,供阻塞等待中的用户浏览。

iOS 系统 (SpringBoard) 根据 App Bundle 目录下的 Info.plist 中"Launch screen interface file base name"字段的值,找到所指定的 xib 文件,加载渲染展示该视图。

闪屏页的展示是系统行为,因此无法控制;加载的是 xib 描述文件,无法定制动态展示逻辑,因此是静态展示。

对应技术启动阶段的 pre-main 阶段

T2(可选):欢迎页(广告)
App 运行后根据特定的业务逻辑展示的第一个页面。常见的有广告页和装机引导流程。

欢迎页是业务定制的,因此可根据业务需要优化展示策略,该阶段本身也是可选的。

T3:目标页 (落地页)
App 启动的目标页。

可以是首页或特定的落地页

目标页的加载渲染渲染完成标志着 T3 阶段的结束,也标志着启动流程的结束。

启动治理的最终目标是提升用户体验,在这样的思想下,本文关于启动流程的讨论主要围绕用户视角进行。

0X1

方法论及关键指标

APM 方法论

对 iOS 启动的治理,本质上是对应用性能优化 (App Performance Management) 的过程,其基本的方法论可以归纳为:

界定问题
准确描述现象,确定问题的边界

确定量化评价手段,明确关键指标

分析问题
分析问题产生的主要原因,根本原因

确定问题的重要性,优先级

性能问题可能是单点的短板,也可能是复杂的系统性问题,切忌「头痛医头,脚痛医脚」。要严谨全面地分析问题,找到主要原因、根本原因予以优先解决

解决问题
确定解题的具体技术方案

根据关键指标量化成果

对问题进行总结,积累沉淀

持续监控
性能问题是持续的,长期的

对关键技术指标建立长效的监控机制,确保增量能被及时反馈,予以处理

关键指标

  1. 启动耗时
    启动耗时是衡量启动性能的核心指标,因为它直接影响了用户体验并对用户转化率产生影响。

对启动耗时指标的拆解有助于细粒度地监控启动过程,帮助找到问题环节。具体可以拆解为:

技术启动耗时指标
pre-main

core-postmain

主观启动耗时指标
T1_duration : 从程序运行起点到主视窗可见

T2_duration

T3_duration

total_duration

根据对马蜂窝 App 用户的行为数据分析确认,我们得到以下结论:

启动耗时和启动流失率正相关

启动耗时和次日留存负相关

2.启动流失率
1). 如何定义启动流失
用户视角的启动流程完成前(即目标页渲染完成前),用户主动离开 App(进入后台,杀死 App, 切换到其他 App 等),记做 一次启动流失 。

启动流失率计算公式为:

启动 PV 流失率: 启动流失 PV / App 首次进入前台 PV

启动 UV 流失率: 启动流失 UV / DAU

UV 绝对流失率: 当日仅进入前台一次且流失的 UV / DAU

2) 如何定义首次进入前台
我们先来区分下 冷启动,热启动和首次进入前台 的概念:

iOS App 有后台机制,App 可在某些条件下,在用户不感知的情况下在后台启动(如后台刷新)。 由于用户不感知,如果当日该用户没有主动进入前台,则不会记作活跃用户。因此,单纯的后台启动不是启动流失率的分母。

但是当 iOS App 从后台启动,并留在内存中没有被操作系统清除,而一段时间后,用户触发 App 进入前台,这种情况虽然是热启动,但应被看作「首次进入前台」。

3) 如何定位流失的时机
根据定义,用户主动离开 App 则记作一次流失。从技术角度可以找到两个点:

applicationdidEnterBackground

applicaitonWillTerminate

但在实践的典型场景中我们发现,从用户点击 Home 键到程序接收到-applicationdidEnterBackground 回调存在一定的时间差,该时间差会影响到流失率的判断。

例如,用户在时刻 0.0s 启动 app,启动总时长为 4.0s。用户在时刻 3.8s 点击了 home 键离开 App,则应该记作 launch_leave = true。而程序在时刻 4.3s 接收到了-applicationDidEnterBackground 回调,此时启动已经结束,获得了启动耗时 4.0s。通过比较 Tleave > Tlaunch_total,则错误地记为 launch_leave = false。

由此推测,这里的 delay 是设置灵敏度阻尼,消除用户决策的摆动。这个延时大约在 0.5s 左右。

为了避免这个误差,我们的解决方案是利用 inactive 状态,找到准确的用户决策起点:

用户即将离开前台时,会先进入 inactive 状态,通过-appWillResignActive:拿到决策起点的时间戳 Tdetermine

根据用户最终决策行为,是否确实离开,再决定决策 Tdetermine 是否有效

最终根据有效的 Tdetermine 作为判断流失行为的标准,而不是-applicationdidEnterBackground 的时间点

  1. 启动广告曝光率
    广告是 App 盈利的主要手段之一。广告曝光率直接决定了广告点击消费率;而广告曝光 PV 和加载 PV 直接影响了广告售价。

我们定义:启动广告曝光率 = 启动广告曝光 PV / 启动广告加载 PV。

其中广告素材需要下载,素材渲染需要一定耗时,这些都会对广告曝光率产生影响。进一步来说,启动广告的曝光率会受到 App 启动性能的影响,但更主要的是受缓存和曝光策略的影响,详细阐述在下文「精细化策略」部分介绍。

0X2

iOS App 启动优化

以上,我们对 iOS App 启动治理的思路和关键指标进行了分析和拆解,下面来说一下从技术层面和业务层面,我们对启动性能的优化和流程治理分别做了哪些事情。

一、技术启动优化

  1. 优化pre-main

1). pre-main 主要流程分析
在进行该阶段的优化前,我们需要对 Pre-Main 阶段的过程有所了解,网上的文章较多,这里主要推荐两篇 WWDC 参考文章:

App Startup Time: Past, Present, and Future (https://developer.apple.com/videos/play/wwdc2017/413/)

Optimizing App Startup Time (https://developer.apple.com/videos/play/wwdc2016/406/)

总结来看,pre-main 主要流程包括:

fork 进程

加载 executable

加载 DYLD

分析依赖,迭代加载动态库

rebase

rebind

耗时多

  1. 准备环境

准备 OC 运行时

准备 C++环境

  1. main 函数

2). 优化建议
尽量少使用动态库

尽量编译到静态库中,减少 rebase,rebind 耗时

尽量合并动态库,减轻依赖关系

控制 Class 类的数量规模

由于 selector 需要在初始化时做唯一性检查,应尽量减少使用

少用 initializers

严格控制 +load 方法使用

多用 Swift

Swift 没有运行时

Swift 没有 initializers

Swift 没有数据不对齐问题

3). 性能监控:如何获取启动起点
启动的结束时间相对来说是比较好确定的,但如何定位启动的起点,是启动监控的一个难点。

对于开发环境,可以通过 Xcode 配置启动参数,获得 pre-main 的启动报告:

DYLD_PRINT_STATICS = 1
对于线上环境,根据 premain 主要流程的分析,我们的解决方案是:

创建动态库 ABootMonitor.dylib

ABootMonitor.dylib 实现+load 方法,记录启动起点时间

将 ABootMonitor.dylib 放在 executable 动态库依赖的头部

通过上述方法,可以在线上环境尽量地模拟出最早的启动时间点,从而更好地监测优化效果。

  1. 优化post-main
    post-main 阶段的技术优化主要针对两个方法的执行耗时来进行:

  • (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:

  • (void)applicationDidBecomeActive:(UIApplication *)application;

为什么包含 2,需要我们对 iOS App 生命周期有一定理解。从操作系统的视角来看,iOS App 本质上是一个进程。对于 Mac OS/iOS 系统,进程的生命周期状态包括了:

not-running

running

进程激活,可以运行的状态

suspend

进程被挂起,不可以执行代码,通常在 UIApplication 进入后台后一段时间被系统挂起

zombie

进程回收前的临时状态,很短暂

terminated

进程终止,并被清理

而对于 UIApplication,定义了生命周期状态:

//  UIApplication.h

typedef NS_ENUM(NSInteger, UIApplicationState) {
    UIApplicationStateActive,     // 前台, UIApplication响应事件
    UIApplicationStateInactive,   // 前台, UIApplication不响应事件
    UIApplicationStateBackground  // 后台, UIApplication不在屏幕上显示
} NS_ENUM_AVAILABLE_IOS(4_0);

组合起来的状态机如下图:

通过上面的讨论,我们可以分析出以下问题:

UIApplication 会因为某种原因,在用户不感知的情况下被唤起,进程进入 running 状态,但停留在 iOS 的 background 状态

每次冷启动都会执行- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:,但未必进入前台

在 didFinishLaunchingWithOptions 中进行大量 UI 和网络请求等操作是不合理

post-main 优化思路和建议
整理拆分启动项,以启动项为粒度进行测量

启动项执行尽量在背景线程

启动的过程 CPU 占用较高,占用主线程会导致卡顿,耗时延长,用户体验不佳

启动项并发执行

启动项延迟执行

当 CPU 时间片跑满时,使用多线程并发不能提高性能,反而会因为频繁的线程上下文切换,造成 overhead 耗时增长

尽可能将启动项延迟执行,在时间轴上平滑,降低 CPU 利用率峰值

启动项分组

-didFinishLaunchingWithOptions 只执行必要的核心启动项

其他启动项,在首次调用-applicationDidBecomeActive:后执行

二、精细化策略

  1. 交互优化
    通过技术的实现手段,我们可以从客观上减少启动的绝对耗时。而从用户视角来看,对于启动是否流畅会受到很多心理因素的主观影响。因此从另一方面,我们可以从优化交互的角度提升用户体验。

避免阻塞等待

我们都希望用户可以尽快地使用 App,不要出现流失。但在快消费的时代,用户的耐心是极其有限的。

因此,如果有理由需要用户进行等待,就应该注意尽量避免产品流程是阻塞的。即使有更充足的理由必须让用户在阻塞状态原地等待,也应该给用户提供可响应的交互。

例如,在 T2 欢迎/广告页阶段,为了避免用户阻塞等待,应该提供明显的「跳过」按钮,允许用户进行跳过操作。

如果非要用户在这个阶段等待不可,也可以花一些小心思提供可响应的交互,比如点击触发视觉的变化等,不要让用户除了等待无事可做。

增加视觉信息量

增加屏幕上视图的信息量提供给用户消费,转移其注意力,降低用户对等待的感受。

例如,在 T1 闪屏页阶段,用户处于阻塞等待的状态,无法跳过。而且闪屏页是系统渲染的静态视图,我们无法提供动态响应。那么,我们可以通过在静态视图上提供更多信息量,给等待中的用户消费。

主观感受对比如下图:

合理的动态提示

合适的动画
事实上,早期在部分高性能 Android 设备上,App 的启动比同水平 iDevice 要快。但由于 iOS 设计了符合神经认知学的交互动画,使得主观感受到的时间缩短。

动画是否「合适」,关键在于对场景的选择和数量的把握。一个常见的动画耗时约为 0.25s,对于启动流程来说,已经可以解决或掩盖不少问题了。

合适的提示信息
好的交互体验和产品流程,至少应该是符合用户预期的。给以合适的动态提示,让用户知道此刻使用的 App 正在发生什么,可以极大地提升用户体验。

例如在 T2 广告页阶段,广告需要占时 3 秒钟的时间。交互上建议给与广告消失的倒计时提示:

一方面,倒计时提示可以有动态 loading 的视觉效果,展现 App 的良好运行;

另一方面,倒计时可以让用户安心,主观上耗时减少,情绪上不至于焦虑和退出。

  1. 基于场景的启动会话
    根据对启动过程的定义,我们可以列举出一些启动的「起点」和「终点」,比如:

启动触发点:
点击 App 图标正常启动

初次安装

点击 PUSH 进入

应用间跳转

3DTouch

Siri 唤起

其他

启动终点--目标页:
应用首页

指定的落地页

可以看出,启动的起点和终点多种多样,而对于启动流程的设定,很多都是和业务场景强相关的,比如:

初次安装需要进入装机引导流程

正常启动需要展示广告

PUSH 进入可以不展示广告,直达落地页

其他

如何才能维护这些复杂的启动关系,提高业务承载能力呢?我们的优化思路是基于场景创建启动会话:

由启动参数和其他条件确定启动场景

根据启动场景创建具体的启动会话

启动会话接管之后的启动流程

  1. 启动广告曝光和缓存策略
    广告曝光主要流程为: 请求广告接口 —> 准备广告素材 —> 展示广告页,进行曝光。

在准备广告素材环节,我们会判断广告素材是否命中缓存。如果命中则直接使用缓存,这样可以明显缩短广告加载的时间。如果没有命中,则开始下载广告素材。当广告素材超过设定的准备时长,则此次曝光不显示。

通过以往数据量化分析,我们发现通常情况下,广告未曝光的主要原因是由于广告素材准备超时,且素材体积和广告曝光率是负相关的。 为了保证广告的曝光率,我们应该尽量减少广告素材的体积,并且提高广告素材缓存的命中率。

下面分别介绍下我们的启动广告预缓存策略和启动广告曝光策略。

启动广告预缓存策略
广告素材接口和广告曝光接口分离

在可能的合适时机,下载广告素材

例如后台启动,后台刷新等

尽可能地提前下发广告素材

拉长广告素材投放的时间窗口

常见地可提前半月下发广告素材

对于 「 双十一等大促活动,应尽早地下发素材

启动广告曝光策略
分级的广告曝光QoS策略
若业务许可,可对广告优先级进行分级

对于低优先级,应用 cache-only 的曝光策略

对于普通优先级,应用 max-wait 的曝光策略

对于高优先级,应用 max-retry 的曝光策略

灵活的曝光时机选择
通常我们仅在首次进入前台时,进行广告曝光,但这有一定的缺陷:

启动耗时长了,用户体验差,启动流失率高

对于当日只有一次启动且启动流失的用户,丢了这个 DAU

我们可以在 App 首次进入前台,和热启动切回前台时选择时机,进行有策略的曝光

可依据策略,在首启时不展示广告页,提升用户体验,DAU,减少启动流失

可在 App 切回时展示,提升广告曝光 PV,和曝光率。

由于 App 之前已经启动,此时大概率已经缓存了广告素材

由于 App 一次生命周期存在多次切回前台,曝光 PV 可以得到提升

根据马蜂窝 App 的统计分析,在激进策略下可提升曝光 PV 约 4 倍

三、合理利用平台机制

iOS 经过多年的迭代,提供了很多智能的平台机制。合理利用这些机制,可以强化 App 的功能和性能。

  1. 内存保活
    我们已经讨论了冷启动和热启动的区别:

冷启动是进程并不存在的状态,一切需要从 0 开始。

热启动是指进程在内存中(iOS 不支持 SWAP),此时可能处于 background 的 running 状态或 suspend 状态,用户唤起进去前台。

热启动可以极大地减少 T1 闪屏页时间,从而减少启动耗时。

因此,我们应该尽量增加热启动概率,并且尽量减少 App 在后台被系统回收的概率。

iOS App 生命周期中关于系统内回收策略如下:

App 进入后台后,进程会活跃一段时间后,会被操作系统挂起,进入 suspend 状态。除非在 info.plist 指定进入后台即退出。

前台运行的 App 拥有内存的优先使用权

当前台的 App 需要更多物理内存时,系统根据一定策略,将一部分挂起的 App 进行释放

系统优先选择占用内存多的 App 进行释放

优化思路:
App 进入后台时,应该将内存资源竟可能的释放,尽量在内存中保活

尤其对于可重得的图片,文件等资源进行释放

对于可持久化的非重要内存,也可做持久化后释放

对于线上,应利用后台进程激活状态,加强对后台内存使用的监控

  1. 后台拉起
    iOS 系统提供了一些机制,可以帮助我们实现在用户不感知的情况下拉起 App。合适的拉起策略,可以优化 App 性能和功能表现,比如提升当日首启热启动的概率;在后台准备更新一些数据,如更新 PUSH token、准备启动广告素材等。

iOS 常见的后台拉起机制包括:

Background-fetch 后台刷新

需要权限

在某特定时机拉起,智能策略

PUSH

静默推送

远端推送

aps 中指定 "content-available = 1"

App 实现相关处理方法

地理围栏

后台网络任务 NSURLBackgroundSession

VOIP 等其他

使用后台机制时,有以下几点需要注意:

常见的后台机制需要 entitlement 声明和用户授权

部分节能模式会使部分拉起机制失效,节能模式不可用

拉起策略参考用户意图,用户主动杀死 App,会使部分拉起机制失效

正常进入后台,该 App 会向系统应用 「 AppSwitcher 」 注册,并受其管理

如果用户主动杀死 App,该 App 不会向 「 AppSwitcher 」 注册

后台拉起时,主要从 AppSwitcher 的注册列表选择 App 进行操作。 例如,后台刷新会根据某种策略排序,依此拉起 AppSwitcher 中注册的部分 App

批量拉起会导致服务端接口压力过大

例如使用 PUSH 拉起,则短时间内可能有数千万的 App 被拉起,此时接口请求不亚于一次针对服务端的 DDOS 攻击,需要整理和优化

四、结构化定制

页面栈/树优化
App 通过页面进行组织,在启动过程中,我们需要构建根页面栈。

由上分析我们知道,App 存在后台拉起,我们建议在首次进入前台时才进行页面渲染操作。但另一方面,根页面栈是 App 的基本结构,应该作为核心启动流程。因此我们提出以下解决方案:

涉及启动的页面,如首页、落地页等,应将页面栈创建、数据请求、页面渲染分离

在核心启动流程 (didFinishLaunch) 创建核心页面栈

在即将进入前台时,异步请求数据

在目标页即将展示时,进行渲染

例如,在广告页消失前的 1s,通知首页进行渲染,如下图

由于目标页可能和 T2 等启动阶段重叠,应特别注意页面加载的性能问题,避免交叉影响

0x3

结语

经过团队 3 个月的持续优化治理,马蜂窝 iOS App 的启动优化取得了一些成果:

启动耗时: 约 3.6s,减少约 50%

PV启动流失率: 降低约 30%

启动广告曝光率: 大幅提升

ios App 的启动治理乃至性能管理,是一个长期且艰巨的过程,需要各位开发同学具备良好的对平台和对代码性能的理解意识。其次,性能问题也常常是一个复杂的系统性问题,需要严谨地分析和推理,在此感谢支持以上工作的马蜂窝数据分析师。最后,这项工作需要建立完善的性能监控机制,持续跟踪,主动解决。

One

More Thing

我们计划于近期将马蜂窝 iOS 的启动框架开源,欢迎持续关注马蜂窝公众号动态。期待和大家交流。

本文作者:许旻昊,马蜂窝 iOS 研发技术专家。

2019/5/16 posted in  苹果开发

时间的格式

NSDateFormatter的作用

//NSString * -> NSDate *
- (nullable NSDate *)dateFromString:(NSString *)string;
//NSDate * -> NSString *
- (NSString *)stringFromDate:(NSDate *)date;

常见的日期格式

http://www.cnblogs.com/mailingfeng/archive/2011/07/28/2120422.html

NSString * -> NSDate *

"2016-10-03 14:01:00"

// 时间字符串
NSString *string = "2016-10-03 14:01:00";

// 日期格式化类
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
// 设置日期格式(为了转换成功)
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";

// NSString * -> NSDate *
NSDate *date = [fmt dateFromString:string];

NSLog(@"%@", date);

10月-03号/2016年 09-10:05秒

// 时间字符串
NSString *string = @"10月-03号/2016年 09-10:05秒";

// 日期格式化类
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"MM月-dd号/yyyy年 HH-mm:ss秒";

NSLog(@"%@", [fmt dateFromString:string]);

Tue May 31 17:46:55 +0800 2011

// 时间字符串
NSString *string = @"Tue May 31 17:46:55 +0800 2011";

// 日期格式化类
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";
// fmt.dateFormat = @"EEE MMM dd HH:mm:ss ZZZZ yyyy";
// 设置语言区域(因为这种时间是欧美常用时间)
fmt.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];

NSLog(@"%@", [fmt dateFromString:string]);

1745645645645

// 时间戳 : 从1970年1月1号 00:00:00开始走过的毫秒数

// 时间字符串 - 时间戳
NSString *string = @"1745645645645";
NSTimeInterval second = string.longLongValue / 1000.0;

// 时间戳 -> NSDate *
NSDate *date = [NSDate dateWithTimeIntervalSince1970:second];
NSLog(@"%@", date);

NSCalendar的注意点

#define iOS(version) ([UIDevice currentDevice].systemVersion.doubleValue >= (version))

NSCalendar *calendar = nil;
if ([UIDevice currentDevice].systemVersion.doubleValue >= 8.0) {
    calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
} else {
    calendar = [NSCalendar currentCalendar];
}

NSCalendar *calendar = nil;
if ([NSCalendar respondsToSelector:@selector(calendarWithIdentifier:)]) {
    calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
} else {
    calendar = [NSCalendar currentCalendar];
}

NSDate * -> NSString *

NSDate *date = [NSDate date];

NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"yyyy年MM月dd号 HH:mm:ss";

NSString *string = [fmt stringFromDate:date];

获得日期元素

NSString *string = @"2016-10-03 14:01:00";

NSString *month = [string substringWithRange:NSMakeRange(5, 2)];

NSLog(@"%@", month);
// 时间字符串
NSString *string = @"2016-10-03 14:01:00";

// 日期格式化类
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
// 设置日期格式(为了转换成功)
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";

// NSString * -> NSDate *
NSDate *date = [fmt dateFromString:string];

// 利用NSCalendar处理日期
NSCalendar *calendar = [NSCalendar currentCalendar];
NSInteger month = [calendar component:NSCalendarUnitMonth fromDate:date];
NSInteger hour = [calendar component:NSCalendarUnitHour fromDate:date];
NSInteger minute = [calendar component:NSCalendarUnitMinute fromDate:date];

NSLog(@"%zd %zd %zd", month, hour, minute);
// 时间字符串
NSString *string = @"2016-10-03 14:01:00";

// 日期格式化类
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
// 设置日期格式(为了转换成功)
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";

// NSString * -> NSDate *
NSDate *date = [fmt dateFromString:string];

// 利用NSCalendar处理日期
NSCalendar *calendar = [NSCalendar currentCalendar];

NSCalendarUnit unit = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
NSDateComponents *cmps = [calendar components:unit fromDate:date];

// NSLog(@"%zd %zd %zd", cmps.year, cmps.month, cmps.day);
NSLog(@"%@", cmps);
日期比较

// 时间字符串
NSString *createdAtString = @"2016-10-03 14:01:00";
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";
NSDate *createdAtDate = [fmt dateFromString:createdAtString];

// 手机当前时间
NSDate *nowDate = [NSDate date];

/**
NSComparisonResult的取值
NSOrderedAscending = -1L, // 升序, 越往右边越大
NSOrderedSame, // 相等
NSOrderedDescending // 降序, 越往右边越小
*/
// 获得比较结果(谁大谁小)
NSComparisonResult result = [nowDate compare:createdAtDate];
if (result == NSOrderedAscending) { // 升序, 越往右边越大
NSLog(@"createdAtDate > nowDate");
} else if (result == NSOrderedDescending) { // 降序, 越往右边越小
NSLog(@"createdAtDate < nowDate");
} else {
NSLog(@"createdAtDate == nowDate");
}
// 时间字符串
NSString *createdAtString = @"2016-10-03 14:01:00";
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";
NSDate *createdAtDate = [fmt dateFromString:createdAtString];

// 手机当前时间
// NSDate *nowDate = [NSDate date];

// 获得createdAtDate和nowDate的时间间隔(间隔多少秒)
// NSTimeInterval interval = [nowDate timeIntervalSinceDate:createdAtDate];
NSTimeInterval interval = [createdAtDate timeIntervalSinceNow];
NSLog(@"%f", interval);
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";

// 时间字符串
NSString *createdAtString = @"2016-10-03 14:01:00";
NSDate *createdAtDate = [fmt dateFromString:createdAtString];

// 其他时间
NSString *otherString = @"2016-10-03 14:01:00";
NSDate *otherDate = [fmt dateFromString:otherString];

// 获得NSCalendar
NSCalendar *calendar = nil;
if ([NSCalendar respondsToSelector:@selector(calendarWithIdentifier:)]) {
calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
} else {
calendar = [NSCalendar currentCalendar];
}

// 获得日期之间的间隔
NSCalendarUnit unit = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
NSDateComponents *cmps = [calendar components:unit fromDate:createdAtDate toDate:otherDate options:0];

NSLog(@"%@", cmps);
条件判断的一些注意点

1.判断一个数组中是否有具体内容
1> 正确
if (array.count) {

}

2> 错误
if (array) {

}

2.判断一个字符串是否有具体内容
1> 正确
if (string.length) {

}

2> 错误
if (string) {

}

2018/7/23 posted in  苹果开发

ios NSNumberFormatter 金额格式处理

NSNumberFormatter

可以通过NSNumberFormatter,同样可以设置NSNumber输出的格式
比如123,456,789 或者 123 456 789

NSNumber *number = @(123456789.6789);
2017-10-27 17:20:59.064200+0800 NSNumberFormatter的使用[59895:2506616] No Style                  = 123456790
2017-10-27 17:20:59.064354+0800 NSNumberFormatter的使用[59895:2506616] Decimal Style             = 123,456,789.679
2017-10-27 17:20:59.064436+0800 NSNumberFormatter的使用[59895:2506616] Currency Style            = $123,456,789.68
2017-10-27 17:20:59.064537+0800 NSNumberFormatter的使用[59895:2506616] Percent Style             = 12,345,678,968%
2017-10-27 17:20:59.064626+0800 NSNumberFormatter的使用[59895:2506616] Scientific Style          = 1.234567896789E8
2017-10-27 17:20:59.064720+0800 NSNumberFormatter的使用[59895:2506616] Spell Out Style           = one hundred twenty-three million four hundred fifty-six thousand seven hundred eighty-nine point six seven eight nine
2017-10-27 17:20:59.064828+0800 NSNumberFormatter的使用[59895:2506616] Ordinal Style             = 123,456,790th
2017-10-27 17:20:59.064916+0800 NSNumberFormatter的使用[59895:2506616] Currency ISO Style        = USD123,456,789.68
2017-10-27 17:20:59.065019+0800 NSNumberFormatter的使用[59895:2506616] Currency plural Style     = 123,456,789.68 US dollars
2017-10-27 17:20:59.065107+0800 NSNumberFormatter的使用[59895:2506616] Currency accounting Style = $123,456,789.68

NSNumberFormatter类有个属性numberStyle,它是一个枚举型,设置不同的值可以输出不同的数字格式。该枚举包括

NSNumber *number = @(123456789.6789);
enum {
    NSNumberFormatterNoStyle = kCFNumberFormatterNoStyle, //无格式,四舍五入,输出123456790 
    NSNumberFormatterDecimalStyle = kCFNumberFormatterDecimalStyle, //小数型,保留小数输出123,456,789.679
    NSNumberFormatterCurrencyStyle = kCFNumberFormatterCurrencyStyle, //货币型,加上了人民币标志,原值输出$123,456,789.68
    NSNumberFormatter
    NSNumberFormatterPercentStyle = kCFNumberFormatterPercentStyle,  //百分比型,本身数值乘以100后用百分号表示,输出12,345,678,968%
    NSNumberFormatterScientificStyle = kCFNumberFormatterScientificStyle, //科学计数型,原值表示,输出1.234567896789E8
    NSNumberFormatterSpellOutStyle = kCFNumberFormatterSpellOutStyle  //全拼,原值的中文表示,
};

typedef NSUInteger NSNumberFormatterStyle;

以下是所有API的注释:

NSNumberFormatter *numberFormatter = [NSNumberFormatter new];
numberFormatter.numberStyle.rawValue // numberStyle 0
numberFormatter.locale.localeIdentifier // 语言环境 语言环境
numberFormatter.generatesDecimalNumbers // 是否生成小数 false
numberFormatter.formatterBehavior.rawValue // formatterBehavior 1,040
numberFormatter.negativeFormat // 负格式 "#"
numberFormatter.textAttributesForNegativeValues // textAttributesForNegativeValues nil
numberFormatter.positiveFormat // 正格式 "#"
numberFormatter.textAttributesForPositiveValues // textAttributesForPositiveValues nil
numberFormatter.allowsFloats // 是否允许浮点值 true
numberFormatter.decimalSeparator // 小数分隔符 "."
numberFormatter.alwaysShowsDecimalSeparator // 是否始终显示小数分隔符 false
numberFormatter.currencyDecimalSeparator // 货币小数分隔符 "."
numberFormatter.usesGroupingSeparator // 是否采用分组分隔符 false
numberFormatter.groupingSeparator // 分组分隔符 ","
numberFormatter.zeroSymbol // 零符号 nil
numberFormatter.textAttributesForZero // 文本属性 nil
numberFormatter.nilSymbol // nil符号 ""
numberFormatter.textAttributesForNil // 文本属性 nil
numberFormatter.notANumberSymbol // 非数字符号 "NaN"
numberFormatter.textAttributesForNotANumber // 文本属性 nil
numberFormatter.positiveInfinitySymbol // 正无穷大符号 "+∞"
numberFormatter.textAttributesForPositiveInfinity // 文本属性 nil
numberFormatter.negativeInfinitySymbol // 负无穷大符号 "+∞"
numberFormatter.textAttributesForNegativeInfinity // 文本属性 nil
numberFormatter.positivePrefix // 正前缀 ""
numberFormatter.positiveSuffix // 正后缀 ""
numberFormatter.negativePrefix // 负前缀 "-"
numberFormatter.negativeSuffix // 负后缀 ""
numberFormatter.currencyCode // 货币代码 "USD"
numberFormatter.currencySymbol // 货币符号 "$"
numberFormatter.internationalCurrencySymbol // 国际货币符号 "USD"
numberFormatter.percentSymbol // 百分号符号 "%"
numberFormatter.perMillSymbol // 千分号符号 "‰"
numberFormatter.minusSign // 减号 "-"
numberFormatter.plusSign //加号 "+"
numberFormatter.exponentSymbol // 指数符号 "E"
numberFormatter.groupingSize // 分组大小 0
numberFormatter.secondaryGroupingSize // 第二分组大小 0
numberFormatter.multiplier // 乘数 nil
numberFormatter.formatWidth // 格式宽度 0
numberFormatter.paddingCharacter // 填充字符 "*"
numberFormatter.paddingPosition.rawValue // 填充位置 0
numberFormatter.roundingIncrement // 舍入增量 0
numberFormatter.minimumIntegerDigits // 最小的整数位 0
numberFormatter.maximumIntegerDigits // 最大的整数位 42
numberFormatter.minimumFractionDigits // 最小的小数位数 0
numberFormatter.maximumFractionDigits // 最大的小数位数 0
numberFormatter.minimum // 最小值 nil
numberFormatter.maximum // 最大值 nil
numberFormatter.currencyGroupingSeparator // 货币分组符号 ","
numberFormatter.lenient // false
numberFormatter.usesSignificantDigits // 是否使用有效数字 false
numberFormatter.minimumSignificantDigits // 最小有效数字 1
numberFormatter.maximumSignificantDigits // 最大有效数字 6
numberFormatter.partialStringValidationEnabled // 是否部分字符串验证启用 false

// ==================== 设置属性 ====================

NSNumberFormatter *numberFormatter = [NSNumberFormatter new];
    
    numberFormatter.groupingSize = 4; // 数字分割的尺寸
    numberFormatter.usesGroupingSeparator = true;
    numberFormatter.groupingSeparator = @" "; //数字分割的格式
    
    NSNumber *number = @(123456789);
    
    NSString *spaceStr = [numberFormatter stringFromNumber:number];
    
    NSLog(@"空格 ---- %@",spaceStr);
    
    numberFormatter.numberStyle = NSNumberFormatterDecimalStyle;
    
    // 格式宽度
    numberFormatter.formatWidth = 15;
    
    // 填充符
    numberFormatter.paddingCharacter = @"?";
    
    // 填充位置
    numberFormatter.paddingPosition = kCFNumberFormatterPadBeforeSuffix;
    numberFormatter.positiveSuffix = @"元";
    
    NSLog(@"%@",[numberFormatter numberFromString:@"10000000元"]);  // 10000000
    
    // 貌似没什么用
    numberFormatter.allowsFloats = NO;
    numberFormatter.alwaysShowsDecimalSeparator = NO;
    numberFormatter.maximum = @1000;
    numberFormatter.minimum = @100;
    
    // 小数点样式
    numberFormatter.decimalSeparator = @".";
    
    // 零的样式
    numberFormatter.zeroSymbol       = @"-";
    
    // 前缀和后缀
    numberFormatter.positivePrefix = @"!";
    numberFormatter.positiveSuffix = @"元";
    numberFormatter.negativePrefix = @"@";
    numberFormatter.negativeSuffix = @"亏";
    
    // 指定符号,与我们在前面类方法中说明的一致
    NSLog(@"货币代码%@",numberFormatter.currencyCode);                     // 货币代码USD
    NSLog(@"货币符号%@",numberFormatter.currencySymbol);                   // 货币符号$
    NSLog(@"国际货币符号%@",numberFormatter.internationalCurrencySymbol);   // 国际货币符号USD
    NSLog(@"百分比符号%@",numberFormatter.percentSymbol);                   // 百分比符号%
    NSLog(@"千分号符号%@",numberFormatter.perMillSymbol);                   // 千分号符号‰
    NSLog(@"减号符号%@",numberFormatter.minusSign);                         // 减号符号-
    NSLog(@"加号符号%@",numberFormatter.plusSign);                          // 加号符号+
    NSLog(@"指数符号%@",numberFormatter.exponentSymbol);                    // 指数符号E
    
    // 整数最多位数
    numberFormatter.maximumIntegerDigits = 10;
    
    // 整数最少位数
    numberFormatter.minimumIntegerDigits = 2;
    
    // 小数位最多位数
    numberFormatter.maximumFractionDigits = 3;
    
    // 小数位最少位数
    numberFormatter.minimumFractionDigits = 1;
    
    // 数字分割的尺寸
    numberFormatter.groupingSize = 4;
    
    // 除了groupingSize决定的尺寸外,其他数字位分割的尺寸
    numberFormatter.secondaryGroupingSize = 2;
    
    // 最大有效数字个数
    numberFormatter.maximumSignificantDigits = 12;
    
    // 最少有效数字个数
    numberFormatter.minimumSignificantDigits = 3;
    
    NSLog(@"正数%@,负数%@",[numberFormatter stringFromNumber:@(+12135230.2346)],[numberFormatter stringFromNumber:@(-12135231.2346)]);  // 正数!12,13,5230.2346元,负数@12,13,5231.2346亏
    NSLog(@"零 = %@",[numberFormatter stringFromNumber:@(0)]); //  零 = -
    
    // 舍入值,比如以10为进位值,那么156就进位为160,154进位为150
    numberFormatter.roundingIncrement = @10;
    
    // 舍入方式
    numberFormatter.roundingMode = kCFNumberFormatterRoundHalfUp;
    NSLog(@"%@",[numberFormatter stringFromNumber:@123456.7890]);  // !12,3460元

LINKE:https://github.com/shenyuan000/NSNumberFormatter-

2018/6/15 posted in  苹果开发

NSObject Class 浅析

Objective-C中有两个NSObject,一个是NSObject类,另一个是NSObject协议。而其中NSObject类采用了NSObject协议。在本文中,我们主要整理一下NSObject类的使用。

说到NSObject类,写Objective-C的人都应该知道它。它是大部分Objective-C类继承体系的根类。这个类提供了一些通用的方法,对象通过继承NSObject,可以从其中继承访问运行时的接口,并让对象具备Objective-C对象的基本能力。以下我们就来看看NSObejct提供给我们的一些基础功能。

+load与+initialize

这两个方法可能平时用得比较少,但很有用。在我们的程序编译后,类相关的数据结构会保留在目标文件中,在程序运行后会被解析和使用,此时类的信息会经历加载和初始化两个过程。在这两个过程中,会分别调用类的load方法和initialize方法,在这两个方法中,我们可以适当地做一些定制处理。不当是类本身,类的分类也会经历这两个过程。对于一个类,我们可以在类的定义中重写这两个方法,也可以在分类中重写它们,或者同时重写。

load方法

对于load方法,当Objective-C运行时加载类或分类时,会调用这个方法;通常如果我们有一些类级别的操作需要在加载类时处理,就可以放在这里面,如为一个类执行Swizzling Method操作。

load消息会被发送到动态加载和静态链接的类和分类里面。不过,只有当我们在类或分类里面实现这个方法时,类/分类才会去调用这个方法。

在类继承体系中,load方法的调用顺序如下:

一个类的load方法会在其所有父类的load方法之后调用
分类的load方法会在对应类的load方法之后调用
在load的实现中,如果使用同一库中的另外一个类,则可能是不安全的,因为可能存在的情况是另外一个类的load方法还没有运行,即另一个类可能尚未被加载。另外,在load方法里面,我们不需要显示地去调用[super load],因为父类的load方法会自动被调用,且在子类之前。

在有依赖关系的两个库中,被依赖的库中的类其load方法会优先调用。但在库内部,各个类的load方法的调用顺序是不确定的。

initialize方法

当我们在程序中向类或其任何子类发送第一条消息前,runtime会向该类发送initialize消息。runtime会以线程安全的方式来向类发起initialize消息。父类会在子类之前收到这条消息。父类的initialize实现可能在下面两种情况下被调用:

子类没有实现initialize方法,runtime将会调用继承而来的实现
子类的实现中显示的调用了[super initialize]
如果我们不想让某个类中的initialize被调用多次,则可以像如下处理:

+ (void)initialize { if (self == [ClassName self]) { // ... do the initialization ... } }

因为initialize是以线程安全的方式调用的,且在不同的类中initialize被调用的顺序是不确定的,所以在initialize方法中,我们应该做少量的必须的工作。特别需要注意是,如果我们initialize方法中的代码使用了锁,则可能会导致死锁。因此,我们不应该在initialize方法中实现复杂的初始化工作,而应该在类的初始化方法(如-init)中来初始化。

另外,每个类的initialize只会被调用一次。所以,如果我们想要为类和类的分类实现单独的初始化操作,则应该实现load方法。

如果想详细地了解这两个方法的使用,可以查看《Effective Objective-C 2.0》的第51条,里面有非常详细的说明。如果想更深入地了解这两个方法的调用,则可以参考objc库的源码,另外,NSObject的load和initialize方法一文从源码层面为我们简单介绍了这两个方法。

对象的生命周期

一说到对象的创建,我们会立即想到[[NSObject alloc] init]这种经典的两段式构造。对于这种两段式构造,唐巧大神在他的”谈ObjC对象的两段构造模式“一文中作了详细描述,大家可以参考一下。

本小节我们主要介绍一下与对象生命周期相关的一些方法。

对象分配

NSObject提供的对象分配的方法有alloc和allocWithZone:,它们都是类方法。这两个方法负责创建对象并为其分配内存空间,返回一个新的对象实例。新的对象的isa实例变量使用一个数据结构来初始化,这个数据结构描述了对象的信息;创建完成后,对象的其它实例变量被初始化为0。

alloc方法的定义如下:

+ (instancetype)alloc

而allocWithZone:方法的存在是由历史原因造成的,它的调用基本上和alloc是一样的。既然是历史原因,我们就不说了,官方文档只给了一句话:

This method exists for historical reasons; memory zones are no longer used by Objective-C.
我们只需要知道alloc方法的实现调用了allocWithZone:方法。

对象初始化

我们一般不去自己重写alloc或allocWithZone:方法,不用去关心对象是如何创建、如何为其分配内存空间的;我们更关心的是如何去初始化这个对象。上面提到了,对象创建后,isa以外的实例变量都默认初始化为0。通常,我们希望将这些实例变量初始化为我们期望的值,这就是init方法的工作了。

NSObject类默认提供了一个init方法,其定义如下:

1

  • (instancetype)init
    正常情况下,它会初始化对象,如果由于某些原因无法完成对象的创建,则会返回nil。注意,对象在使用之前必须被初始化,否则无法使用。不过,NSObject中定义的init方法不做任何初始化操作,只是简单地返回self。

当然,我们定义自己的类时,可以提供自定义的初始化方法,以满足我们自己的初始化需求。需要注意的就是子类的初始化方法需要去调用父类的相应的初始化方法,以保证初始化的正确性。

讲完两段式构造的两个部分,有必要来讲讲NSObject类的new方法了。

new方法实际上是集alloc和init于一身,它创建了对象并初始化了对象。它的实现如下:

+ (instancetype)new {
     return [[self alloc] init]; 
}

new方法更多的是一个历史遗留产物,它源于NeXT时代。如果我们的初始化操作只是调用[[self alloc] init]时,就可以直接用new来代替。不过如果我们需要使用自定义的初始化方法时,通常就使用两段式构造方式。

拷贝

说到拷贝,相信大家都很熟悉。拷贝可以分为“深拷贝”和“浅拷贝”。深拷贝拷贝的是对象的值,两个对象相互不影响,而浅拷贝拷贝的是对象的引用,修改一个对象时会影响到另一个对象。

在Objective-C中,如果一个类想要支持拷贝操作,则需要实现NSCopying协议,并实现copyWithZone:【注意:NSObject类本身并没有实现这个协议】。如果一个类不是直接继承自NSObject,则在实现copyWithZone:方法时需要调用父类的实现。

虽然NSObject自身没有实现拷贝协议,不过它提供了两个拷贝方法,如下:

1

  • (id)copy
    这个是拷贝操作的便捷方法。它的返回值是NSCopying协议的copyWithZone:方法的返回值。如果我们的类没有实现这个方法,则会抛出一个异常。

与copy对应的还有一个方法,即:

1

  • (id)mutableCopy
    从字面意义来讲,copy可以理解为不可变拷贝操作,而mutableCopy可以理解为可变操作。这便引出了拷贝的另一个特性,即可变性。

顾名思义,不可变拷贝即拷贝后的对象具有不可变属性,可变拷贝后的对象具有可变属性。这对于数组、字典、字符串、URL这种分可变和不可变的对象来说是很有意义的。我们来看如下示例:

NSMutableArray *mutableArray = [NSMutableArray array]; 
NSMutableArray *array = [mutableArray copy]; 
[array addObject:@"test1"];

实际上,这段代码是会崩溃的,我们来看看崩溃日志:

-[__NSArrayI addObject:]: unrecognized selector sent to instance 0x100107070 
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSArrayI addObject:]: unrecognized selector sent to instance 0x100107070'

从中可以看出,经过copy操作,我们的array实际上已经变成不可变的了,其底层元类是__NSArrayI。这个类是不支持addObject:方法的。

偶尔在代码中,也会看到类似于下面的情况:

@property (copy) NSMutableArray *array;

这种属性的声明方式是有问题的,即上面提到的可变性问题。使用self.array = **赋值后,数组其实是不可变的,所以需要特别注意。

mutableCopy的使用也挺有意思的,具体的还请大家自己去试验一下。

释放

当一个对象的引用计数为0时,系统就会将这个对象释放。此时runtime会自动调用对象的dealloc方法。在ARC环境下,我们不再需要在此方法中去调用[super dealloc]了。我们重写这个方法主要是为了释放对象中用到的一些资源,如我们通过C方法分配的内存空间。dealloc方法的定义如下:

- (void)dealloc

需要注意的是,我们不应该直接去调用这个方法。这些事都让runtime去做吧。

消息发送

Objective-C中对方法的调用并不是像C++里面那样直接调用,而是通过消息分发机制来实现的。这个机制核心的方法是objc_msgSend函数。消息机制的具体实现我们在此不做讨论,可以参考Objective-C Runtime 运行时之三:方法与消息

对于消息的发送,除了使用[obj method]这种机制之外,NSObject类还提供了一系列的performSelector**方法。这些方法可以让我们更加灵活地控制方法的调用。接下来我们就来看看这些方法的使用。

在线程中调用方法

如果我们想在当前线程中调用一个方法,则可以使用以下两个方法:

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay 
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes

这两个方法会在当前线程的Run loop中设置一个定时器,以在delay指定的时间之后执行aSelector。如果我们希望定时器运行在默认模式(NSDefaultRunLoopMode)下,可以使用前一个方法;如果想自己指定Run loop模式,则可以使用后一个方法。

当定时器启动时,线程会从Run loop的队列中获取到消息,并执行相应的selector。如果Run loop运行在指定的模式下,则方法会成功调用;否则,定时器会处于等待状态,直到Run loop运行在指定模式下。

需要注意的是,调用这些方法时,Run loop会保留方法接收者及相关的参数的引用(即对这些对象做retain操作),这样在执行时才不至于丢失这些对象。当方法调用完成后,Run loop会调用这些对象的release方法,减少对象的引用计数。

如果我们想在主线程上执行某个对象的方法,则可以使用以下两个方法:

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait 
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array

我们都知道,iOS中所有的UI操作都需要在主线程中处理。如果想在某个二级线程的操作完成之后做UI操作,就可以使用这两个方法。

这两个方法会将消息放到主线程Run loop的队列中,前一个方法使用的是NSRunLoopCommonModes运行时模式;如果想自己指定运行模式,则使用后一个方法。方法的执行与之前的两个performSelector方法是类似的。当在一个线程中多次调用这个方法将不同的消息放入队列时,消息的分发顺序与入队顺序是一致的。

方法中的wait参数指定当前线程在指定的selector在主线程执行完成之后,是否被阻塞住。如果设置为YES,则当前线程被阻塞。如果当前线程是主线程,而该参数也被设置为YES,则消息会被立即发送并处理。

另外,这两个方法分发的消息不能被取消。

如果我们想在指定的线程中分发某个消息,则可以使用以下两个方法:

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wait 
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array

这两个方法基本上与在主线程的方法差不多。在此就不再讨论。

如果想在后台线程中调用接收者的方法,可以使用以下方法:

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

这个方法会在程序中创建一个新的线程。由aSelector表示的方法必须像程序中的其它新线程一样去设置它的线程环境。

当然,我们经常看到的performSelector系列方法中还有几个方法,即:

- (id)performSelector:(SEL)aSelector 
- (id)performSelector:(SEL)aSelector withObject:(id)anObject 
- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject

不过这几个方法是在NSObject协议中定义的,NSObject类实现了这个协议,也就定义了相应的实现。这个我们将在NSObject协议中来介绍。

取消方法调用请求

对于使用performSelector:withObject:afterDelay:方法(仅限于此方法)注册的执行请求,在调用发生前,我们可以使用以下两个方法来取消:

+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget 
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(id)anArgument

前一个方法会取消所以接收者为aTarget的执行请求,不过仅限于当前run loop,而不是所有的。

后一个方法则会取消由aTarget、aSelector和anArgument三个参数指定的执行请求。同样仅限于当前run loop。

消息转发及动态解析方法

当一个对象能接收一个消息时,会走正常的方法调用流程。但如果一个对象无法接收一个消息时,就会走消息转发机制。

消息转发机制基本上分为三个步骤:

动态方法解析
备用接收者
完整转发
具体流程可参考Objective-C Runtime 运行时之三:方法与消息,《Effective Objective-C 2.0》一书的第12小节也有详细描述。在此我们只介绍一下NSObject类为实现消息转发提供的方法。

首先,对于动态方法解析,NSObject提供了以下两个方法来处理:

+ (BOOL)resolveClassMethod:(SEL)name 
+ (BOOL)resolveInstanceMethod:(SEL)name

从方法名我们可以看出,resolveClassMethod:是用于动态解析一个类方法;而resolveInstanceMethod:是用于动态解析一个实例方法。

我们知道,一个Objective-C方法是其实是一个C函数,它至少带有两个参数,即self和_cmd。我们使用class_addMethod函数,可以给类添加一个方法。我们以resolveInstanceMethod:为例,如果要给对象动态添加一个实例方法,则可以如下处理:

void dynamicMethodIMP(id self, SEL _cmd) { 
    // implementation .... 
} 
+ (BOOL) resolveInstanceMethod:(SEL)aSEL { 
if (aSEL == @selector(resolveThisMethodDynamically)) { 
    class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:"); 
    return YES; 
    } 
return [super resolveInstanceMethod:aSel]; 
}

其次,对于备用接收者,NSObject提供了以下方法来处理:

- (id)forwardingTargetForSelector:(SEL)aSelector

该方法返回未被接收消息最先被转发到的对象。如果一个对象实现了这个方法,并返回一个非空的对象(且非对象本身),则这个被返回的对象成为消息的新接收者。另外如果在非根类里面实现这个方法,如果对于给定的selector,我们没有可用的对象可以返回,则应该调用父类的方法实现,并返回其结果。

最后,对于完整转发,NSObject提供了以下方法来处理

- (void)forwardInvocation:(NSInvocation *)anInvocation

当前面两步都无法处理消息时,运行时系统便会给接收者最后一个机会,将其转发给其它代理对象来处理。这主要是通过创建一个表示消息的NSInvocation对象并将这个对象当作参数传递给forwardInvocation:方法。我们在forwardInvocation:方法中可以选择将消息转发给其它对象。

在这个方法中,主要是需要做两件事:

找到一个能处理anInvocation调用的对象。
将消息以anInvocation的形式发送给对象。anInvocation将维护调用的结果,而运行时则会将这个结果返回给消息的原始发送者。
这一过程如下所示:

- (void)forwardInvocation:(NSInvocation *)invocation { 
    SEL aSelector = [invocation selector]; 
    if ([friend respondsToSelector:aSelector]) 
        [invocation invokeWithTarget:friend]; 
    else 
    [super forwardInvocation:invocation]; 
}

当然,对于一个非根类,如果还是无法处理消息,则应该调用父类的实现。而NSObject类对于这个方法的实现,只是简单地调用了doesNotRecognizeSelector:。它不再转发任何消息,而是抛出一个异常。doesNotRecognizeSelector:的声明如下:

- (void)doesNotRecognizeSelector:(SEL)aSelector

运行时系统在对象无法处理或转发一个消息时会调用这个方法。这个方法引发一个NSInvalidArgumentException异常并生成一个错误消息。

任何doesNotRecognizeSelector:消息通常都是由运行时系统来发送的。不过,它们可以用于阻止一个方法被继承。例如,一个NSObject的子类可以按以下方式来重写copy或init方法以阻止继承:

- (id)copy { 
    [self doesNotRecognizeSelector:_cmd]; 
}

这段代码阻止子类的实例响应copy消息或阻止父类转发copy消息—虽然respondsToSelector:仍然报告接收者可以访问copy方法。

当然,如果我们要重写doesNotRecognizeSelector:方法,必须调用super的实现,或者在实现的最后引发一个NSInvalidArgumentException异常。它代表对象不能响应消息,所以总是应该引发一个异常。

获取方法信息

在消息转发的最后一步中,forwardInvocation:参数是一个NSInvocation对象,这个对象需要获取方法签名的信息,而这个签名信息就是从methodSignatureForSelector:方法中获取的。

该方法的声明如下:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

这个方法返回包含方法描述信息的NSMethodSignature对象,如果找不到方法,则返回nil。如果我们的对象包含一个代理或者对象能够处理它没有直接实现的消息,则我们需要重写这个方法来返回一个合适的方法签名。

对应于实例方法,当然还有一个处理类方法的相应方法,其声明如下:

+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector

另外,NSObject类提供了两个方法来获取一个selector对应的方法实现的地址,如下所示:

- (IMP)methodForSelector:(SEL)aSelector 
+ (IMP)instanceMethodForSelector:(SEL)aSelector

获取到了方法实现的地址,我们就可以直接将IMP以函数形式来调用。

对于methodForSelector:方法,如果接收者是一个对象,则aSelector应该是一个实例方法;如果接收者是一个类,则aSelector应该是一个类方法。

对于instanceMethodForSelector:方法,其只是向类对象索取实例方法的实现。如果接收者的实例无法响应aSelector消息,则产生一个错误。

测试类

对于类的测试,在NSObject类中定义了两个方法,其中类方法instancesRespondToSelector:用于测试接收者的实例是否响应指定的消息,其声明如下:

1

  • (BOOL)instancesRespondToSelector:(SEL)aSelector
    如果aSelector消息被转发到其它对象,则类的实例可以接收这个消息而不会引发错误,即使该方法返回NO。

为了询问类是否能响应特定消息(注意:不是类的实例),则使用这个方法,而不使用NSObject协议的实例方法respondsToSelector:。

NSObject还提供了一个方法来查看类是否采用了某个协议,其声明如下:

+ (BOOL)conformsToProtocol:(Protocol *)aProtocol

如果一个类直接或间接地采用了一个协议,则我们可以说这个类实现了该协议。我们可以看看以下这个例子:

@protocol AffiliationRequests <Joining> 
@interface MyClass : NSObject <AffiliationRequests, Normalization> 
BOOL canJoin = [MyClass conformsToProtocol:@protocol(Joining)];

通过继承体系,MyClass类实现了Joining协议。

不过,这个方法并不检查类是否实现了协议的方法,这应该是程序员自己的职责了。

识别类

NSObject类提供了几个类方法来识别一个类,首先是我们常用的class类方法,该方法声明如下:

+ (Class)class

该方法返回类对象。当类是消息的接收者时,我们只通过类的名称来引用一个类。在其它情况下,类的对象必须通过这个方法类似的方法(-class实例方法)来获取。如下所示:

BOOL test = [self isKindOfClass:[SomeClass class]];

NSObject还提供了superclass类方法来获取接收者的父类,其声明如下:

+ (Class)superclass

另外,我们还可以使用isSubclassOfClass:类方法查看一个类是否是另一个类的子类,其声明如下:

1

  • (BOOL)isSubclassOfClass:(Class)aClass
    描述类

描述类是使用description方法,它返回一个表示类的内容的字符串。其声明如下:

1

  • (NSString *)description
    我们在LLDB调试器中打印类的信息时,使用的就是这个方法。

当然,如果想打印类的实例的描述时,使用的是NSObject协议中的实例方法description,我们在此不多描述。

归档操作

一说到归档操作,你会首先想到什么呢?我想到的是NSCoding协议以及它的两个方法:

initWithCoder:和encodeWithCoder:。如果我们的对象需要支持归档操作,则应该采用这个协议并提供两个方法的具体实现。

在编码与解码的过程中,一个编码器会调用一些方法,这些方法允许将对象编码以替代一个更换类或实例本身。这样,就可以使得归档在不同类层次结构或类的不同版本的实现中被共享。例如,类簇能有效地利用这一特性。这一特性也允许每个类在解码时应该只维护单一的实例来执行这一策略。

NSObject类虽然没有采用NSCoding协议,但却提供了一些替代方法,以支持上述策略。这些方法分为两类,即通用和专用的。

通用方法由NSCoder对象调用,主要有如下几个方法和属性:

@property(readonly) Class classForCoder 
- (id)replacementObjectForCoder:(NSCoder *)aCoder 
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder

专用的方法主要是针对NSKeyedArchiver对象的,主要有如下几个方法和属性:

@property(readonly) Class classForKeyedArchiver 
+ (NSArray *)classFallbacksForKeyedArchiver 
+ (Class)classForKeyedUnarchiver 
- (id)replacementObjectForKeyedArchiver:(NSKeyedArchiver *)archiver

子类在归档的过程中如果有特殊的需求,可以重写这些方法。这些方法的具体描述,可以参考官方文档。

在解码或解档过程中,有一点需要考虑的就是对象所属类的版本号,这样能确保老版本的对象能被正确地解析。NSObject类对此提供了两个方法,如下所示:

+ (void)setVersion:(NSInteger)aVersion 
+ (NSInteger)version

它们都是类方法。默认情况下,如果没有设置版本号,则默认是0.

总结

NSObject类是Objective-C中大部分类层次结构中的根类,并为我们提供了很多功能。了解这些功能更让我们更好地发挥Objective-C的特性。

2018/4/20 posted in  苹果开发

关于CLASS , SEL, IMP的说明

cocoa当中的函数调用,是一种以消息的方式进行的函数调用,这一点与C++,java是有很大差别的。因此该类型的理解,会涉及到三个重要的概念,class,sel,IMP。

class

每个NSObject的第一个成员变量都是class类型的成员,isa,这个isa的对象可以访问到本类的父类,也可以访问到本类的所有方法的列表。

SEL

这个是方法名称的描述。

IMP

这个是具体的方法的地址。

Class 的含义

Class 被定义为一个指向 objc_class的结构体指针,这个结构体表示每一个类的类结构。而 objc_class 在objc/objc_class.h中定义如下:

struct objc_class {
    struct objc_class super_class;  /*父类*/
    const char *name;                 /*类名字*/
    long version;                   /*版本信息*/
    long info;                        /*类信息*/
    long instance_size;               /*实例大小*/
    struct objc_ivar_list *ivars;     /*实例参数链表*/
    struct objc_method_list **methodLists;  /*方法链表*/
    struct objc_cache *cache;               /*方法缓存*/
    struct objc_protocol_list *protocols;   /*协议链表*/
};

由此可见,Class 是指向类结构体的指针,该类结构体含有一个指向其父类类结构的指针,该类方法的链表,该类方法的缓存以及其他必要信息。

NSObject 的class 方法就返回这样一个指向其类结构的指针。每一个类实例对象的第一个实例变量是一个指向该对象的类结构的指针,叫做isa。通过该指针,对象可以访问它对应的类以及相应的父类。如图一所示:

如图一所示,圆形所代表的实例对象的第一个实例变量为 isa,它指向该类的类结构 The object’s class。而该类结构有一个指向其父类类结构的指针superclass, 以及自身消息名称(selector)/实现地址(address)的方法链表。

方法的含义:

注意这里所说的方法链表里面存储的是Method 类型的。图一中selector 就是指 Method的 SEL, address就是指Method的 IMP。 Method 在头文件 objc_class.h中定义如下:

typedef struct objc_method *Method;
typedef struct objc_ method {
    SEL method_name;
    char *method_types;
    IMP method_imp;
};

一个方法 Method,其包含一个方法选标 SEL – 表示该方法的名称,一个types – 表示该方法参数的类型,一个 IMP - 指向该方法的具体实现的函数指针。

SEL 的含义:

在前面我们看到方法选标 SEL 的定义为:

typedef struct objc_selector   *SEL;   

它是一个指向 objc_selector 指针,表示方法的名字/签名。如下所示,打印出 selector。

-(NSInteger)maxIn:(NSInteger)a theOther:(NSInteger)b{
    return (a > b) ? a : b;
}
NSLog(@"SEL=%s", @selector(maxIn:theOther:));

输出:SEL=maxIn:theOther:

不同的类可以拥有相同的 selector,这个没有问题,因为不同类的实例对象performSelector相同的 selector 时,会在各自的消息选标(selector)/实现地址(address) 方法链表中根据 selector 去查找具体的方法实现IMP, 然后用这个方法实现去执行具体的实现代码。这是一个动态绑定的过程,在编译的时候,我们不知道最终会执行哪一些代码,只有在执行的时候,通过selector去查询,我们才能确定具体的执行代码。

IMP 的含义:

在前面我们也看到 IMP 的定义为:

typedef id (*IMP)(id, SEL, ...);

根据前面id 的定义,我们知道 id是一个指向 objc_object 结构体的指针,该结构体只有一个成员isa,所以任何继承自 NSObject 的类对象都可以用id 来指代,因为 NSObject 的第一个成员实例就是isa。

至此,我们就很清楚地知道 IMP 的含义:IMP 是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self 指针), 调用方法的选标 SEL (方法名),以及不定个数的方法参数,并返回一个id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在C语言里面一样使用这个函数指针。

NSObject 类中的methodForSelector:方法就是这样一个获取指向方法实现IMP 的指针,methodForSelector:返回的指针和赋值的变量类型必须完全一致,包括方法的参数类型和返回值类型。

下面的例子展示了怎么使用指针来调用setFilled:的方法实现:

void (*setter)(id, SEL, BOOL);
int i;
 
setter = (void(*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
 
for (i = 0; i < 1000; i++)
    setter(targetList[i], @selector(setFilled:), YES);

使用methodForSelector:来避免动态绑定将减少大部分消息的开销,但是这只有在指定的消息被重复发送很多次时才有意义,例如上面的for循环。

注意,methodForSelector:是Cocoa运行时系统的提供的功能,而不是Objective-C语言本身的功能。

几个重要的辅助函数,可以在使用过程中起到很好的辅助作用,尤其是在动态编译等起到了比较大的作用。

我们可以通过NSObject的一些方法获取运行时信息或动态执行一些消息:

class 返回对象的类;

isKindOfClass 和 isMemberOfClass检查对象是否在指定的类继承体系中;

respondsToSelector 检查对象能否相应指定的消息;

conformsToProtocol 检查对象是否实现了指定协议类的方法;

methodForSelector 返回指定方法实现的地址。

performSelector:withObject 执行SEL 所指代的方法。

2018/4/12 posted in  苹果开发

iOS中的内存管理

既然是内存管理,首先要知道那么内存是用来干什么的?内存具体管理的东西是什么?当然这些东西基本都是常识了,内存当然是用来存储数据的,内存要管理的东西简单的说也就是如何将数据存储到内存中(比如说我们的声明变量就可以将数据存储到内存中),其次存储到内存中的数据怎样释放,什么时候释放,这都是内存管理需要来关心的.

正文:

1.内存中的五大区域

内存分为5个区域,分别指的是----->栈区/堆区/BSS段/数据段/代码段

栈:存储局部变量,当其作用域执行完毕之后,就会被系统立即收回

堆:存储OC对象,手动申请的字节空间,需要调用free来释放

BSS段:未初始化的全局变量和静态变量,一旦初始化就会从BSS段中回收掉,转存到数据段中

数据段:存储已经初始化的全局变量和静态变量,以及常量数据,直到结束程序时才会被立即收回

代码段:代码,直到结束程序时才会被立即收回

2.OC中堆区存储对象的特点

除了堆区,其他区域的中存储的数据,都是又系统自动释放的

堆区中的OC对象,是不会自动释放的,如果不主动释放,那么将在程序结束的时候才去释放

3.引用计数器

首先简单来说一下,每一个OC对象都有一个属性,叫做retainCount,翻译过来也就是引用计数器,类型为unsigned long,占据8个字节,每一个对象负责维护对象所引用的记数值,当一个新的引用指向对象,那么这个对象的引用值增加1,当我们新创建出一个对象的时候这个对象的引用计数器的值默认为1,当这个对象被少一次引用的时候那么就先让这个对象的引用记数值减1,当这个对象所引用的记数值为0的时候,代表这个对象没有被使用,这时系统会自动回收掉此对象,回收这个对象的同时自动调用这个对象的dealloc方法

控制规则:

为对象发送一条retain消息,那么对象的引用计数器的值就会+1

为对象发送一条release消息,那么对象的引用计数器的值就会-1

为对象发送一条retainCount消息,那么就可以得到这个对象的引用计数器的值

4.内存管理的分类

MRC:Manual Reference Counting 手动引用计数器,需要我们手动管理对象引用计数器的值

ARC:Autimatic Reference Counting 自动引用计数器,系统自动的改变对象引用计数器的值(iOS5之后),ARC是基于MRC的

5.内存管理基本原则

新创建一个对象,这个对象的引用计数器的值为1,有对象的创建,就需要匹配一个release

是谁来负责retain的,谁就要负责release,使用的时候retain,不是用的时候release

retain的次数要和release次数想匹配,有加有减,做到平衡

6.野指针

在C中,声明一个指针变量,没有为这个指针变量初始化,那么这个指针变量的值也就是一个垃圾值,指针指向随机的一块空间,那么我们叫做野指针

在OC中,一个指针指向的对象被释放了,那么这个指针叫野指针

7.对象的回收

对象所占用的字节空间,分配给别人使用,系统未分配这块空间被别人使用之前,这个对象的数据仍然处在内存中

8.僵尸对象

已经被收回但是这个对象的数据仍然处在内存中,像这样的对象叫做僵尸对象

僵尸对象有可能可以访问也有可能不可以访问,当僵尸对象所占的内存空间还没有分配给别人使用的时候,这个数据的对象其实仍然存在,通过指针仍然可以找到这个对象,所以说这个时候僵尸对象还可以被访问,当这个僵尸对象已经分配给别人使用的时候,这个对象就不存在了,这个时候不可以被访问

注意:一旦一个对象成为僵尸对象之后,这个对象无论如何都不应该被使用,无论有没有分配给别人使用,都不能用!且不可以复活!

9.避免内存泄漏

有对象的创建,就必须要匹配一个release,retain和release的次数要匹配,不要随便为一个指针赋值为nil,除非这个指针是野指针,在方法中不要随意对传入的对象进行retain

当我们通过野指针去访问僵尸对象的时候会报错,为了避免报错当一个指针成为野指针后,为这个指针赋值为nil

FROM:https://blog.csdn.net/oboe_b/article/details/78140027

2018/4/12 posted in  苹果开发

Face Liveness Detection for iOS

2017/8/2 posted in  苹果开发

get the iOS device CPU architecture in runtime

from:http://stackoverflow.com/questions/19859388/how-can-i-get-the-ios-device-cpu-architecture-in-runtime

#include <sys/types.h>
#include <sys/sysctl.h>
#include <mach/machine.h>

NSString *getCPUType(void)
{
    NSMutableString *cpu = [[NSMutableString alloc] init];
    size_t size;
    cpu_type_t type;
    cpu_subtype_t subtype;
    size = sizeof(type);
    sysctlbyname("hw.cputype", &type, &size, NULL, 0);

    size = sizeof(subtype);
    sysctlbyname("hw.cpusubtype", &subtype, &size, NULL, 0);

    // values for cputype and cpusubtype defined in mach/machine.h
    if (type == CPU_TYPE_X86)
    {
            [cpu appendString:@"x86 "];
             // check for subtype ...

    } else if (type == CPU_TYPE_ARM)
    {
            [cpu appendString:@"ARM"];
            switch(subtype)
            {
                    case CPU_SUBTYPE_ARM_V7:
                    [cpu appendString:@"V7"];
                    break;
                    // ...
            }
    }
    return [cpu autorelease];
}
- (NSString *)getCPUType {
      NSMutableString *cpu = [[NSMutableString alloc] init];
      size_t size;
      cpu_type_t type;
      cpu_subtype_t subtype;
      size = sizeof(type);
      sysctlbyname("hw.cputype", &type, &size, NULL, 0);

      size = sizeof(subtype);
      sysctlbyname("hw.cpusubtype", &subtype, &size, NULL, 0);

      // values for cputype and cpusubtype defined in mach/machine.h
      if (type == CPU_TYPE_X86_64) {
          [cpu appendString:@"x86_64"];
      } else if (type == CPU_TYPE_X86) {
          [cpu appendString:@"x86"];
      } else if (type == CPU_TYPE_ARM) {
          [cpu appendString:@"ARM"];
          switch(subtype)
          {
              case CPU_SUBTYPE_ARM_V6:
                  [cpu appendString:@"V6"];
                  break;
              case CPU_SUBTYPE_ARM_V7:
                  [cpu appendString:@"V7"];
                  break;
              case CPU_SUBTYPE_ARM_V8:
                  [cpu appendString:@"V8"];
                  break;
          }
      }
      return cpu; 
  }
2017/5/3 posted in  苹果开发

Enum-枚举的正确使用-Effective-Objective-C-读书笔记-Item-5

前言

Enum,也就是枚举,从C语言开始就有了,C++、Java、Objective-C、Swift这些语言,当然都有对应的枚举类型,功能可能有多有少,但是最核心的还是一个—规范的定义代码中的状态、选项等“常量”。

Item 5 - Use Enumerations for States, Options, and Status Codes

本节的内容就是如何正确的使用枚举。

状态与选项的区别(states and options)

在用enum之前,我个人觉得,区分一下状态和选项的概念还是很必要的。

状态,同时只能有一种,如“OK”,“Error”,不可能同时是OK和Error。
选项,同时可以有一种或一种以上,如App可以同时支持横屏和竖屏,横屏竖屏在这个时候就是“屏幕方向”的两种不同的选项。

接下来,我们看看如何用枚举定义状态和选项。

enum与状态(states)

不好的做法
经常看到这样的写法:

#define STATE_OK 0
#define STATE_ERROR 1
#define STATE_UNKNOW 2
//直接用int型变量接收
int STATE = STATE_UNKNOW;

这样做有如下“不恰当”:

  • 宏定义没有类型约束,只是单纯的替换。
  • 无法限制状态的所有情况,如,认为的将STATE赋值成3,程序可能就会出错,找不到匹配的状态,因为编译器不会对“STATE = 3;”提出警告。

正确的做法

typedef enum _TTGState {
    TTGStateOK  = 0,
    TTGStateError,
    TTGStateUnknow
} TTGState;
//指明枚举类型
TTGState state = TTGStateOK;

用的时候就如下:

- (void)dealWithState:(TTGState)state {
    switch (state) {
        case TTGStateOK:
            //...
            break;
        case TTGStateError:
            //...
            break;
        case TTGStateUnknow:
            //...
            break;
    }
}

enum与选项 (options)

选项,就是说一个“选项变量”的类型要能够同时表示一个或多个组合的选择,如下例子:

//方向,可同时支持一个或多个方向
typedef enum _TTGDirection {
    TTGDirectionNone = 0,
    TTGDirectionTop = 1 << 0,
    TTGDirectionLeft = 1 << 1,
    TTGDirectionRight = 1 << 2,
    TTGDirectionBottom = 1 << 3
} TTGDirection;

看,这里的选项是用位运算的方式定义的,这样的好处就是,我们的选项变量可以如下表示:

//用“或”运算同时赋值多个选项
TTGDirection direction = TTGDirectionTop | TTGDirectionLeft | TTGDirectionBottom;
//用“与”运算取出对应位
if (direction & TTGDirectionTop) {
    NSLog(@"top");
}
if (direction & TTGDirectionLeft) {
    NSLog(@"left");
}
if (direction & TTGDirectionRight) {
    NSLog(@"right");
}
if (direction & TTGDirectionBottom) {
    NSLog(@"bottom");
}

direction变量的实际内存如下:

这样,用位运算,就可以同时支持多个值。

enum在Objective-C中的“升级版”

一般来说,我们不能指定枚举变量的实际类型是什么,就是说,我们不知道枚举最后是int型,还是其他的什么类型。但是从C++ 11开始,我们可以为枚举指定其实际的存储类型,如下语法:

enum TTGState : NSInteger {/*...*/};

但是,我们在定义枚举的时候如何保证兼容性呢?Foundation框架已经为我们提供了更加“统一、便捷”的枚举定义方法,我们重新定义上面的例子:

//NS_ENUM,定义状态等普通枚举
typedef NS_ENUM(NSUInteger, TTGState) {
    TTGStateOK = 0,
    TTGStateError,
    TTGStateUnknow
};
//NS_OPTIONS,定义选项
typedef NS_OPTIONS(NSUInteger, TTGDirection) {
    TTGDirectionNone = 0,
    TTGDirectionTop = 1 << 0,
    TTGDirectionLeft = 1 << 1,
    TTGDirectionRight = 1 << 2,
    TTGDirectionBottom = 1 << 3
};

所以,在开发Mac、iOS程序中,最好所有的枚举都用“NS_ENUM”和“NS_OPTIONS”定义,保证统一。

总结

充分的用好枚举,可以增强代码的可读性,减少各种“错误”,让代码更加的规范。

2017/4/28 posted in  苹果开发

iOS中的唯一标识

IDFA(广告标识符)-identifierForldentifier

依赖:AdSupport.framework
系统支持:iOS6及以上系统
获取方式:[ASIdentifierManager sharedManager].advertisingIdentifier.UUIDString
定义: 由数字和字母组成的用来标识唯一设备的字符串。
特点

每个设备只有一个IDFA,不同APP在同一设备上获取IDFA的结果是一样的
设备重启不会产生新的IDFA
但IDFA存在重新生成的情况:
用户完全重置系统(设置程序 -> 通用 -> 还原 -> 还原位置与隐私)
用户明确还原广告(设置程序-> 通用 -> 关于本机 -> 广告 -> 还原广告标示符)
注意:Appstore禁止不使用广告而采集IDFA的app上架。请参考:
如何防止应用因获取IDFA被AppStore拒绝

IDFV-identifierForVendor

依赖:UIKit.framework
系统支持:iOS6及以上系统
获取方式:[UIDevice currentDevice].identifierForVendor.UUIDString
定义:由数字和字母组成的用来标识唯一设备的字符串。
特点: 根据vendor的值,如果vendor相同,则返回同一字符串;如果vendor不同,则返回不同的字符串。
vendor解释:英文解释为卖家,小贩。根据xcode文档解释,正常情况下,会根据App Store提供的数据进行判断。但是如果app不是通过app store进行安装的(如企业应用或开发调试阶段),那么会根据bundle ID判断。
判断准则

如:com.example.app1和com.example.app2,只有最后的后缀不同,所以会产生相同的vendor ID

详细参考:iOS唯一标示符引导
:https://possiblemobile.com/2013/04/unique-identifiers/

简书:iOS怎样获取设备唯一标识符

2016/10/27 posted in  苹果开发