在技术面前,我们还是太渺小

很久没有写一些纯原创的文章了。有时候当自己知道的东西越多,才发觉自己越无知。相比那些国外牛逼的大神,自己写的东西根本没什么看头。还不如好好的把那些知识吸收一下,然后分享出来。

如何获取知识,运用知识才是王道,不是你知道得越多就越牛逼,因为知识更新太快了,我已经感觉到力不从心,学海无涯( ⊙ o ⊙ )!

2016-6-27

网易NeteaseAPM iOS SDK技术实现分享

Application Performance Management(APM)

总体感觉

阅读完这篇文章,感觉学到了如何做一些应用统计相关的知识。总结起来是就是AOP统计,OC方法转发,代理模式拦截。

整体设计

  • Hooker
    • Hooker负责在用户感知不到的情况下替换程序原实现,转发消息回调,完成对系统消息的hook和数据的采集。
  • DataBuilder
    • 收集监控数据
  • Persistence
    • 缓存监控数据
  • Poster
    • 上传监控数据到NeteaseAPM;

线程模型: 监控数据的保存和发送都在后台队列中执行,不会影响用户线程。

  • 数据上传规则:
    • 可设置允许数据上传的网络环境;
    • 数据支持批量发送,可自定义发送批量和等待间隔;

一些关键实现

1. 使用面向切面编程(Swizzling)

AOP基本原理:将一个函数替换为一个新函数,新的函数中插入代码片段,然后执行原函数。


  • Objective-C对AOP的支持非常容易

    • Runtime支持方法名和方法实现的分离,Objective-C的方法名类型是SEL,方法实现类型是IMP。
    • 一个Objective-C方法[self setFilled:YES]完全可以用下面的代码代替


  • C的AOP涉及到程序较底层

    • C函数指针的地址可以通过dlsym函数取得,如:


    • C的AOP需要查找到这个函数指针的地址,再使用新的函数指针替换原函数指针。
    • 基于iOS动态链接器的符号绑定,查找函数指针地址的过程见下图:
    • 借助FaceBook开源库FishHook

使用代理模式采集回调消息的数据

这种方式不错。

通过AOP,可以监控指定类的指定方法了,我们可以取得方法调用的时机了, 但是程序中除了方法调用还存在方法回调,这是一种不适合用AOP监控的情况。

例如NSURLConnection的构造方法和start方法可以通过Method Swizzling监控到, 但是回调消息的接收者delegate的类名不固定,可能是任意一个页面实例, 如果还要使用Method Swizzling的方法来监控,会面对未知个数的页面的delegate方法,不是一个好办法。

解决方法是构造一个回调消息的转发者作为代理,在转发者中收集数据,再转发给用户。

下图演示对NSURLConnection的监控,MAM IMP就是被替换过的新的start方法的实现, ProxyDelegate就是消息转发者,负责将回调消息转发给delegate对象:

要实现一个Objective-C的代理,一定要注意以下问题:
  • 系统在向delegate发送消息时会调用-[NSObject respondsToSelector:]方法,所以ProxyDelegate需要重写此方法,才能正确地获取到回调消息;
  • ProxyDelegate没必要实现的用户delegate方法,如鉴权请求,需要借助Objective-C的动态特性,使用-[NSObject forwardInvocation:]方法将delegate能够响应的方法直接转发给用户的delegate;

ProxyDelegate中的forwardInvocation:方法实现:


不止NSURLConnection,CFNetwork的监控也使用了代理模式:


Proxy Stream 拦截read方法,记录stream读取成功的数据长度,再转发给Original Stream。

借助桥接模式,从面向过程CFNetwork到面向对象的数据(这个平时接触得有点少,没怎么懂)

CFNetwork是一个C语言实现的网络系统框架,虽然使用起来比较麻烦,但是可配置的功能更多,由于面向过程难以扩展,没办法选择性地监控和http有关的CFReadStream,而不影响到来自文件或内存的CFReadStream;。为什么没办法了,因为Swizzling关键作用于对象,虽然有c的AOP但非常麻烦。

然面向过程编程难以扩展,但是Objective-C支持从面向过程到面向对象的桥接:

  • Toll-Free Bridging, 它允许某些CoreFoundation类与其对应的Objective-C类互换使用,使我们在面向过程编程和面向对象编程两种编程思想中自由切换。
  • NeteaseAPM的策略:在系统构造http stream时,将一个NSInputStream的子类ProxyStream桥接为CFReadStream,返回给用户, 达到单独监控http stream的效果。
  • 使用Toll-Free Bridging时需要重点关注两种编程思想切换时内存管理机制的不同引起的内存问题;
    • 例如-[NSInputStream propertyForKey]方法和CFReadStreamCopyProperty函数是桥接的,但是它们内存管理方法不同,前者是自动管理,后者是手动管理,后者在调用之后需要使用者在随后手动调用CFRelease
    • 以ProxyStream举例,ProxyStream是CFReadStream的代理,负责管理http stream的数据收集。如果用户对一个http stream执行如下调用:


    • ProxyStream的propertyForKey:方法会因为桥接而被调用,ProxyStream需要从original stream中获取正确的property,如果ProxyStream中这么写


    • 那么这个方法返回的结果会被CFReadStreamCopyProperty随后的CFRelease函数错误地释放,引起内存异常。
      正确的书写方式应该是


    • ProxyStream只需要转发CFReadStreamCopyProperty函数给original stream就可以了。

借助NSURLProtocol的UIWebView监控

NSURLProtocol是监控UIWebView请求最普遍的解决方案。

用户注册了NSURLProtocol,拦截了NeteaseAPM的URLProtocol的请求, 但用户的URLProtocol发送请求时的NSURLConnection仍然会被NeteaseAPM监控到的;

UIWebView的请求比较复杂,下面收集几个容易出错的问题:

  • 某些网页的验证码的请求可能是一个阻塞主线程的请求,在URLProtocol的网络请求需要放在后台执行,否则会被阻塞住而无法得到响应。
  • 重定向的网页请求需要转发给网页,APM不能监控。

QA环节

验证码请求阻塞主线程的例子吗?

这个问题的现象是,UIWebView访问https://reg.163.com时,点击注册邮箱,填写完成后点击注册,界面卡住一段时间后无响应,没有跳转。

调查发现:UIWebView访问https://reg.163.com/services/checkSsnAll?isret=1&username=XXX 时,会在主线程使用ajax向服务器发送验证码请求。由于这个请求阻塞了主线程,NeteaseAPM的数据发送失败,导致这么问题。

监控数据上报,会缓存到本地,然后再上报么?会有流量问题么?

是的,数据上传使用了短码,并且压缩上传,一次上传的数据大小在60比特。并且可以设置上传的网络环境。流量问题可以通过各种方式解决,NeteaseAPM支持自定义数据发送方式,有些产品有免流量的通道,NeteaseAPM就把数据交给产品发送了。

页面加载监控,hook那些方法?

页面监控的方法是viewDidLoad,viewDidAppear,viewDidDisappear等方法。如果页面过于复杂渲染时间长,页面的生命周期方法会正常执行的,可以准确地取得页面的状态,慢加载也是通过页面加载时间诊断的。

流媒体能监控吗? 我尝试过oneAPM,用AVPlayerLayer实现的流媒体没法监控。

流媒体监控可以使用NSURLProtocol监控到,但是目前还没有针对这方面的测试。AVPlayerLayer的请求非常频繁,可能会被过滤掉,有同事反映,他们后来也选择了过滤掉AVPlayerLayer触发的请求。

回调的信息数据,是程序退出后台的时候发送,还是回调以后就会发送统计服务器。

数据保存和数据发送是两个队列,数据保存在请求结束后执行,数据发送的规则是批量发送,支持自定义。

上传到平台的didload统计时长应该有对应的页面唯一标识吧,这个标记是在业务页面手动加的,还是hook页面方法自动加的,如果是自动加的,页面唯一标识是怎么在hook里传递记录的?

页面唯一标识保存在一个容器中,这个容器维护着页面引用和页面标识的对应关系,hook中需要根据页面查询页面标识。

页面类字符串映射页面唯一标识么?

会使用页面的指针映射页面唯一标识,防止同时有多个同类页面。


iOS瘦身之删除FrameWork中无用mach-O文件

这篇文章讲的内容,关于Mach-O确实了解不是很多。读了这篇文章也算是弥补了一下这方面的短板。

什么是mach-O?

Mach-O格式全称为Mach Object文件格式的缩写,是mac上可执行文件的格式,类似于windows上的PE格式 (Portable Executable ), linux上的elf格式 (Executable and Linking Format)。

上面第一个图是苹果给出的mach-O格式的示意图,而第二个图是我们使用machOView来分析某个可执行文件中的armv7的格式。

在machO这其中包含了很多的有效的信息,包括字符串,代码段,oc类,oc协议等各种的信息,利用这些信息我们也做到分析代码或者程序逻辑的作用,比如,下面这个数据就是我从这个machO文件里面导出来的,获取到了某个framework一个OC类中的所有基本元素。

那什么又是FatFile/FatBinary

简单来说,就是一个由不同的编译架构后的Mach-O产物所合成的集合体。例如上面我就只截取armv7的Mach-O格式座位示例, 而实际上常用的还有arm64/x86_64/i386等格式。

可以看到arm64/armv7架构的存在。


FrameWork跟最终可执行文件的区别在哪里?

frameWork也是另外一种情况的Mach-O集合体,是由多个不同的子mach-O文件所组合而成的,他们可以单独的拆开,而可执行文件则把同一架构下的所有Mach-O文件都进行了合并,他们不能拆开,如果想要更加清晰的定义的话,可以去研究一下苹果的定义,这里不做过多的阐述。

估计看了也不会再项目中用到,简单了解一下这个东东!


XCode基本使用及调试技巧

基本调试

如果在watch窗口想查看寄存器的内容,可以将调试窗口左下角的Auto选成All。

断点

Xcode支持很多种不同类型的断点,包括普通断点、条件断点、符号断点、watch断点、线程断点,下面一个个来介绍,同时会根据工作中的经验介绍下相关断点的使用场景。

条件断点

在我认识条件断点之前我都是在代码中加一个if,然后在if里面写一行log,最后在调试的时候在log那一行下断点的,不知你当年是怎么下的。

条件断点和普通断点相比起来只是多了一个条件而已,右键普通断点 -> Edit Breakpoint

这样一来只有满足条件的情况下断点才会断下来,条件断点对于一个函数重复多次但是只需要调试其中某一次的情况很适用。如某个用户反馈他的通讯录中的一个好友签名显示有问题,就可以对好友的用户名下条件断点直接调试有问题的用户信息。另外条件断点对于一些因为异常数据导致的bug的调试也是很有用的。

符号断点

符号断点其实就是对一个特定的函数名下断点,这里得方法可以是OC的方法也可以是一个C++的函数名。
在导航区选择断点tab页 -> 点击最下面的加号 -> Add Symbolic Breakpoint

设置符号断点可以输入类名+函数名,也可输入函数名,Xcode会自动匹配在不同类中同名的方法进行断点。

一旦onOK方法被调用就会命中断点。
符号断点在调试一些没有源码的模块时比较有用,比如调试一个第三方提供的Lib库,或者系统的模块,可以在相应函数处下断点,可以大概调试清楚程序的运行流程。同时可以在断点的时候查看到参数信息。

异常断点

异常断点在调试时如果程序抛出异常,导致程序退出,下个异常断点很快就能定位问题了。
导航区 -> 断点tab –> 左下角加号 -> Add Exception Breakpoint

可以编辑exception breakpoint

可以选择Exception类型,选择在抛出异常或者捕获异常的时候断点等。
注意:有的程序会使用异常来组织程序逻辑,比如微信的扫一扫,所以如果Exception选了All,那么异常断点会一直被触发,所以这种情况下可以只选择Objective-c异常。

watch断点

顾名思义watch断点就是当某个变量发声改变的时候触发的断点。
在Xcode的watch窗口-> 右键需要watch的变量 -> watch “XXX”

如例子中,当m_nsContent变量发声变化时调试器会自动断下来,同时输出变化信息。

watch断点对于要跟踪某个变量或者某个状态的变化是非常有用的,可以方便的用watch断点跟踪到到底哪些地方改变了变量的值。
需要注意的一点是watch断点是有个数限制的,在intel CPU上最大是4个,在ARM CPU上最大是2个。

线程断点

线程断点在调试多线程代码的时候用处很大,一段代码可能会被多个线程同时执行,如果下普通断点,那么你会在不同线程之前切来切去,最后自己都迷糊了,所以最好就是使用线程断点。

如何下线程断点呢?调试区域右边控制台输出 -> breakpoint set –f 文件名–l 行号–t 线程id

这样一来只有当tid为0x2dde99的线程执行这行代码是调试器才会断下来。

  • 断点后的action:
    断点后的action,其实就是当断点被触发可以执行一些操作。
    右键断点 -> Edit breakpoint -> Add action


Action对于调试是非常有用的,比如你需要在断点的地方频繁打印某一对象时,你不用新加log,然后重新编译,直接加一条Debugger Command就可以

Action对于调试是非常有用的,比如你需要在断点的地方频繁打印某一对象时,你不用新加log,然后重新编译,直接加一条Debugger Command就可以


每次断点触发的时候都会输出m_text对象。
如果觉得仅仅输出对象信息不够,还想加一些自己指定的内容,可以使用Log Message。

常用命令

p命令

Xcode里可以使用p命令查看基本数据类型的值,使用po命令查看oc对象。简单的查看一个变量或者一个OC对象的值在watch窗口完全可以满足,但是如果需要查看一个oc对象的属性,或者一个oc对象方法的返回值怎么办呢?p和po命令后面都可以接相应的表达式,如:

expr命令

expr命令全称expression,可以在调试时动态的执行表达式,同时打印出结果。常用来动态修改变量的值。

使用expr命令动态修改变量的值,可以在调试的时候覆盖一些异常路径,对调试异常处理代码很有用。

call命令

除了动态修改变量的值,Xcode还支持动态调用函数,可以在不增加代码,不重新编译的情况下动态调用一个方法。


动态的将m_text从父view中移除。

不常用的

  • image命令:image命令可以列出当前App中的所有模块,可以查找一个地址对应的代码位置。
    在调试越狱插件时,可以方面的用image list命令查看越狱插件是否注入自己的App。
    当遇到crash时,查看线程栈只能看到栈帧的地址,使用“image lookup –address 地址”命令可以方便的定位到这个地址对应的代码行。
  • bt命令:bt命令可以查看线程的堆栈信息,该信息也可以在导航区的Debug Navigator看到。
    bt 打印当前线程栈
    btall 打印所有线程栈

-----------------分割线-------------------
介绍了基本的调试技巧,下面介绍一下不同场景下的调试经验
-----------------分割线-------------------

多线程

在调试多线程bug时,很多时候bug在调试的时候是不会出现的,一旦关闭断掉直接运行bug就出现了。这种问题大部分是因为调试影响了多线程的执行顺序。

对于这种问题可以在关键点输出log,既然提到log,之前介绍的断点action里的LogMessage自然会派上用场,你不需要在代码中添加冗余的log即可以调试。

另外对于多线程问题合理的使用线程断点和条件断点也是很有帮助的。

UI调试

控件信息:

查看控件信息无外乎还是使用p和po命令。同样可以使用expr命令修改控件属性,如内容、左边、大小等,这样可以不重启程序就能看到界面变化。

注:有兴趣的同学可以自行google一个名叫injectionPlugin的xcode插件,他能够在改变代码后不重启APP即可让改变生效。

界面结构:

查看界面结构有一个比较有用的命令:po [view recursiveDescription],该命令可以打印出view的所有子view的结构关系,对于调试界面层级关系很有用。但是再清楚也是日志输出,还是不够直观,有兴趣的同学可以尝试神器reveal。

Xcode5新特性快速预览

Xcode5支持在调试时对变量进行快速预览,调试时将鼠标放在变量上,然后点击快速预览按钮即可看到控件的显示。

使用符号断点跟踪UI变化:

对于一些系统控件的信息,如果发现最终显示和自己设置的不一样,可以使用符号断点,在一些设置函数下断点,这样就可以很清晰的看到是从哪里改变了这个属性的值。比如一个UIButton的title在显示的时候和设置时不一样,这个时候只需要对UIButton的setTitle设置断点即可发现问题。

其他

打开异常断点

一般情况下很多crash都是因为异常导致的,打开异常断点可以在抛出异常的时候断下,然后查看堆栈即可确认是那一行代码引起的异常。

如果日志窗口没有输出异常信息,可以切到抛异常的那一帧,然后po $r0或者po $eax即可看到异常信息。

野指针导致的crash
野指针导致的crash一般都是因为释放后使用,比如一个对象已经释放了,但还有持有该对象的指针调用方法等等。这一类crash如果可以复现的话可以利用instrument 的zombies模板,他可以看到对象的每一次retain和release。对于野指针问题定位起来很方便。

野指针没有Zombie可以参考如何在LLDB下排查message sent to deallocated instance问题


2016-6-28

Type Encodings

@encode,@编译器指令 之一,返回一个给定类型编码为一种内部表示的字符串(例如,@encode(int) → i),类似于 ANSI C 的 typeof 操作。苹果的 Objective-C 运行时库内部利用类型编码帮助加快消息分发。

示例代码:

NSLog(@"int        : %s", @encode(int));
NSLog(@"float      : %s", @encode(float));
NSLog(@"float *    : %s", @encode(float*));
NSLog(@"char       : %s", @encode(char));
NSLog(@"char *     : %s", @encode(char *));
NSLog(@"BOOL       : %s", @encode(BOOL));
NSLog(@"void       : %s", @encode(void));
NSLog(@"void *     : %s", @encode(void *));

NSLog(@"NSObject * : %s", @encode(NSObject *));
NSLog(@"NSObject   : %s", @encode(NSObject));
NSLog(@"[NSObject] : %s", @encode(typeof([NSObject class])));
NSLog(@"NSError ** : %s", @encode(typeof(NSError **)));

int intArray[5] = {1, 2, 3, 4, 5};
NSLog(@"int[]      : %s", @encode(typeof(intArray)));

float floatArray[3] = {0.1f, 0.2f, 0.3f};
NSLog(@"float[]    : %s", @encode(typeof(floatArray)));

typedef struct _struct {
    short a;
    long long b;
    unsigned long long c;
} Struct;
NSLog(@"struct     : %s", @encode(typeof(Struct)));

值得注意的:

指针的标准编码是加一个前置的 ^,而 char * 拥有自己的编码 *。这在概念上是很好理解的,因为 C 的字符串被认为是一个实体,而不是指针。

BOOL 是 c,而不是某些人以为的 i。原因是 char 比 int 小,且在 80 年代 Objective-C 最开始设计的时候,每一个 bit 位都比今天的要值钱(就像美元一样)。BOOL 更确切地说是 signed char (即使设置了 -funsigned-char 参数),以在不同编译器之间保持一致,因为 char 可以是 signed 或者 unsigned。

直接传入 NSObject 将产生 #。但是传入 [NSObject class] 产生一个名为 NSObject 只有一个类字段的结构体。很明显,那就是 isa 字段,所有的 NSObject 实例都用它来表示自己的类型。

方法编码

比较底层,了解一下。实际应用只有在消息转发中用到。


@ 编译器指令

  • 方括号
  • 长的荒唐的方法名
  • @指令

@ 或者 "at" 符号编译器指令对于理解 Objective-C 的格式以及其起源和底层机制非常重要。它是使得 Objective-C 如此强大,具有表现力,并仍能一路编译成底层 C 语言的关键。

@简写符号它们涵盖了广泛的实用性却也由晦涩难懂的用法,从主要的用途如 @interface 和 @implementation 到你的整个职业生涯或许都不会遇到的如 @defs 和 @compatibility_alias。

  • 总结:
接口与实现
@interface...@end
@implementation...@end
@class

实例变量可视性
@public
@package
@protected
@private

属性
@property
@synthesize
@dynamic

协议
@protocol
@required
@optional

异常处理
@try
@catch
@finally
@throw

对象常量
@""
@42, @3.14, @YES, @'Z'
@[]
@{}
@()

Objective-C 常量
@selector()
@protocol()

C 常量
@encode():返回一个类型的类型编码。这个类型值可以用于 NSCoder -encodeValueOfObjCType:at 中的第一个参数编码。
@defs():返回一个 Objective-C 类的布局。比如,定义一个与 NSObject 有相同布局的 struct,你只需要这样:

优化
@autoreleasepool{}
@synchronized{}

兼容
@compatibility_alias:允许现有类有不同的名称作为别名。

NameSpacing

C和Objective-C中的类型

Objective-C是直接建立在C语言之上的,一个重要的原因是Objective-C和C语言共用一个类型系统,他们都要求标识符是全局唯一的。

你可以自己定义一个和@interface同名的静态变量,编译之后你会得到一个错误:

@interface XXObject : NSObject
@end

static char * XXObject;//将“XXObject”重新定义为不同的符号

也就是说,Objective-C的runtime在C语言的类型系统上又创建了一个抽象层

通过Objective-C的环境,程序能区别所有相同名字的类,协议,类别,实例变量,实例方法和类方法。

前缀

在Objective-C应用中的所有类名都必须是全局唯一的。由于很多不同的框架中会有一些相似的功能,所以在名字上也可能会有重复(users, views, requests / responses 等等),所以苹果官方文档规定类名需要有2-3个字母作为前缀。

类前缀

第三方类前缀

由于CocoaPods的出现和大量新的iOS开发者的涌现,开源代码的遍布,第三方代码在很大程度上对苹果和其余的Objective-C开发社区来说已经不是问题了。最近苹果官方的命名指南也发生了变化,它将三个字母作为前缀的建议只是做为一个习惯做法。

正因为这样,那些已经存在的第三方库依然使用2个字母作为前缀,你可以参考一些那些在GitHub上得到很多star的Objective-C的仓库

对于那些针对特殊功能而写的第三方库的作者,可以考虑在下一次主要升级时使用@compatibility_alias来为那些使用者们提供一个天衣无缝的转移路径。

方法前缀

不仅是类容易造成命名冲突,selectors也很容易造成命名冲突,甚至方法比类会有更多的问题。 考虑一下这个category:

@interface NSString (PigLatin)
- (NSString *)pigLatinString;
@end

如果 -pigLatinString方法被另一个category实现了(或者以后版本的iOS或者OS X 在NSString类中也添加了同样名字的方法),那么调用这个方法就会得到未定义的行为错误,因为我们不能保证在runtime中哪个方法会先被定义。

我们可以通过在方法名前加前缀来避免这个问题,就像加这个类名一样(在类别名前加前缀也是个好办法):

@interface NSString (XXXPigLatin)
- (NSString *)xxx_pigLatinString;
@end

苹果官方建议所有category方法都要使用前缀,这个建议比类名需要加前缀的规定更加广为人知和接受。

当我在编译器的环境参数中将OBJC_PRINT_REPLACED_METHODS这个参数设置为YES,那我们就能在编译的时候检测方法名是否有冲突。实际上,方法名的冲突是很少发生的,而且在发生的时候,他们通常会得到一个needlessly duplicated across dependencies的提示。即使发生最坏的情况,程序在运行是出现异常,那么很可能是两个方法名一样,那么他们做的事情也是一样的,所以结果也不会有什么变化。

Swizzling

在Swizzling时,方法名加前缀或者后缀也是非常有必要的,这个我在上周关于swizzling的文章中提到过。

@implementation UIViewController (Swizzling)

- (void)xxx_viewDidLoad {
    [self xxx_viewDidLoad];

    // Swizzled implementation
}

BOOL / bool / Boolean / NSCFBoolean

在Objective-C中,当遇到处理真值的参数,属性和实例变量时,使用类型BOOL。当分配字面值时,使用宏YES和NO。

错误问题的错误答案

  • Wrong
if ([a isEqual:b] == YES) {
  ...
}

因为BOOL在实际中被typedef为signed char,这句话非常重要,非常重要,本质。

从算数运算中提取真值并不是一个好主意。就像这句话"Colorless green ideas sleep furiously",它可能符合语法(毕竟,BOOL是一个signed char,因此它可以被视为数字),但是它在语意上完全说不通。

所以,取而代之的方法是,使用==输出的结果,或者将数值通过!(或者!!)转换成布尔值。

The Truth About NSNumber and BOOL

NSLog(@"%@", [@(YES) class]);

输出: __NSCFBoolean

我们都以为NSNumber将原始数封装为对象表示。其它任何由integer-和float-衍生出来的NSNumber对象都显示它的类为__NSCFNumber。

NSCFBoolean是NSNumber类簇中的一个私有的类。它是通往CFBooleanRef类型的桥梁,它被用来给Core Foundation的属性列表和集合封装布尔数值。CFBoolean定义了常量kCFBooleanTrue和kCFBooleanFalse。因为CFNumberRef和CFBooleanRef在Core Foundation中属于不同种类,这样是有道理的,它们在NSNumber被以不同的衔接类呈现。


Equality

首先,我们要对于 相等性 和 本体性 进行一下区分。

当两个物体有一系列相同的可观测的属性时,两个物体可能是互相相等或者等价 的。但这两个物体本身仍然是不同的 ,它们各自有自己的本体 。

在编程中,一个对象的本体和它的内存地址是相关联的。

NSObject 使用 isEqual:这个方法来测试和其他对象的相等性。在它的基类实现中,相等性检查本质上就是对本体性的检查。两个 NSObject 如果指向了同一个内存地址,那它们就被认为是相同的。

对于 NSArray,NSDictionary 和 NSString 这种容器类来说,大家所期望的,同时也是更加有用的行为,应该是进行深层的相等性检查,对于集合中的每个成员都进行判断。

NSObject 的子类在实现它们自己的 isEqual: 方法时,应该完成下面的工作:

  • 实现一个新的 isEqualTo__ClassName__ 方法,进行实际意义上的值的比较。
  • 重载 isEqual: 方法进行类和对象的本体性检查,如果失败则回退到上面提到的值比较方法。
  • 重载 hash 方法,在下一个部分会详细介绍。

下面是 NSArray 可能使用的解决方案(对于这个例子来说,我们暂时忽略掉 NSArray 实际上是一个类簇,真正的实现会比这个复杂得多)。

@implementation NSArray (Approximate)
- (BOOL)isEqualToArray:(NSArray *)array {
  if (!array || [self count] != [array count]) {
    return NO;
  }

  for (NSUInteger idx = 0; idx < [array count]; idx++) {
      if (![self[idx] isEqual:array[idx]]) {
          return NO;
      }
  }

  return YES;
}

- (BOOL)isEqual:(id)object {
  if (self == object) {
    return YES;
  }

  if (![object isKindOfClass:[NSArray class]]) {
    return NO;
  }

  return [self isEqualToArray:(NSArray *)object];
}
@end

系统继承NSobject提供了:

NSAttributedString -isEqualToAttributedString:
NSData -isEqualToData:
NSDate -isEqualToDate:
NSDictionary -isEqualToDictionary:
NSHashTable -isEqualToHashTable:
NSIndexSet -isEqualToIndexSet:
NSNumber -isEqualToNumber:
NSOrderedSet -isEqualToOrderedSet:
NSSet -isEqualToSet:
NSString -isEqualToString:
NSTimeZone -isEqualToTimeZone:
NSValue -isEqualToValue:

NSString

NSString *a = @"Hello";
NSString *b = @"Hello";
BOOL wtf = (a == b); // YES

首先我们要明确一点,比较 NSString 对象正确的方法是 -isEqualToString:。任何情况下都不要直接使用 == 来对 NSString 进行比较。

都来源于一种称为字符串驻留的优化技术,它把一个不可变字符串对象的值拷贝给各个不同的指针。NSString *a 和 *b都指向同样一个驻留字符串值 @"Hello"。 注意所有这些针对的都是静态定义的不可变字符串。

有意思的是,Objective-C 选择器的名字也是作为驻留字符串储存在一个共享的字符串池当中的。

散列(这块就不用看了)


rand(3) / random(3) / arc4random(3) / et al.

如何生成一个 Objective-C 的随机数?

具体而言,产生一个 0 和 N - 1 之间的随机数,使用 arc4random_uniform(),从而避免模偏差(modulo bias)。

0到N-1NSUInteger r = arc4random_uniform(N);

0到NNSUInteger r = arc4random_uniform(N) + 1;

  • 0 到 1 之间的随机浮点数(double):如果你要生成一个随机 double 或 float,另一个很好的选择是功能较模糊的 rand48 家族,包括 drand48(3)。
    • srand48(time(0));double r = drand48();

我如何从一个 NSArray 选择一个随机元素?

使用 arc4random_uniform(3) 产生一个在非空数组范围内的随机数

if ([array count] > 0) {
  id obj = array[arc4random_uniform([array count])];
}

我如何随机排序一个 NSArray?

NSMutableArray *mutableArray = [NSMutableArray arrayWithArray:array];
NSUInteger count = [mutableArray count];
// See http://en.wikipedia.org/wiki/Fisher–Yates_shuffle
if (count > 1) {
  for (NSUInteger i = count - 1; i > 0; --i) {
      [mutableArray exchangeObjectAtIndex:i withObjectAtIndex:arc4random_uniform((int32_t)(i + 1))];
  }
}

NSArray *randomArray = [NSArray arrayWithArray:mutableArray];

为什么要使用 arc4random(3),而不是 rand(3) 或 random(3)?

arc4random 不需要初始种子(用 srand 或 srandom),使它更加容易使用。
arc4random 范围可达 0x100000000 (4294967296),而 rand 和 random 的上限在 RAND_MAX = 0x7fffffff (2147483647)。
rand 经常定期被周期低位的方式,使其更可预测执行。

什么是 rand(3), random(3), 和 arc4random(3),以及它们从哪里来的?

rand 是一个标准的 C 函数。
random 是定义为 POSIX 标准的一部分。
arc4random 是在 BSD 和派生平台。

补充

ios 有如下三种随机数方法:
  1. srand((unsigned)time(0)); //不加这句每次产生的随机数不变
    int i = rand() % 5;
  2. srandom(time(0));
    int i = random() % 5;
  3. int i = arc4random() % 5 ;
    注:rand()和random()实际并不是一个真正的伪随机数发生器,在使用之前需要先初始化随机种子,否则每次生成的随机数一样。
    arc4random() 是一个真正的伪随机算法,不需要生成随机种子,因为第一次调用的时候就会自动生成。而且范围是rand()的两倍。在iPhone中,RAND_MAX是0x7fffffff (2147483647),而arc4random()返回的最大值则是 0x100000000 (4294967296)。
    精确度比较:arc4random() > random() > rand()。
    常用方法:arc4random
    1、获取一个随机整数范围在:[0,100)包括0,不包括100
    int x = arc4random() % 100;
    2、 获取一个随机数范围在:[500,1000],包括500,包括1000
    int y = (arc4random() % 501) + 500;
    3、获取一个随机整数,范围在[from,to],包括from,包括to
    -(int)getRandomNumber:(int)from to:(int)to
    {
    return (int)(from + (arc4random() % (to – from + 1)));
    }
    随机数的范围是否包含边界函数,我个人有点迷茫,因为用简单的数进行测试时第二种方法包含了边界函数,第三种直接错误;但是从别的地方看到有的说包含边界函数有的说不包含,所以大家看的时候自己记得简单测试来避免一些不必要的麻烦

线程安全类的设计

有些费脑,看了这么多,发觉能记住的太少,大概有一些概念而已。

推荐阅读更多精彩内容