├── Block类型.md ├── BugByNSUInteger.md ├── CLLocationManager导致内存泄漏.png ├── CoreAnimation.md ├── Fastlane命令.md ├── OC消息转发机制.md ├── Other Linker Flags.md ├── PickerView.md ├── RAC.md ├── README.md ├── ResponderChain+Strategy+MVVM实现一个优雅的TableView.md ├── Runtime源码 +load 和 +initialize.md ├── Runtime源码 Category(分类).md ├── Runtime源码 autoreleasepool.md ├── Runtime源码 protocol(协议).md ├── Runtime源码 成员变量与属性.md ├── Runtime源码 方法调用的过程.md ├── Runtime源码 类、对象、isa.md ├── SDWebImage源码解析.md ├── SwiftGuide.md ├── UITableView代理执行顺序.md ├── iOS 一个轻量级的组件化思路 .md ├── iOS事件处理.md ├── iOS仿滴滴预约用车时间选择器.md ├── iOS各种时间选择Picker.md ├── iOS启动优化.md ├── iOS崩溃类型.md ├── iOS编译过程.md ├── iOS设计模式.md ├── iOS静态库与动态库的使用.md ├── 公开库命令.md ├── 图解HTTP.md ├── 极光推送.md └── 私有库命令.md /Block类型.md: -------------------------------------------------------------------------------- 1 | # Block的类型 2 | ### 前言 3 | Block在iOS日常开发中极其常见,大家应该几乎都使用过,比较熟悉它的用法,而且知道Block可能引起循环引用,今天来聊聊Block,以及Block造成内存泄露的根本原因。 4 | 5 | ### Block是什么 6 | 首先,Block和普通实例一样是是一个对象,他有自己的isa指针。 7 | 它就是一个里面存储了指向定义代码块的函数指针和block外部上下文变量信息的结构体。通过断点我们看到block的isa指针,如下图: 8 | 9 | ![isa](https://user-gold-cdn.xitu.io/2019/2/23/16918a290bdf247f?w=736&h=548&f=png&s=135828) 10 | 11 | 我们发现block的类型其实是不同的,这是为什么接下来我们看看Block到底有哪些类型。 12 | 13 | ### Block的类型 14 | 我们通过实际例子看看的各种类型的block 15 | 16 | - NSMallocBlock 17 | 18 | ``` 19 | - (void)NSMallocBlock { 20 | int tempInt = 1; 21 | void (^block)(void) = ^ { 22 | NSLog(@"----------%d----------\n\n",tempInt); 23 | }; 24 | block(); 25 | [self printBlockSuperClass:block]; 26 | } 27 | ``` 28 | 29 | 结果:__NSMallocBlock__ -> __NSMallocBlock -> NSBlock -> NSObject 30 | 31 | - NSStaticBlock 32 | 33 | ``` 34 | - (void)NSStaticBlock { 35 | int tempInt = 1; 36 | __weak void (^block)(void) = ^ { 37 | NSLog(@"----------%d----------\n\n",tempInt); 38 | }; 39 | block(); 40 | 41 | [self printBlockSuperClass:block]; 42 | } 43 | ``` 44 | 结果:__NSStackBlock__ -> __NSStackBlock -> NSBlock -> NSObject 45 | 46 | 47 | - NSGlobalBlock 48 | 49 | ``` 50 | - (void)NSGlobalBlock { 51 | void (^block)(int a) = ^ (int a){ 52 | NSLog(@"----------%d----------\n\n",a); 53 | }; 54 | block(1); 55 | 56 | [self printBlockSuperClass:block]; 57 | } 58 | ``` 59 | 60 | 结果:__NSGlobalBlock__ -> __NSGlobalBlock -> NSBlock -> NSObject 61 | 62 | 我们发现: 63 | 64 | 1. 当没有外部变量时,block为__NSMallocBlock,它由开发者创建,存储在堆内存上。 65 | 2. 当有```__weak```修饰时block为__NSStackBlock,存储在栈区。 66 | 3. 当block有参数时(捕获了外部变量时)block为__NSGlobalBlock,存储在全局区。 67 | 68 | ### 属性关键字和外部变量类型对Block内存的影响 69 | 为了验证我们定义了三中关键字的block,分别有storng、weak、copy修饰: 70 | 71 | ``` 72 | @property (nonatomic, strong) TestBlock strongBlock; 73 | @property (nonatomic, weak) TestBlock weakBlock; 74 | @property (nonatomic, copy) TestBlock copyBlock; 75 | ``` 76 | 77 | 验证方法如下: 78 | 79 | ``` 80 | int globalInt = 1000;//全局变量 81 | static staticInt = 10000;//全局静态变量 82 | 83 | - (void)blockInMemory { 84 | static tempStaticInt = 100000;//局部静态变量 85 | int normalInt = 20000; 86 | _strongBlock = ^(int tempInt) { 87 | NSLog(@"tempInt = %d", normalInt); 88 | }; 89 | _weakBlock = ^(int tempInt) { 90 | NSLog(@"tempInt = %d", normalInt); 91 | }; 92 | _copyBlock = ^(int tempInt) { 93 | NSLog(@"tempInt = %d", normalInt); 94 | }; 95 | NSLog(@"\nstrongBlock:%@\n_weakBlock:%@\n_copyBlock:%@",object_getClass(_strongBlock),object_getClass(_weakBlock),object_getClass(_copyBlock)); 96 | } 97 | ``` 98 | 分别打印不同变量类型(全局变量、全局静态变量、局部静态变量、局部变量)和属性关键字下block的类型,我们可以得出如下结论: 99 | 100 | 1. 没有外部变量时,三种Block都是 ```__NSGlobalBlock__``` 101 | 2. 有外部变量时, 102 | 2.1 外部变量时全局变量、全局静态变量、局部静态变量时:```__NSGlobalBlock__ ```(全局区) 103 | 2.2 外部变量时普通外部变量:copy和strong修饰的Block是``` __NSMallocBlock__```(堆区);weak修饰的block是```__NSStackBlock__```(栈区) 104 | 105 | >有普通外部变量的block是在栈区创建的,当有copy和strong修饰符修饰的时,会把block从栈移到堆区。 106 | 107 | >ARC下使用copy和strong关键字修饰block是一样的。 108 | 109 | ### 结语 110 | 本篇为Block系列的第一篇,由此,我们了解了三种不同类型Block,接下来会以源码的方式深入了解block的底层实现,我们下篇再见。 111 | [Demo工程](https://github.com/qingfengiOS/BlockInMemory) -------------------------------------------------------------------------------- /BugByNSUInteger.md: -------------------------------------------------------------------------------- 1 | #NSUInteger造成的Bug 2 | 前两天接到测试同学的反映一个一级bug:点击了某个按钮之后,整个APP卡死,点击任何按钮都没反应,第一反应就是主线程有耗时操作,导致主线程被阻塞,不响应事件,紧接着一步一步排查代码,结果发现完全不是耗时操作阻塞主线程的锅,万恶之源是这个: 3 | ``` 4 | NSInteger row = ((self.hotArray.count - 1) / 3) + 1; 5 | ``` 6 | ``` 7 | self.selectTriperView.slwy_height = (row * 30 + row * 5 + 5) + 25; 8 | ``` 9 | ``` 10 | [self.selectTriperView refreshView:(row * 30 + row * 5 + 5) selectArray:self.hotArray fromType:2]; 11 | ``` 12 | 这里是通过接口获取一个数组,把数组值展示在collectionView之上,由于数组数量是动态的,所以需要根据具体具体个数计算出整个collectionView的高度。好了,问题就在这个self.hotArray.count - 1,当这个数组为空的时候,取出来的count和预期中的0大相径庭,结果是:18446744073709551615 ,(它等于2的64次方减一,也就是64位系统的能表示的最大数减一), .count是调用了方法: 13 | ```- (NSUInteger)count;``` 14 | 它获取数组元素数量,但是它的返回值是NSUInteger,当一个无符号数0减一时,结果就是无法预期的,这里由于计算出的数值大于了NSInteger能表示的最大值导致卡死。 15 | 参见:[BugByNSUInteger](https://github.com/qingfengiOS/BugByNSUInteger). 16 | 最后,在此提醒广大同仁,使用.count时注意对空数组的处理!!!!!! -------------------------------------------------------------------------------- /CLLocationManager导致内存泄漏.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingfengiOS/Summary/1c7aae3decee0da1e6f3ea62e40fde29ddfca55e/CLLocationManager导致内存泄漏.png -------------------------------------------------------------------------------- /CoreAnimation.md: -------------------------------------------------------------------------------- 1 | #CoreAnimation 2 | 3 | CoreAnimation直译为核心动画,它包含了非常强大的动画处理API,使用它能做出非常炫丽的动画效果,而且往往是事半功倍。但它不仅仅只能做动画的,它是一个图形渲染和动画基础设施,可用于iOS和OS X,它的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的图层,存储在一个叫做图层树的体系之中。于是这个树形成了UIKit以及在iOS应用程序当中你所能在屏幕上看见的一切的基础。书中章节如下: 4 | 5 | 1、图层树 6 | 7 | 2、寄宿图 8 | 9 | 3、图层几何学 10 | 11 | 4、视觉效果 12 | 13 | 5、变换 14 | 15 | 6、专用图层 16 | 17 | 7.、隐式动画 18 | 19 | 8、显式动画 20 | 21 | 9、图层时间 22 | 23 | 10、缓冲 24 | 25 | 11、基于定时器的动画 26 | 27 | 12、性能调优 28 | 29 | 13、高效绘图 30 | 31 | 14、图像IO 32 | 33 | 15、图层性能 34 | 35 | 最后声明,笔者不是大神,最近在学习CoreAnimation系列,仅仅只是记录和分享,资料来源于gitbook,学习过程中自己写了一个小Demo,代码已上传gitHub。有兴趣的可以看看。[CoreAnimation系列](https://github.com/qingfengiOS/CoreAnimation)。 36 | -------------------------------------------------------------------------------- /Fastlane命令.md: -------------------------------------------------------------------------------- 1 | #FastLane命令 2 | s1: 3 | fastlane init 4 | 5 | s2: 6 | fastlane add_plugin pgyer 7 | 8 | s3: 9 | vim ./fastlane/Fastfile 10 | 11 | s4: 12 | build_app(export_method: "ad-hoc") 13 | pgyer(api_key: "0cc5b8f7162750b13f2a4982c18cc7d5", user_key: "7e78a93092d38fa3d43227a4bed897b2") 14 | 15 | s5: 16 | fastlane beta 17 | 18 | 19 | 20 | 21 | xcode-select – 更改默认Xcode 22 | xcode-select -switch xcode_folder_path(xcode_folder_path表示应用程序路劲,需要使用最新的xcode9.2) 23 | xcode-select --print-path 完成后可以打印路劲 24 | 25 | 解决安装gem没有权限问题sudo gem install fastlane -NV -n /usr/local/bin -------------------------------------------------------------------------------- /OC消息转发机制.md: -------------------------------------------------------------------------------- 1 | #OC消息转发机制 2 | ## 前言 3 | 在上一篇[Runtime源码 方法调用的过程](https://www.jianshu.com/p/13d475db9c99)中我们了解了消息的响应过程,即 4 | 5 | 1. 先缓存查找,若未找到 6 | 2. 接下来查找本类的方法列表查找,若未找到 7 | 3. 则递归继承体系查找父类的方法列表直到NSObject 8 | 9 | 在第二第三步过程中,如果找到了则响应消息,并填充缓存,缓存是保存在元类上的,如果还找不到,则进入接下来的消息转发流程。 10 | 11 | 消息转发流程也分为三个步骤,动态解析 -> 前端转发 -> 方法签名转发,流程如下: 12 | 13 | ![消息转发](https://upload-images.jianshu.io/upload_images/2598795-16d6f15b5b06b0f4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 14 | 我们结合一个实例来具体看看[传送门:QFMessageForwardDemo](https://github.com/qingfengiOS/QFMessageForwardDemo.git) 15 | ## 动态解析 16 | 第一步,当没找到方法时,你可以通过```+ (BOOL)resolveInstanceMethod:(SEL)sel ```和```+ (BOOL)resolveClassMethod:(SEL)sel```来添加实例方法和类方法 17 | 18 | 这里我们在```ViewController```中直接调用了```QFPerson```的```run```方法,但是```QFPerson```并没有实现这个方法,所以动态添加了这个方法。 19 | 20 | ``` 21 | + (BOOL)resolveInstanceMethod:(SEL)sel { 22 | 23 | if (sel == @selector(run:age:)) {//第一步自己添加方法 24 | class_addMethod(self, sel, (IMP)newRun, "v@:@:"); 25 | return YES; 26 | } 27 | return [super resolveInstanceMethod:sel]; 28 | } 29 | ``` 30 | 31 | 这个添加一个自定义的方法 32 | 33 | ``` 34 | void newRun(id self,SEL sel, NSString *str, NSInteger age) {//自定义方法实现 35 | NSLog(@"---run ok---%@---%ld",str,(long)age); 36 | } 37 | ``` 38 | 这里用到了runtime的动态添加方法,不熟悉的可以看看这个系列文章的前几篇 39 | ,执行结果: 40 | 41 | ``` 42 | QFMessageForwardDemo[18572:704661] ---run ok---hello---18 43 | ``` 44 | ## 前端转发 45 | 46 | 如果第一步没有动态添加方法,则会进入转发的第二步,前端转发,所谓前端转发即是,本类没有实现这个方法,但是另外的一个类实现了这个方法,那么我们可以直接转发这条消息到另外的类实现调用。这里我们新建```QFOtherPerson```` 并且实现```- (void)run:(NSString *)name age:(NSInteger)age```方法 47 | 48 | ``` 49 | - (void)run:(NSString *)name age:(NSInteger)age{ 50 | NSLog(@"%@ 执行了 run name = %@,age = %ld",NSStringFromClass([self class]), name,age); 51 | } 52 | ``` 53 | 在```QFPerson```的实现文件中,实现下面的方法完成转发 54 | 55 | ``` 56 | - (id)forwardingTargetForSelector:(SEL)aSelector { 57 | return [[QFOtherPerson alloc]init];//第二步,前端转发 58 | } 59 | ``` 60 | >ps:此时要先注释注释掉第一步的动态添加方法 61 | 62 | 执行结果: 63 | 64 | ``` 65 | QFMessageForwardDemo[19629:739692] QFOtherPerson 执行了 run name = hello,age = 18 66 | ``` 67 | ## 签名转发 68 | 第三步,也是最后的机会处理这条消息了,首先生成方法签名: 69 | 70 | ``` 71 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {//第三步,签名转发 72 | NSString *methodName = NSStringFromSelector(aSelector); 73 | if ([methodName isEqualToString:@"run:age:"]) {//是我们需要转发的run方法 74 | return [NSMethodSignature signatureWithObjCTypes:"v@:"]; 75 | } else { 76 | return [super methodSignatureForSelector:aSelector]; 77 | } 78 | } 79 | ``` 80 | 关于invocation你熟悉的可以看[ResponderChain+Strategy+MVVM实现一个优雅的TableView](https://juejin.im/post/5bd6c734e51d45410c10eb54)这里面有完整的invocation调用方法的例子,这里就不做粘贴了。 81 | 82 | 最后调用```-forwardInvocation:```指定target,完成转发: 83 | 84 | ``` 85 | - (void)forwardInvocation:(NSInvocation *)anInvocation {//签名转发 86 | SEL selector = [anInvocation selector];//目标方法 87 | 88 | QFOtherPerson *other = [[QFOtherPerson alloc]init];//转发对象 89 | 90 | if ([other respondsToSelector:selector]) {//目标对象能相应此方法 91 | [anInvocation invokeWithTarget:other]; 92 | } else { 93 | return [super forwardInvocation:anInvocation]; 94 | } 95 | } 96 | ``` 97 | 运行结果: 98 | 99 | ``` 100 | QFMessageForwardDemo[31317:930989] QFOtherPerson 执行了 run name = hello,age = 18 101 | ``` 102 | 以上就是方法转发的流程,如果以上三步都没有实现则会崩溃,显示一个很常见的异常**- unrecognized selector sent to instance 0x60000001b440'** 103 | 104 | 关于之前的流程请参考:[Runtime源码 方法调用的过程](https://juejin.im/post/5bda5a18e51d45688561c2f6) 105 | -------------------------------------------------------------------------------- /Other Linker Flags.md: -------------------------------------------------------------------------------- 1 | #Other Linker Flags 2 | ##前言 3 | 一个程序从代码到执行文件需要经历如下几个阶段: 4 | __源代码->预处理->编译->汇编->机器码->链接->可执行文件(镜像文件)__ 5 | 6 | Other Linker Flags到底是用来干嘛的呢?就是ld命令除了默认参数外的其他参数。ld命令实现的是链接的工作,详细说明可以在终端man ld查看。 7 | 8 | 源文件经过一系列处理以后,会生成对应的.obj文件,然后一个项目必然会有许多.obj文件,并且这些文件之间会有各种各样的联系,例如函数调用。链接器做的事就是把这些目标文件和所用的一些库链接在一起形成一个完整的可执行文件。 9 | 10 | ##selector not recognized 11 | 苹果官方Q&A上有这么一段话: 12 | 13 | The "selector not recognized" runtime exception occurs due to an issue between the implementation of standard UNIX static libraries, the linker and the dynamic nature of Objective-C. Objective-C does not define linker symbols for each function (or method, in Objective-C) - instead, linker symbols are only generated for each class. If you extend a pre-existing class with categories, the linker does not know to associate the object code of the core class implementation and the category implementation. This prevents objects created in the resulting application from responding to a selector that is defined in the category. 14 | 15 | 16 | 翻译过来,大概意思就是Objective-C的链接器并不会为每个方法建立符号表,而是仅仅为类建立了符号表。这样的话,如果静态库中定义了已存在的一个类的分类,链接器就会以为这个类已经存在,不会把分类和核心类的代码合起来。这样的话,在最后的可执行文件中,就会缺少分类里的代码,这样函数调用就失败了。 17 | 18 | ##解决方法 19 | 解决方法在背景那块我就提到了,就是在Other Linker Flags里加上所需的参数,用到的参数一般有以下3个: 20 | 21 | 1. -ObjC 22 | 2. -all_load 23 | 3. -force_load 24 | 25 | -ObjC, 一般这个参数足够解决前面提到的问题,苹果官方说明如下: 26 | This flag causes the linker to load every object file in the library that defines an Objective-C class or category. While this option will typically result in a larger executable (due to additional object code loaded into the application), it will allow the successful creation of effective Objective-C static libraries that contain categories on existing classes. 27 | 28 | 简单说来,加了这个参数后,链接器就会把静态库中所有的Objective-C类和分类都加载到最后的可执行文件中,虽然这样可能会因为加载了很多不必要的文件而导致可执行文件变大,但是这个参数很好地解决了我们所遇到的问题。但是事实真的是这样的吗? 29 | 30 | 如果-ObjC参数真的这么有效,那么事情就会简单多了。 31 | 32 | Important: For 64-bit and iPhone OS applications, there is a linker bug that prevents -ObjC from loading objects files from static libraries that contain only categories and no classes. The workaround is to use the -allload or -forceload flags. 33 | 34 | 当静态库中只有分类而没有类的时候,-ObjC参数就会失效了。这时候,就需要使用-all_load或者-force_load了。 35 | 36 | -allload会让链接器把所有找到的目标文件都加载到可执行文件中,但是__千万不要随便使用这个参数!__假如你使用了不止一个静态库文件,然后又使用了这个参数,那么你很有可能会遇到ld: duplicate symbol错误,因为不同的库文件里面可能会有相同的目标文件,所以建议在遇到-ObjC失效的情况下使用-force_load参数。 37 | 38 | -forceload所做的事情跟-all_load其实是一样的,但是-force_load需要指定要进行全部加载的库文件的路径,这样的话,你就只是完全加载了一个库文件,不影响其余库文件的按需加载。 39 | [原文](https://my.oschina.net/u/728866/blog/194741) 40 | -------------------------------------------------------------------------------- /PickerView.md: -------------------------------------------------------------------------------- 1 | #带年月和至今以及设置分钟间隔的时间选择器 2 | 在平时的开发过程中,肯定遇到过一些需求,比如一些场景中,只需要选择年、月不需要天,并且包含“至今”,系统默认的pickerView呢,并不能满足,只能自定义数据源,当然这并不困难,但如果有一个现成的时间选择器,何乐不为呢。 3 | 4 | 使用起来也比较方便,导入头文件头文件,在需要的地方调用```initDatePackerWithResponse```方法和```show```方法,在block里面处理得到的时间字符串,时间字符串在block里面,注意循环引用的问题。 5 | 效果如下: 6 | ![datePicker](https://upload-images.jianshu.io/upload_images/2598795-ca8facdd816d008b.png) 7 | 上面我们已经完成了对日期的选择,接下来就是对时间的选择,苹果原生的DatePicker支持以每一分钟为单位的时间选择,但是在实际项目中常常需求并非如此,并不需要选到每一分钟,可能需要以5分钟、10分钟等为单位选择,好了,话不多说,看效果: 8 | ![timepicker](https://upload-images.jianshu.io/upload_images/2598795-4ffe057b67124b5c.png) 9 | 调用方法和日期选择一样,直接调用```initDatePackerWithStartHour:endHour: period: response:(void (^)(NSString *))block```方法,传入开始时间,结束时间,时间间隔三个参数,返回结果在block里处理。 10 | [Github地址:](github.com/qingfengiOS/QFDatePickerView)如果有用呢,希望能顺手给个star,有什么意见和建议希望各路大神不吝赐教! 11 | -------------------------------------------------------------------------------- /RAC.md: -------------------------------------------------------------------------------- 1 | #RAC中文资源列表 2 | 3 | QQ讨论群:430033580 4 | 欢迎进群一起讨论。 5 | 6 | ##入门 7 | 8 | [ReactiveCocoa 和 MVVM 入门](http://yulingtianxia.com/blog/2015/05/21/ReactiveCocoa-and-MVVM-an-Introduction/) 9 | 10 | [ReactiveCocoa入门教程:第一部分](http://southpeak.github.io/blog/2014/08/02/reactivecocoazhi-nan-%5B%3F%5D-:xin-hao/) 11 | 12 | [ReactiveCocoa入门教程:第二部分](http://southpeak.github.io/blog/2014/08/02/reactivecocoazhi-nan-er-:twittersou-suo-shi-li/) 13 | 14 | [说说ReactiveCocoa 2](http://www.cocoachina.com/ios/20140115/7702.html) 15 | 16 | [iOS 7最佳实践:一个天气App案例](http://www.cocoachina.com/ios/20140224/7868.html) 17 | 18 | [Reactive Cocoa Tutorial [0] = Overview](http://blog.sunnyxx.com/2014/03/06/rac_0_overview/) 19 | 20 | [Reactive Cocoa Tutorial [1] = 神奇的Macros](http://blog.sunnyxx.com/2014/03/06/rac_1_macros/) 21 | 22 | [Reactive Cocoa Tutorial [2] = 百变RACStream](http://blog.sunnyxx.com/2014/03/06/rac_2_racstream/) 23 | 24 | [Reactive Cocoa Tutorial [3] = RACSignal的巧克力工厂](http://blog.sunnyxx.com/2014/03/06/rac_3_racsignal/) 25 | 26 | [Reactive Cocoa Tutorial [4] = 只取所需的Filters](http://blog.sunnyxx.com/2014/04/19/rac_4_filters/) 27 | 28 | [ReactiveCocoa2 源码浅析](http://nathanli.cn/2015/08/27/reactivecocoa2-源码浅析/) 29 | 30 | [最快让你上手ReactiveCocoa之基础篇](http://www.jianshu.com/p/87ef6720a096) 31 | 32 | [最快让你上手ReactiveCocoa之进阶篇](http://www.jianshu.com/p/e10e5ca413b7) 33 | 34 | [ReactiveCocoa框架菜鸟入门(五)——信号的FlattenMap与Map](http://demo.netfoucs.com/abc649395594/article/details/46552865) 35 | 36 | [iOS开发之ReactiveCocoa下的MVVM](http://www.tuicool.com/articles/J7j6bmR) 37 | 38 | [ReactiveCocoa与Functional Reactive Programming](http://www.cnblogs.com/linyawen/p/3522023.html) 39 | 40 | ##进阶 41 | 42 | [ReactiveCocoa 用 RACSignal 替代 Delegate](http://www.cocoachina.com/ios/20141229/10789.html) 43 | 44 | [ReactiveCocoa2实战](http://www.cocoachina.com/ios/20140609/8737.html) 45 | 46 | [基于AFNetworking2.0和ReactiveCocoa2.1的iOS REST Client](http://www.cocoachina.com/ios/20140126/7759.html) 47 | 48 | [ReactiveCocoa基本组件:理解和使用RACCommand](http://blog.csdn.net/womendeaiwoming/article/details/37597779) 49 | 50 | [ReactiveCocoa2 源码浅析](http://blog.csdn.net/womendeaiwoming/article/details/48036725) 51 | 52 | ##资料 53 | | 资料名称 | 资料描述 | 54 | |:-------:|:-------:| 55 | |[Learn ReactiveCocoa Source](https://github.com/mailworks/LearnReactivecocoaSource)|学习ReactiveCocoa(主要针对2.x Objective-C 版本)过程中整理的一些资料。| 56 | 57 | 58 | ##开源项目 59 | 60 | | 项目名称 | 项目描述 | 61 | |:----------------------------------------------------------------------------:|:------------------------------------------------------------------:| 62 | |[ddaajing/ReactiveCocoaDemo](https://github.com/ddaajing/ReactiveCocoaDemo) | 该Demo主要用来测验MVVM模式的分层,使APP更方便维护,测试 以及练习ReactiveCocoa相关API | 63 | | [ashfurrow/C-41](https://github.com/ashfurrow/C-41) |[介绍博客](http://blog.csdn.net/zzdjk6/article/details/46996571) | 64 | | [arbullzhang/KoalaFRP](https://github.com/arbullzhang/KoalaFRP) | 说明:FunctionalReactivePixels 做了修改,因为编译不过。 | 65 | | [leichunfeng/MVVMReactiveCocoa](https://github.com/leichunfeng/MVVMReactiveCocoa)| **推荐!一个完整的使用MVVM和RAC的Github客户端,leichunfeng大师作品。**| 66 | |[ashfurrow / FunctionalReactivePixels](https://github.com/ashfurrow/FunctionalReactivePixels)|演示使用500px的API来使用FRP与ReactiveCocoa在iOS的环境。| 67 | 68 | ##翻译计划 69 | 如果有同学想翻译什么文章,或者官方文档,请到issue里提出来,可以是自己搜索到的。 70 | 71 | ###官方文档翻译 72 | 73 | RAC文档翻译文件夹下 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | -------------------------------------------------------------------------------- /ResponderChain+Strategy+MVVM实现一个优雅的TableView.md: -------------------------------------------------------------------------------- 1 | #ResponderChain+Strategy+MVVM实现一个优雅的TableView 2 | 3 | ## 前言 4 | 在iOS开发中,常见的MVC中,复杂界面的Controller中的代码极其臃肿,动则上千行的代码量对后期维护简直是一种灾难,因此MVC也被调侃为Messive ViewController,特别是有多种类型Cell的TableView存在时,在```-tableView:cellForRowAtIndexPath:```代理方法中充斥着大量的if-else分支,这次我们尝试用一种新的方式来“优雅”地实现这个方法。 5 | 6 | 传统iOS的对象间交互模式就那么几种:直接property传值、delegate、KVO、block、protocol、多态、Target-Action。这次来说说基于ResponderChain来实现对象间交互。 7 | 8 | 这种方式通过在UIResponder上挂一个category,使得事件和参数可以沿着responder chain逐步传递。 9 | 10 | 这相当于借用responder chain实现了一个自己的事件传递链。这在事件需要层层传递的时候特别好用,然而这种对象交互方式的有效场景仅限于在responder chain上的UIResponder对象上。 11 | 12 | ## 二、MVVM分离逻辑,解耦 13 | 网上关于MVVM的文章很多而且每个人的理解可能都有小小的差别,这里不做赘述,这里说说我在项目中所用到的MVVM,如果错误,请看官多多指教。我的tableView定义在ViewModel中,其代理方法也在ViewModel实现: 14 | 15 | 头文件中: 16 | 17 | ``` 18 | #import 19 | 20 | @interface QFViewModel : NSObject 21 | 22 | /// 暴露一个tableView的属性 提供Controller使用 23 | @property (nonatomic, strong) UITableView *tableView; 24 | 25 | @end 26 | ``` 27 | 28 | 实现文件中两个关键方法: 29 | 30 | ``` 31 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 32 | id model = self.dataArray[indexPath.row]; 33 | id cell = [tableView dequeueReusableCellWithIdentifier:model.identifier]; 34 | [cell configCellDateByModel:model]; 35 | return (UITableViewCell *)cell; 36 | } 37 | 38 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { 39 | id model = self.dataArray[indexPath.row]; 40 | return model.height; 41 | } 42 | ``` 43 | 这里用到了协议Protocol做解耦,两个协议,一个视图层的协议```QFViewProtocol```,一个模型的协议```QFModelProtocol``` 44 | 45 | - QFViewProtocol 这里提供一个方法,通过模型数据设置界面的展示 46 | 47 | ``` 48 | /** 49 | 协议用于保存每个cell的数据源设置方法,也可以不用,直接在每个类型的cell头文件中定义,考虑到开放封闭原则,建议使用 50 | */ 51 | @protocol QFViewProtocol 52 | 53 | /** 54 | 通过model 配置cell展示 55 | 56 | @param model model 57 | */ 58 | - (void)configCellDateByModel:(id)model; 59 | ``` 60 | - QFModelProtocol 这里提供两个属性,一个重用标志符,一个行高 61 | 62 | ``` 63 | #import 64 | 65 | /** 66 | 协议用于保存每个model对应cell的重用标志符和行高,也可以不使用这个协议 直接在对一个的model里指明 67 | */ 68 | @protocol QFModelProtocol 69 | 70 | - (NSString *)identifier; 71 | 72 | - (CGFloat)height; 73 | 74 | @end 75 | ``` 76 | 在控制器层中直接addSubView: 77 | 78 | ``` 79 | - (void)initAppreaence { 80 | [self.view addSubview:self.viewModel.tableView]; 81 | } 82 | ``` 83 | 84 | 85 | ## 三、基于ResponderChain传递点击事件 86 | 在iOS的事件传递响应中有一棵响应树,使用此可以消除掉各个类中的头文件引用。使用这个特性只需要一个方法即可,为UIResponder添加分类,实现一个方法: 87 | 88 | ``` 89 | /** 90 | 通过事件响应链条传递事件 91 | 92 | @param eventName 事件名 93 | @param userInfo 附加参数 94 | */ 95 | - (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo { 96 | [[self nextResponder] routerEventWithName:eventName userInfo:userInfo]; 97 | } 98 | ``` 99 | 100 | 发送事件的时候使用: 101 | 102 | ``` 103 | [self routerEventWithName:kEventOneName userInfo:@{@"keyOne": @"valueOne"}]; 104 | ``` 105 | >这里使用了一个字典来做参数的传递,这里可以使用装饰者模式,在事件层层向上传递的时候,每一层都可以往UserInfo这个字典中添加数据。那么到了最终事件处理的时候,就能收集到各层综合得到的数据,从而完成最终的事件处理。 106 | 107 | 如果要把这个事件继续传递下去直到AppDelegate中的话,调用: 108 | 109 | ``` 110 | // 把响应链继续传递下去 111 | [super routerEventWithName:eventName userInfo:userInfo]; 112 | ``` 113 | ## 四、策略模式避免if-else 114 | 在《大话设计模式》一书中,使用了商场打折的案例分析了策略模式对于不同算法的封装,有兴趣可以去看看,这里我们使用策略模式封装的是NSInvocation,他用于做方法调用,在消息转发的最后阶段会通过NSInvocation来转发。我们以一个方法调用的实例来看NSInvocation 115 | 116 | ``` 117 | #pragma mark - invocation调用方法 118 | - (void)invocation { 119 | SEL myMethod = @selector(testInvocationWithString:number:); 120 | NSMethodSignature *sig = [[self class] instanceMethodSignatureForSelector:myMethod]; 121 | NSInvocation * invocatin = [NSInvocation invocationWithMethodSignature:sig]; 122 | 123 | [invocatin setTarget:self]; 124 | [invocatin setSelector:myMethod]; 125 | 126 | NSString *a = @"string"; 127 | NSInteger b = 10; 128 | [invocatin setArgument:&a atIndex:2]; 129 | [invocatin setArgument:&b atIndex:3]; 130 | 131 | NSInteger res = 0; 132 | [invocatin invoke]; 133 | [invocatin getReturnValue:&res]; 134 | 135 | NSLog(@"%ld",(long)res); 136 | } 137 | 138 | - (NSInteger)testInvocationWithString:(NSString *)str number:(NSInteger)number { 139 | 140 | return str.length + number; 141 | } 142 | ``` 143 | 144 | - 第一步通过方法,生成方法签名; 145 | - 第二步设置参数,注意这里```[invocatin setArgument:&a atIndex:2];```的index从2开始而不是0,因为还有两个隐藏参数self和_cmd占用了两个 146 | - 第三步调用```[invocatin invoke];``` 147 | 148 | 好了我们回归主题,这里用一个dictionary,保存方法调用的必要参数,字典的key是事件名,value是对应的invocation对象,当事件发生时,直接调用 149 | 150 | ``` 151 | - (NSDictionary *)strategyDictionary { 152 | if (!_eventStrategy) { 153 | _eventStrategy = @{ 154 | kEventOneName:[self createInvocationWithSelector:@selector(cellOneEventWithParamter:)], 155 | kEventTwoName:[self createInvocationWithSelector:@selector(cellTwoEventWithParamter:)] 156 | }; 157 | } 158 | return _eventStrategy; 159 | } 160 | 161 | ``` 162 | 163 | 这里调用```UIResponder```中的的方法```- (NSInvocation *)createInvocationWithSelector:(SEL)selector```生成invocation: 164 | 165 | ``` 166 | /** 167 | 通过方法SEL生成NSInvocation 168 | 169 | @param selector 方法 170 | @return Invocation对象 171 | */ 172 | - (NSInvocation *)createInvocationWithSelector:(SEL)selector { 173 | //通过方法名创建方法签名 174 | NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:selector]; 175 | //创建invocation 176 | NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; 177 | [invocation setTarget:self]; 178 | [invocation setSelector:selector]; 179 | return invocation; 180 | } 181 | ``` 182 | ## 五、事件处理 183 | 经过上面的步骤,我们可以在Controller中通过```- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo ```拿到事件做响应的处理,如果有必要,把这个事件继续传递下去: 184 | 185 | ``` 186 | #pragma mark - Event Response 187 | - (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo { 188 | // 处理事件 189 | [self handleEventWithName:eventName parameter:userInfo]; 190 | 191 | // 把响应链继续传递下去 192 | [super routerEventWithName:eventName userInfo:userInfo]; 193 | } 194 | 195 | - (void)handleEventWithName:(NSString *)eventName parameter:(NSDictionary *)parameter { 196 | // 获取invocation对象 197 | NSInvocation *invocation = self.strategyDictionary[eventName]; 198 | // 设置invocation参数 199 | [invocation setArgument:¶meter atIndex:2]; 200 | // 调用方法 201 | [invocation invoke]; 202 | } 203 | 204 | - (void)cellOneEventWithParamter:(NSDictionary *)paramter { 205 | NSLog(@"第一种cell事件---------参数:%@",paramter); 206 | QFDetailViewController *viewController = [QFDetailViewController new]; 207 | viewController.typeName = @"Cell类型一"; 208 | viewController.paramterDic = paramter; 209 | [self presentViewController:viewController animated:YES completion:nil]; 210 | } 211 | 212 | - (void)cellTwoEventWithParamter:(NSDictionary *)paramter { 213 | NSLog(@"第二种cell事件---------参数:%@",paramter); 214 | QFDetailViewController *viewController = [QFDetailViewController new]; 215 | viewController.typeName = @"Cell类型二"; 216 | viewController.paramterDic = paramter; 217 | [self presentViewController:viewController animated:YES completion:nil]; 218 | } 219 | ``` 220 | ## 六、后记 221 | 本篇到此结束了,总结起来,用到的东西还是不少,很多东西都值得深入: 222 | 223 | - Protocol的使用 224 | - 事件处理:事件产生、传递及响应的机制 225 | - 设计模式:策略模式、装饰者模式以及MVVM的使用 226 | - NSInvocation的使用及消息转发机制 227 | 228 | [Demo演示](https://github.com/qingfengiOS/MutableCellTableView) 229 | 有任何意见和建议欢迎交流指导,如果可以,请顺手给个star。 230 | 231 | 最后,万分感谢[Casa大佬](https://casatwy.com)的分享! 232 | [一种基于ResponderChain的对象交互方式](https://casatwy.com/responder_chain_communication.html#seg3) 233 | -------------------------------------------------------------------------------- /Runtime源码 +load 和 +initialize.md: -------------------------------------------------------------------------------- 1 | #Runtime源码 +load 和 +initialize 2 | ## 一、前言 3 | 在iOS的开发中,Runtime的方法交换都是写在```+load```之中,为什么不是```+initialize```中呢?可能不少朋友对此或多或少有一点点的疑问。 我们知道:OC中几乎所有的类都继承自NSObject,而```+load```和```+initialize```用于类的初始化,这两者的区别和联系到底何在呢?接下来我们一起来看看这二者的区别。 4 | 5 | ## 二、+load 6 | 根据[官方文档](https://developer.apple.com/documentation/objectivec/nsobject/1418815-load?language=objc)的描述: 7 | 8 | 1. ```+load```是当一个类或者分类被添加到Objective-C运行时调用的。 9 | 2. 本类```+load```的调用在其所有的父类```+load```调用之后。 10 | 3. 分类的```+load```在类的调用之后。 11 | 12 | > 也就是说调用顺序:父类 > 类 > 分类 13 | > 但是不同类之间的+load方法的调用顺序是不确定的。 14 | 15 | 由于```+load```是在类第一次加载进Runtime运行时才调用,由此我们可以知道: 16 | 17 | - 它只会调用一次,这也是为什么么方法交换写在```+load```的原因。 18 | - 它的调用时机在```main```函数之前。 19 | 20 | ## 三、+load的实现 21 | 在Runtime源码的```objc-runtime-new.mm```和```objc-runtime-old.mm```中的```load_images```方法中都存在这关键代码: 22 | 23 | ``` 24 | prepare_load_methods((const headerType *)mh); 25 | ``` 26 | 和 27 | ``` 28 | call_load_methods(); 29 | ``` 30 | ### 3.1 prepare_ load_methods 31 | 32 | ``` 33 | void prepare_load_methods(const headerType *mhdr) 34 | { 35 | size_t count, i; 36 | 37 | runtimeLock.assertWriting(); 38 | 39 | classref_t *classlist = 40 | _getObjc2NonlazyClassList(mhdr, &count); 41 | for (i = 0; i < count; i++) { 42 | schedule_class_load(remapClass(classlist[i])); 43 | } 44 | 45 | category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count); 46 | for (i = 0; i < count; i++) { 47 | category_t *cat = categorylist[i]; 48 | Class cls = remapClass(cat->cls); 49 | if (!cls) continue; // category for ignored weak-linked class 50 | realizeClass(cls); 51 | assert(cls->ISA()->isRealized()); 52 | add_category_to_loadable_list(cat); 53 | } 54 | } 55 | ``` 56 | 这里就是准备好满足 +load 方法调用条件的类和分类,而对**class**和**category**分开做了处理。 57 | 58 | - 在处理**class**的时候,调用了```schedule_class_load```: 59 | 60 | ``` 61 | /*********************************************************************** 62 | * prepare_load_methods 63 | * Schedule +load for classes in this image, any un-+load-ed 64 | * superclasses in other images, and any categories in this image. 65 | **********************************************************************/ 66 | // Recursively schedule +load for cls and any un-+load-ed superclasses. 67 | // cls must already be connected. 68 | static void schedule_class_load(Class cls) 69 | { 70 | if (!cls) return; 71 | assert(cls->isRealized()); // _read_images should realize 72 | 73 | if (cls->data()->flags & RW_LOADED) return; 74 | 75 | // Ensure superclass-first ordering 76 | schedule_class_load(cls->superclass); 77 | 78 | add_class_to_loadable_list(cls); 79 | cls->setInfo(RW_LOADED); 80 | } 81 | ``` 82 | **从倒数第三句代码看出这里对参数class的父类进行了递归调用,以此确保父类的优先级** 83 | 84 | 然后调用了```add_class_to_loadable_list```,把class加到了**loadable_classes**中: 85 | 86 | ``` 87 | /*********************************************************************** 88 | * add_class_to_loadable_list 89 | * Class cls has just become connected. Schedule it for +load if 90 | * it implements a +load method. 91 | **********************************************************************/ 92 | void add_class_to_loadable_list(Class cls) 93 | { 94 | IMP method; 95 | 96 | loadMethodLock.assertLocked(); 97 | 98 | method = cls->getLoadMethod(); 99 | if (!method) return; // Don't bother if cls has no +load method 100 | 101 | if (PrintLoading) { 102 | _objc_inform("LOAD: class '%s' scheduled for +load", 103 | cls->nameForLogging()); 104 | } 105 | 106 | if (loadable_classes_used == loadable_classes_allocated) { 107 | loadable_classes_allocated = loadable_classes_allocated*2 + 16; 108 | loadable_classes = (struct loadable_class *) 109 | realloc(loadable_classes, 110 | loadable_classes_allocated * 111 | sizeof(struct loadable_class)); 112 | } 113 | 114 | loadable_classes[loadable_classes_used].cls = cls; 115 | loadable_classes[loadable_classes_used].method = method; 116 | loadable_classes_used++; 117 | } 118 | ``` 119 | 120 | - 对于分类则是调用```add_category_to_loadable_list```把category加入到**loadable_categories**之中: 121 | 122 | ``` 123 | /*********************************************************************** 124 | * add_category_to_loadable_list 125 | * Category cat's parent class exists and the category has been attached 126 | * to its class. Schedule this category for +load after its parent class 127 | * becomes connected and has its own +load method called. 128 | **********************************************************************/ 129 | void add_category_to_loadable_list(Category cat) 130 | { 131 | IMP method; 132 | 133 | loadMethodLock.assertLocked(); 134 | 135 | method = _category_getLoadMethod(cat); 136 | 137 | // Don't bother if cat has no +load method 138 | if (!method) return; 139 | 140 | if (PrintLoading) { 141 | _objc_inform("LOAD: category '%s(%s)' scheduled for +load", 142 | _category_getClassName(cat), _category_getName(cat)); 143 | } 144 | 145 | if (loadable_categories_used == loadable_categories_allocated) { 146 | loadable_categories_allocated = loadable_categories_allocated*2 + 16; 147 | loadable_categories = (struct loadable_category *) 148 | realloc(loadable_categories, 149 | loadable_categories_allocated * 150 | sizeof(struct loadable_category)); 151 | } 152 | 153 | loadable_categories[loadable_categories_used].cat = cat; 154 | loadable_categories[loadable_categories_used].method = method; 155 | loadable_categories_used++; 156 | } 157 | ``` 158 | ### 3.2 call_ load_methods 159 | 160 | ``` 161 | void call_load_methods(void) 162 | { 163 | static bool loading = NO; 164 | bool more_categories; 165 | 166 | loadMethodLock.assertLocked(); 167 | 168 | // Re-entrant calls do nothing; the outermost call will finish the job. 169 | if (loading) return; 170 | loading = YES; 171 | 172 | void *pool = objc_autoreleasePoolPush(); 173 | 174 | do { 175 | // 1. Repeatedly call class +loads until there aren't any more 176 | while (loadable_classes_used > 0) { 177 | call_class_loads(); 178 | } 179 | 180 | // 2. Call category +loads ONCE 181 | more_categories = call_category_loads(); 182 | 183 | // 3. Run more +loads if there are classes OR more untried categories 184 | } while (loadable_classes_used > 0 || more_categories); 185 | 186 | objc_autoreleasePoolPop(pool); 187 | 188 | loading = NO; 189 | } 190 | ``` 191 | 从注释我们可以明确地看出: 192 | 193 | 1. 重复地调用class里面的```+load ```方法 194 | 2. 一次调用category里面的```+load```方法 195 | 196 | 还是看一下调用具体实现,以```call_class_loads ```为例: 197 | 198 | ``` 199 | /*********************************************************************** 200 | * call_class_loads 201 | * Call all pending class +load methods. 202 | * If new classes become loadable, +load is NOT called for them. 203 | * 204 | * Called only by call_load_methods(). 205 | **********************************************************************/ 206 | static void call_class_loads(void) 207 | { 208 | int i; 209 | 210 | // Detach current loadable list. 211 | struct loadable_class *classes = loadable_classes; 212 | int used = loadable_classes_used; 213 | loadable_classes = nil; 214 | loadable_classes_allocated = 0; 215 | loadable_classes_used = 0; 216 | 217 | // Call all +loads for the detached list. 218 | for (i = 0; i < used; i++) { 219 | Class cls = classes[i].cls; 220 | load_method_t load_method = (load_method_t)classes[i].method; 221 | if (!cls) continue; 222 | 223 | if (PrintLoading) { 224 | _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging()); 225 | } 226 | (*load_method)(cls, SEL_load); 227 | } 228 | 229 | // Destroy the detached list. 230 | if (classes) free(classes); 231 | } 232 | ``` 233 | 这就是真正负责调用类的 +load 方法了。它从上一步获取到的全局变量 **loadable_classes** 中取出所有可供调用的类,并进行清零操作。 234 | 235 | - ```loadable_classes``` 指向用于保存类信息的内存的首地址, 236 | - ```loadable_classes_allocated``` 标识已分配的内存空间大小, 237 | - ```loadable_classes_used``` 则标识已使用的内存空间大小。 238 | 239 | 然后,循环调用所有类的 +load 方法。注意,这里是(调用分类的 +load 方法也是如此)直接使用函数内存地址的方式``` (*load_method)(cls, SEL_load)```; 对 +load 方法进行调用的,而不是使用发送消息 objc_msgSend 的方式。 240 | 241 | 这样的调用方式就使得 +load 方法拥有了一个非常有趣的特性,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。也就是说如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用。因此,我们常常可以利用这个特性做一些“有趣”的事情,比如说方法交换[method-swizzling](https://nshipster.com/method-swizzling/)。 242 | ## 四、+initialize 243 | 根据官方文档[initialize](https://developer.apple.com/documentation/objectivec/nsobject/1418639-initialize?language=objc)的描述: 244 | 245 | 1. Runtime发送```+initialize```消息是在类或者其子类第一次收到消息时,而且父类会在类之前接收到消息 246 | 2. ```+initialize```的实现是线程安全的,多线程下会有线程等待 247 | 3. 父类的```+initialize ```可能会被调用多次 248 | 249 | 也就是说 +initialize 方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的 +initialize 方法是永远不会被调用的。那这样设计有什么好处呢?好处是显而易见的,那就是节省系统资源,避免浪费。 250 | 251 | ## 五、+initialize的实现 252 | 在runtime源码```objc-runtime-new.mm```的方法```lookUpImpOrForward```中有如下代码片段: 253 | 254 | ``` 255 | IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 256 | bool initialize, bool cache, bool resolver) 257 | { 258 | ··· 259 | if (initialize && !cls->isInitialized()) { 260 | runtimeLock.unlockRead(); 261 | _class_initialize (_class_getNonMetaClass(cls, inst)); 262 | runtimeLock.read(); 263 | 264 | // If sel == initialize, _class_initialize will send +initialize and 265 | // then the messenger will send +initialize again after this 266 | // procedure finishes. Of course, if this is not being called 267 | // from the messenger then it won't happen. 2778172 268 | } 269 | } 270 | 271 | ``` 272 | 可以知道:在方法调用过程中,如果类没有被初始化的时候,会调用```_class_initialize```对类进行初始化,方法细节如下: 273 | 274 | ``` 275 | /*********************************************************************** 276 | * class_initialize. Send the '+initialize' message on demand to any 277 | * uninitialized class. Force initialization of superclasses first. 278 | **********************************************************************/ 279 | void _class_initialize(Class cls) 280 | { 281 | assert(!cls->isMetaClass()); 282 | 283 | Class supercls; 284 | bool reallyInitialize = NO; 285 | 286 | // Make sure super is done initializing BEFORE beginning to initialize cls. 287 | // See note about deadlock above. 288 | * supercls = cls->superclass; 289 | * if (supercls && !supercls->isInitialized()) { 290 | * _class_initialize(supercls); 291 | * } 292 | 293 | // Try to atomically set CLS_INITIALIZING. 294 | { 295 | monitor_locker_t lock(classInitLock); 296 | if (!cls->isInitialized() && !cls->isInitializing()) { 297 | cls->setInitializing(); 298 | reallyInitialize = YES; 299 | } 300 | } 301 | 302 | if (reallyInitialize) { 303 | // We successfully set the CLS_INITIALIZING bit. Initialize the class. 304 | 305 | // Record that we're initializing this class so we can message it. 306 | _setThisThreadIsInitializingClass(cls); 307 | 308 | if (MultithreadedForkChild) { 309 | // LOL JK we don't really call +initialize methods after fork(). 310 | performForkChildInitialize(cls, supercls); 311 | return; 312 | } 313 | 314 | // Send the +initialize message. 315 | // Note that +initialize is sent to the superclass (again) if 316 | // this class doesn't implement +initialize. 2157218 317 | if (PrintInitializing) { 318 | _objc_inform("INITIALIZE: thread %p: calling +[%s initialize]", 319 | pthread_self(), cls->nameForLogging()); 320 | } 321 | 322 | // Exceptions: A +initialize call that throws an exception 323 | // is deemed to be a complete and successful +initialize. 324 | // 325 | // Only __OBJC2__ adds these handlers. !__OBJC2__ has a 326 | // bootstrapping problem of this versus CF's call to 327 | // objc_exception_set_functions(). 328 | 329 | // Exceptions: A +initialize call that throws an exception 330 | // is deemed to be a complete and successful +initialize. 331 | // 332 | // Only __OBJC2__ adds these handlers. !__OBJC2__ has a 333 | // bootstrapping problem of this versus CF's call to 334 | // objc_exception_set_functions(). 335 | #if __OBJC2__ 336 | @try 337 | #endif 338 | { 339 | callInitialize(cls); 340 | 341 | if (PrintInitializing) { 342 | _objc_inform("INITIALIZE: thread %p: finished +[%s initialize]", 343 | pthread_self(), cls->nameForLogging()); 344 | } 345 | } 346 | ... 347 | } 348 | ``` 349 | 这源码我们可以可出结论: 350 | 351 | 1. 从前面*的行数知道,_class_initialize方法会对class的父类进行递归调用,由此可以确保父类优先于子类初始化。 352 | 2. 在截出的代码末尾有着如下方法:```callInitialize(cls);``` 353 | 354 | ``` 355 | void callInitialize(Class cls) 356 | { 357 | ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize); 358 | asm(""); 359 | } 360 | ``` 361 | 这里指明了```+initialize```的调用方式是objc_msgSend,它和普通方法一样是由Runtime通过发消息的形式,调用走的都是发送消息的流程。换言之,如果子类没有实现 +initialize 方法,那么继承自父类的实现会被调用;如果一个类的分类实现了 +initialize 方法,那么就会对这个类中的实现造成覆盖。 362 | 363 | 因此,如果一个子类没有实现 +initialize 方法,那么父类的实现是会被执行多次的。有时候,这可能是你想要的;但如果我们想确保自己的 +initialize 方法只执行一次,避免多次执行可能带来的副作用时,我们可以使用下面的代码来实现: 364 | 365 | ``` 366 | + (void)initialize { 367 | if (self == [ClassName self]) { 368 | // ... do the initialization ... 369 | } 370 | } 371 | ``` 372 | 373 | ## 总结 374 | | | +load | +initialize | 375 | | ------ | :------: | :------: | 376 | | 调用时机 | 加载到runtime时 | 收到第一条消息时,可能永远不调用 | 377 | | 调用方式(本质) | 函数调用 | runtime调度(和普通的方法一样,通过objc_msgSend) | 378 | | 调用顺序 | 父类 > 类 > 分类 | 父类 > 类 | 379 | | 调用次数 | 一次 | 不定,可能多次可能不调用 | 380 | | 是否沿用父类的实现 | 否 | 是 | 381 | | 分类的中实现 | 类和分类都执行 | "覆盖"类中的方法,只执行分类的实现 | 382 | ## 后记 383 | 应该尽可能减少initialize的调用,节省资源,截取官方原文的供大家参考: 384 | >Because initialize is called in a blocking manner, it’s important to limit method implementations to the minimum amount of work necessary possible. Specifically, any code that takes locks that might be required by other classes in their initialize methods is liable to lead to deadlocks. Therefore, you should not rely on initialize for complex initialization, and should instead limit it to straightforward, class local initialization. 385 | 386 | 更多资料: 387 | [load](https://developer.apple.com/documentation/objectivec/nsobject/1418815-load?language=objc) 388 | [initialize](https://developer.apple.com/documentation/objectivec/nsobject/1418639-initialize) 389 | [Objective-C +load vs +initialize](http://blog.leichunfeng.com/blog/2015/05/02/objective-c-plus-load-vs-plus-initialize/) 390 | -------------------------------------------------------------------------------- /Runtime源码 Category(分类).md: -------------------------------------------------------------------------------- 1 | #Runtime源码 Category(分类) 2 | 3 | ## 一、概述 4 | **Category**又叫分类,类别,类目,作为Objective-C 2.0之后添加的语言特性,Category在如今的OC工程中随处可见,它可以在即使不知道源码的情况下为类添加方法,根据官方文档[Category](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/Category.html#//apple_ref/doc/uid/TP40008195-CH5-SW2)其主要作用有: 5 | 6 | 1. 分离类的实现到独立的文件,这样做的好处有: 7 | - 减小单个文件的代码量(维护一个2000代码的类和维护四个500的代码的类差别还是比较明显的)。 8 | - 把不同功能组织到不同的Category。 9 | - 方便多人维护一个类 10 | - 按需加载想要的Category 11 | 2. 定义私有方法 12 | 3. 模拟多继承 13 | 4. 把framework的私有方法公开 14 | 15 | >注:Category 有一个非常容易误用的场景,那就是用 Category 来覆写父类或主类的方法。虽然目前 Objective-C 是允许这么做的,但是这种使用场景是非常不推荐的。[使用 Category 来覆写方法](https://stackoverflow.com/questions/5272451/overriding-methods-using-categories-in-objective-c)有很多缺点,比如不能覆写 Category 中的方法、无法调用主类中的原始实现等,且很容易造成无法预估的行为。 16 | 17 | ## 二、底层实现 18 | 在objc4-723中Category的定义如下: 19 | 20 | ``` 21 | struct category_t { 22 | const char *name; 23 | classref_t cls; 24 | struct method_list_t *instanceMethods; 25 | struct method_list_t *classMethods; 26 | struct protocol_list_t *protocols; 27 | struct property_list_t *instanceProperties; 28 | // Fields below this point are not always present on disk. 29 | struct property_list_t *_classProperties; 30 | 31 | method_list_t *methodsForMeta(bool isMeta) { 32 | if (isMeta) return classMethods; 33 | else return instanceMethods; 34 | } 35 | 36 | property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi); 37 | }; 38 | ``` 39 | - name 40 | 分类名 41 | - cls 42 | 类 43 | - instanceMethods 44 | 实例方法列表 45 | - classMethod 46 | 类方法列表 47 | - protocols 48 | 遵守的协议列表 49 | - instanceProperties 50 | 实例属性列表 51 | - _classProperties 52 | 类属性列表 53 | 54 | 从Category的结构可见: 55 | 1. 它可以添加实例方法,类方法,甚至可以实现协议,添加属性(但是这里的属性不会自动生成实例变量和对应的set、get方法需要通过[关联对象](https://www.jianshu.com/p/c741ffe47cf1)实现) 56 | 2. 不可以添加实例变量。 57 | 58 | ## 三、Category加载 59 | 对于OC运行时,在入口方法中: 60 | 61 | ``` 62 | void _objc_init(void) 63 | { 64 | static bool initialized = false; 65 | if (initialized) return; 66 | initialized = true; 67 | 68 | // fixme defer initialization until an objc-using image is found? 69 | environ_init(); 70 | tls_init(); 71 | static_init(); 72 | lock_init(); 73 | exception_init(); 74 | 75 | _dyld_objc_notify_register(&map_images, load_images, unmap_image); 76 | } 77 | ``` 78 | category被附加到类上面是在map_images的时候发生的,在new-ABI的标准下,_objc_init里面的调用的map_images最终会调用objc-runtime-new.mm里面的_read_images方法,而在_read_images方法的结尾,有以下的代码片段: 79 | 80 | ``` 81 | // Discover categories. 82 | for (EACH_HEADER) { 83 | category_t **catlist = 84 | _getObjc2CategoryList(hi, &count); 85 | bool hasClassProperties = hi->info()->hasCategoryClassProperties(); 86 | 87 | for (i = 0; i < count; i++) { 88 | category_t *cat = catlist[i]; 89 | Class cls = remapClass(cat->cls); 90 | 91 | if (!cls) { 92 | // Category's target class is missing (probably weak-linked). 93 | // Disavow any knowledge of this category. 94 | catlist[i] = nil; 95 | if (PrintConnecting) { 96 | _objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with " 97 | "missing weak-linked target class", 98 | cat->name, cat); 99 | } 100 | continue; 101 | } 102 | 103 | // Process this category. 104 | // First, register the category with its target class. 105 | // Then, rebuild the class's method lists (etc) if 106 | // the class is realized. 107 | bool classExists = NO; 108 | if (cat->instanceMethods || cat->protocols 109 | || cat->instanceProperties) 110 | { 111 | addUnattachedCategoryForClass(cat, cls, hi); 112 | if (cls->isRealized()) { 113 | remethodizeClass(cls); 114 | classExists = YES; 115 | } 116 | if (PrintConnecting) { 117 | _objc_inform("CLASS: found category -%s(%s) %s", 118 | cls->nameForLogging(), cat->name, 119 | classExists ? "on existing class" : ""); 120 | } 121 | } 122 | 123 | if (cat->classMethods || cat->protocols 124 | || (hasClassProperties && cat->_classProperties)) 125 | { 126 | addUnattachedCategoryForClass(cat, cls->ISA(), hi); 127 | if (cls->ISA()->isRealized()) { 128 | remethodizeClass(cls->ISA()); 129 | } 130 | if (PrintConnecting) { 131 | _objc_inform("CLASS: found category +%s(%s)", 132 | cls->nameForLogging(), cat->name); 133 | } 134 | } 135 | } 136 | } 137 | 138 | ``` 139 | 这里做的就是: 140 | 141 | 1. 把category的实例方法、协议以及属性添加到类上 142 | 2. 把category的类方法和协议添加到类的metaclass上 143 | 144 | 接着往里看,category的各种列表是怎么最终添加到类上的,就拿实例方法列表来说吧: 145 | 在上述的代码片段里,addUnattachedCategoryForClass只是把类和category做一个关联映射,而remethodizeClass才是真正去处理添加事宜的功臣。 146 | 147 | ``` 148 | /*********************************************************************** 149 | * remethodizeClass 150 | * Attach outstanding categories to an existing class. 151 | * Fixes up cls's method list, protocol list, and property list. 152 | * Updates method caches for cls and its subclasses. 153 | * Locking: runtimeLock must be held by the caller 154 | **********************************************************************/ 155 | static void remethodizeClass(Class cls) 156 | { 157 | category_list *cats; 158 | bool isMeta; 159 | 160 | runtimeLock.assertWriting(); 161 | 162 | isMeta = cls->isMetaClass(); 163 | 164 | // Re-methodizing: check for more categories 165 | if ((cats = unattachedCategoriesForClass(cls, false/*not realizing*/))) { 166 | if (PrintConnecting) { 167 | _objc_inform("CLASS: attaching categories to class '%s' %s", 168 | cls->nameForLogging(), isMeta ? "(meta)" : ""); 169 | } 170 | 171 | attachCategories(cls, cats, true /*flush caches*/); 172 | free(cats); 173 | } 174 | } 175 | 176 | ``` 177 | 可以看到:在remethodizeClass内部核心方法是:**attachCategories**,它才是真正地添加方法 178 | 179 | ``` 180 | // Attach method lists and properties and protocols from categories to a class. 181 | // Assumes the categories in cats are all loaded and sorted by load order, 182 | // oldest categories first. 183 | static void 184 | attachCategories(Class cls, category_list *cats, bool flush_caches) 185 | { 186 | if (!cats) return; 187 | if (PrintReplacedMethods) printReplacements(cls, cats); 188 | 189 | bool isMeta = cls->isMetaClass(); 190 | 191 | // fixme rearrange to remove these intermediate allocations 192 | method_list_t **mlists = (method_list_t **) 193 | malloc(cats->count * sizeof(*mlists)); 194 | property_list_t **proplists = (property_list_t **) 195 | malloc(cats->count * sizeof(*proplists)); 196 | protocol_list_t **protolists = (protocol_list_t **) 197 | malloc(cats->count * sizeof(*protolists)); 198 | 199 | // Count backwards through cats to get newest categories first 200 | int mcount = 0; 201 | int propcount = 0; 202 | int protocount = 0; 203 | int i = cats->count; 204 | bool fromBundle = NO; 205 | while (i--) { 206 | auto& entry = cats->list[i]; 207 | 208 | method_list_t *mlist = entry.cat->methodsForMeta(isMeta); 209 | if (mlist) { 210 | mlists[mcount++] = mlist; 211 | fromBundle |= entry.hi->isBundle(); 212 | } 213 | 214 | property_list_t *proplist = 215 | entry.cat->propertiesForMeta(isMeta, entry.hi); 216 | if (proplist) { 217 | proplists[propcount++] = proplist; 218 | } 219 | 220 | protocol_list_t *protolist = entry.cat->protocols; 221 | if (protolist) { 222 | protolists[protocount++] = protolist; 223 | } 224 | } 225 | 226 | auto rw = cls->data(); 227 | 228 | prepareMethodLists(cls, mlists, mcount, NO, fromBundle); 229 | rw->methods.attachLists(mlists, mcount); 230 | free(mlists); 231 | if (flush_caches && mcount > 0) flushCaches(cls); 232 | 233 | rw->properties.attachLists(proplists, propcount); 234 | free(proplists); 235 | 236 | rw->protocols.attachLists(protolists, protocount); 237 | free(protolists); 238 | } 239 | ``` 240 | 241 | 需要注意的有两点: 242 | 243 | 1. category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果category和原来类都有methodA,那么category附加完成之后,类的方法列表里会有两个methodA。 244 | 2. category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会罢休^_^,殊不知后面可能还有一样名字的方法。 245 | 246 | ## 四、Category和Extension 247 | - Extension 248 | - 在编译器决议,是类的一部分,在编译器和头文件的@interface和实现文件里的@implement一起形成了一个完整的类。 249 | - 伴随着类的产生而产生,也随着类的消失而消失。 250 | Extension一般用来隐藏类的私有消息,你必须有一个类的源码才能添加一个类的Extension,所以对于系统一些类,如NSString,就无法添加类扩展 251 | - Category 252 | - 是运行期决议的 253 | - 类扩展可以添加实例变量,分类不能添加实例变量 254 | 255 | >因为在运行期,对象的内存布局已经确定,如果添加实例变量会破坏类的内部布局,这对编译性语言是灾难性的。 256 | 257 | 258 | 更多资料: 259 | [Category](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/Category.html#//apple_ref/doc/uid/TP40008195-CH5-SW2) 260 | [深入理解Objective-C:Category](https://tech.meituan.com/DiveIntoCategory.html) 261 | [Objective-C Category 的实现原理](http://blog.leichunfeng.com/blog/2015/05/18/objective-c-category-implementation-principle/) 262 | [iOS Category详解](http://www.imlifengfeng.com/blog/?p=474) 263 | -------------------------------------------------------------------------------- /Runtime源码 autoreleasepool.md: -------------------------------------------------------------------------------- 1 | #Runtime源码 autoreleasepool 2 | 3 | ## 前言 4 | 5 | 在iOS开发中,由于ARC的普遍使用,内存管理的问题好像不那么常见了,但了解Objective-C的内存管理机制依然是非常必要的,今天我们来看看autorelease的一些细节,在ARC时代几乎很少看到autorelease的身影了,唯一常见的应该就是在```main```函数中了: 6 | 7 | ``` 8 | int main(int argc, char * argv[]) { 9 | @autoreleasepool { 10 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 11 | } 12 | } 13 | ``` 14 | 这里可以看到整个 iOS 的应用都是包含在一个自动释放池 block 中的。那么这个autoreleasepool到底是什么呢?接下来我们来一窥究竟。 15 | 16 | ## 结构 17 | 使用```clang -rewrite-objc main.m```将main函数所在的文件,转化为C++代码: 18 | 19 | ``` 20 | int main(int argc, char * argv[]) { 21 | /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 22 | return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")))); 23 | } 24 | } 25 | ``` 26 | 可以看到```autoreleasepool```其实是类型为```__AtAutoreleasePool ```的结构体,那么它又是什么呢?我们来看看他的定义: 27 | 28 | ``` 29 | struct __AtAutoreleasePool { 30 | __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} 31 | ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} 32 | void * atautoreleasepoolobj; 33 | } 34 | ``` 35 | 由此,我们可以知道:当使用autoreleasepool时,实际上是: 36 | 37 | ``` 38 | /* @autoreleasepool */ { 39 | void *atautoreleasepoolobj = objc_autoreleasePoolPush(); 40 | ...// 自己的代码,接收到 autorelease 消息的对象会被添加到这个 autoreleasepool中 41 | objc_autoreleasePoolPop(atautoreleasepoolobj); 42 | } 43 | ``` 44 | 45 | 它其实是调用了```objc_autoreleasePoolPush```,这又是什么?怎么越看越多?别急,马上到了,继续吧,打开runtime源码工程,在```NSObject.mm```文件中我们看到,其实他是: 46 | 47 | ``` 48 | void * 49 | objc_autoreleasePoolPush(void) 50 | { 51 | return AutoreleasePoolPage::push(); 52 | } 53 | ``` 54 | 最后,再来看看```AutoreleasePoolPage```: 55 | 56 | ``` 57 | class AutoreleasePoolPage { 58 | ... 59 | magic_t const magic; 60 | id *next; 61 | pthread_t const thread; 62 | AutoreleasePoolPage * const parent; 63 | AutoreleasePoolPage *child; 64 | uint32_t const depth; 65 | uint32_t hiwat; 66 | ... 67 | } 68 | ``` 69 | 终于看到他的结构了,总的来说,其实每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节(16 进制 0x1000)根据注释``` The stack is divided into a doubly-linked list of pages. Pages are added ```可以知道,他是以双链表的形式存储的,也对应了其结构中的parent和child,接下里我们看看他其他字段: 70 | 71 | - magic 72 | magic是```magic_t```类型的结构体,它用来校验 AutoreleasePoolPage结构的完整性 73 | - *next 74 | 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() 75 | - thread 76 | 当前线程 77 | - parent 78 | 指向父结点,第一个结点的 parent 值为 nil ; 79 | - child 80 | 指向子结点,最后一个结点的 child 值为 nil ; 81 | - depth 82 | 代表深度,从 0 开始,往后递增 1; 83 | - hiwat 84 | 代表 high water mark 。 85 | 86 | 87 | ### objc_autoreleasePoolPush() 88 | 上面我们看到了```objc_autoreleasePoolPush()```接下来,看看push的具体操作: 89 | 90 | ``` 91 | static inline void *push() 92 | { 93 | id *dest; 94 | if (DebugPoolAllocation) { 95 | // Each autorelease pool starts on a new pool page. 96 | dest = autoreleaseNewPage(POOL_BOUNDARY); 97 | } else { 98 | dest = autoreleaseFast(POOL_BOUNDARY); 99 | } 100 | assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); 101 | return dest; 102 | } 103 | ``` 104 | 这里有一个debug环境的判断,我们忽略,直接看到```autoreleaseFast()```: 105 | 106 | ``` 107 | static inline id *autoreleaseFast(id obj) 108 | { 109 | AutoreleasePoolPage *page = hotPage(); 110 | if (page && !page->full()) { 111 | return page->add(obj); 112 | } else if (page) { 113 | return autoreleaseFullPage(obj, page); 114 | } else { 115 | return autoreleaseNoPage(obj); 116 | } 117 | } 118 | ``` 119 | - 当前 page 存在且没有满时: 120 | 直接将对象添加到当前 page 中,即 next 指向的位置; 121 | - 当前 page 存在且已满时: 122 | 创建一个新的 page ,并将对象添加到新创建的 page 中; 123 | - 当前 page 不存在时即还没有 page 时: 124 | 创建第一个 page ,并将对象添加到新创建的 page 中。 125 | 126 | 每调用一次 push 操作就会创建一个新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一个POOL_BOUNDARY,并且返回插入的 POOL_BOUNDARY的内存地址。 127 | >从定义``` # define POOL_BOUNDARY nil```可知:这里的POOL_BOUNDARY其实就是nil的别名 128 | 129 | ### autorelease 130 | 接下来我们看看autorelease的实现,追溯源码发现它的函数调用栈很深,依次为: 131 | 132 | 1. ```-(id) autorelease``` 133 | 2. ```_objc_rootAutorelease(id obj)``` 134 | 3. ```objc_object::rootAutorelease()``` 135 | 4. ```objc_object::rootAutorelease2()``` 136 | 5. ```static inline id autorelease(id obj)``` 137 | 6. ```static inline id *autoreleaseFast(id obj)``` 138 | 139 | 它以obj为参数,通过层层调用,最终还是调用到了```autoreleaseFast```方法, 140 | 也就是说:它和 push 操作的实现基本一致,只是不过 push 操作插入的是一个 POOL_BOUNDARY ,而 autorelease 操作插入的是一个具体的 autoreleased 对象。 141 | 142 | ### objc_autoreleasePoolPop() 143 | 他其实是调用了: 144 | 145 | ``` 146 | objc_autoreleasePoolPop(void *ctxt) 147 | { 148 | AutoreleasePoolPage::pop(ctxt); 149 | } 150 | ``` 151 | pop调用到了```releaseUntil```里面有这样的操作: 152 | 153 | ``` 154 | while (this->next != stop) { 155 | // Restart from hotPage() every time, in case -release 156 | // autoreleased more objects 157 | AutoreleasePoolPage *page = hotPage(); 158 | 159 | // fixme I think this `while` can be `if`, but I can't prove it 160 | while (page->empty()) { 161 | page = page->parent; 162 | setHotPage(page); 163 | } 164 | 165 | page->unprotect(); 166 | id obj = *--page->next; 167 | memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); 168 | page->protect(); 169 | 170 | if (obj != POOL_BOUNDARY) { 171 | objc_release(obj); 172 | } 173 | } 174 | ``` 175 | 176 | pop可以说是:以上面说到的POOL_BOUNDARY的内存地址为入参,根据传入的POOL_BOUNDARY,找到这个push所在的位置,对之前入栈的每个antorealse对象都发送一次- release消息,并移动next指针,当指针的地址移动到POOL_BOUNDARY的位置时,这个自动释放池内的对象释放完毕。 177 | 178 | ## 应用 179 | 1. 当使用循环时,使用@autoreleasePool能降低内存峰值(详见《Effective Objective-C 2.0》第34条) 180 | 2. 遍历时尽量使用系统的enumerateObjectsUsingBlock,因为其内部有自动释放池,而for活着for-in没有 181 | 182 | ## 总结 183 | - 自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的。 184 | - 调用AutoreleasePoolPage::push向栈插入一个nil作为标记,并返回这个地址。 185 | - 当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中(由于后加,所以他们一定在nil所在位置之上)。 186 | - 调用 AutoreleasePoolPage::pop 方法,传递push函数得到的地址,依次向栈中的对象发送 release 消息,并下移指针,直到push的nil所在位置,即完成了自动释放。 187 | 188 | 参考资料: 189 | [NSAutoreleasePool](https://developer.apple.com/documentation/foundation/nsautoreleasepool#//apple_ref/occ/cl/NSAutoreleasePool) 190 | [黑幕背后的Autorelease](https://blog.sunnyxx.com/2014/10/15/behind-autorelease/) 191 | [Objective-C Autorelease Pool 的实现原理](http://blog.leichunfeng.com/blog/2015/05/31/objective-c-autorelease-pool-implementation-principle/) 192 | -------------------------------------------------------------------------------- /Runtime源码 protocol(协议).md: -------------------------------------------------------------------------------- 1 | #Runtime源码 protocol(协议) 2 | ### 一、概述 3 | 协议定义了一个纲领性的接口,所有类都可以选择实现。它主要是用来定义一套对象之间的通信规则。protocol也是我们设计时常用的一个东西,相对于直接继承的方式,protocol则偏向于组合模式。他使得两个毫不相关的类能够相互通信,从而实现特定的目标。因为OC是单继承的,由于不支持多继承,所以很多时候都是用Protocol和Category来代替实现"多继承"。 4 | 5 | ### 二、底层实现 6 | 在objc4-723中protocol的定义如下: 7 | 8 | ``` 9 | struct protocol_t : objc_object { 10 | const char *mangledName; 11 | struct protocol_list_t *protocols; 12 | method_list_t *instanceMethods; 13 | method_list_t *classMethods; 14 | method_list_t *optionalInstanceMethods; 15 | method_list_t *optionalClassMethods; 16 | property_list_t *instanceProperties; 17 | uint32_t size; // sizeof(protocol_t) 18 | uint32_t flags; 19 | // Fields below this point are not always present on disk. 20 | const char **_extendedMethodTypes; 21 | const char *_demangledName; 22 | property_list_t *_classProperties; 23 | 24 | const char *demangledName(); 25 | ... 26 | }; 27 | 28 | ``` 29 | 我们可以看到protocol继承自objc_object,里面的字段基本算是清晰,主要结构如下: 30 | 31 | - mangledName 和 _demangledName 32 | 这是来源于C++的name mangling(命名重整)技术,在C++里面用来区别重载是的函数。 33 | 34 | - protocols 35 | 它是protocol_list_t类型的指针,保存了这个协议所遵守的协议; 36 | 37 | - instanceMethods 38 | 实例方法列表 39 | 40 | - calssMethods 41 | 类方法列表 42 | 43 | - optionalInstanceMethods 44 | 可选择实现的实例方法,在声明时用@optional关键字修饰的实例方法 45 | 46 | - optionalClassMethods 47 | 可选择实现的类方法,在声明时用@optional关键字修饰的类方法 48 | 49 | - instanceProperties 50 | 实例属性 51 | 52 | - _classProperties 53 | 类属性,比较少见 54 | 55 | ``` 56 | @property (nonatomic, strong) NSString *name;//通过实例调用 57 | @property (class, nonatomic, strong) NSString *className;//通过类名调用 58 | ``` 59 | ### 三、 常用方法 60 | - protocol_copyPropertyList 61 | runtime提供了两个方法: 62 | 63 | ``` 64 | objc_property_t * 65 | protocol_copyPropertyList(Protocol *proto, unsigned int *outCount) 66 | { 67 | return protocol_copyPropertyList2(proto, outCount, 68 | YES/*required*/, YES/*instance*/); 69 | } 70 | 71 | 72 | objc_property_t * 73 | protocol_copyPropertyList2(Protocol *proto, unsigned int *outCount, 74 | BOOL isRequiredProperty, BOOL isInstanceProperty) 75 | { 76 | if (!proto || !isRequiredProperty) { 77 | // Optional properties are not currently supported. 78 | if (outCount) *outCount = 0; 79 | return nil; 80 | } 81 | 82 | rwlock_reader_t lock(runtimeLock); 83 | 84 | property_list_t *plist = isInstanceProperty 85 | ? newprotocol(proto)->instanceProperties 86 | : newprotocol(proto)->classProperties(); 87 | return (objc_property_t *)copyPropertyList(plist, outCount); 88 | } 89 | 90 | ``` 91 | **// Optional properties are not currently supported.** 92 | 这里指明了可选属性现在还不支持,这就是没有可选属性的原因 93 | 94 | - conformsToProtocol() 95 | 96 | ``` 97 | + (BOOL)conformsToProtocol:(Protocol *)protocol { 98 | if (!protocol) return NO; 99 | for (Class tcls = self; tcls; tcls = tcls->superclass) { 100 | if (class_conformsToProtocol(tcls, protocol)) return YES; 101 | } 102 | return NO; 103 | } 104 | 105 | - (BOOL)conformsToProtocol:(Protocol *)protocol { 106 | if (!protocol) return NO; 107 | for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { 108 | if (class_conformsToProtocol(tcls, protocol)) return YES; 109 | } 110 | return NO; 111 | } 112 | ``` 113 | 两个方法都是遍历类的继承集体,调用``` class_conformsToProtocol ```方法,其实现如下: 114 | 115 | ``` 116 | BOOL class_conformsToProtocol(Class cls, Protocol *proto_gen) 117 | { 118 | protocol_t *proto = newprotocol(proto_gen); 119 | 120 | if (!cls) return NO; 121 | if (!proto_gen) return NO; 122 | 123 | rwlock_reader_t lock(runtimeLock); 124 | 125 | assert(cls->isRealized()); 126 | 127 | for (const auto& proto_ref : cls->data()->protocols) { 128 | protocol_t *p = remapProtocol(proto_ref); 129 | if (p == proto || protocol_conformsToProtocol_nolock(p, proto)) { 130 | return YES; 131 | } 132 | } 133 | 134 | return NO; 135 | } 136 | ``` 137 | 把class的protocols取出来,并与传入的protocol做比较,如果地址相同直接返回,或者协议"继承"的层级中满足条件: 138 | 139 | ``` 140 | /*********************************************************************** 141 | * protocol_conformsToProtocol_nolock 142 | * Returns YES if self conforms to other. 143 | * Locking: runtimeLock must be held by the caller. 144 | **********************************************************************/ 145 | static bool 146 | protocol_conformsToProtocol_nolock(protocol_t *self, protocol_t *other) 147 | { 148 | runtimeLock.assertLocked(); 149 | 150 | if (!self || !other) { 151 | return NO; 152 | } 153 | 154 | // protocols need not be fixed up 155 | 156 | if (0 == strcmp(self->mangledName, other->mangledName)) { 157 | return YES; 158 | } 159 | 160 | if (self->protocols) { 161 | uintptr_t i; 162 | for (i = 0; i < self->protocols->count; i++) { 163 | protocol_t *proto = remapProtocol(self->protocols->list[i]); 164 | if (0 == strcmp(other->mangledName, proto->mangledName)) { 165 | return YES; 166 | } 167 | if (protocol_conformsToProtocol_nolock(proto, other)) { 168 | return YES; 169 | } 170 | } 171 | } 172 | 173 | return NO; 174 | } 175 | ``` 176 | 递归处理,对比协议的mangledName,有相同的就返回YES。 177 | 178 | 参考: 179 | [协议protocol](https://www.jianshu.com/p/fe8048524e67) 180 | [Protocol](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/Protocol.html) -------------------------------------------------------------------------------- /Runtime源码 成员变量与属性.md: -------------------------------------------------------------------------------- 1 | #Runtime源码 成员变量与属性 2 | 上篇文章我们了解了[类、对象和isa](https://www.jianshu.com/p/174a94704aa0)在runtime中的表示,现在来看看runtime对成员变量和属性的处理。在此之前我们先看看一个重要的概念:**类型编码** 3 | 4 | ### 类型编码 5 | 编译器将每个方法的返回值和参数类型编码为一个字符串,并且将其与方法的selector关联在一起。这个编码方案在其他情况下也是十分有用的,因此我们可以使用@encode编译器指令来获取它: 6 | 7 | ``` 8 | NSLog(@"%s",@encode(NSString *)); 9 | NSLog(@"%s",@encode(NSInteger)); 10 | 11 | //结果: 12 | 2018-10-04 16:02:09.429772+0800 Demo[2690:231491] @ 13 | 2018-10-04 16:02:09.429952+0800 Demo[2690:231491] q 14 | ``` 15 | 当给定一个类型时,@encode返回这个类型的字符串编码。这些类型可以是诸如int、指针这样的基本类型,也可以是结构体、类等类型。事实上,任何可以作为sizeof()操作参数的类型都可以用于@encode()。 16 | 17 | 在Objective-C Runtime Programming Guide中的[Type Encoding](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1)一节中,列出了Objective-C中所有的类型编码。需要注意的是这些类型很多是与我们用于存档和分发的编码类型是相同的。但有一些不能在存档时使用。 18 | 19 | >Objective-C不支持long double类型。@encode(long double)返回d,与double是一样的。 20 | 21 | 一个数组的类型编码位于方括号中;其中包含数组元素的个数及元素类型。如以下示例: 22 | 23 | ``` 24 | float a[] = {1.0, 2.0, 3.0}; 25 | NSLog(@"array encoding type: %s", @encode(typeof(a))); 26 | NSNumber *b[] = {@1, @2, @3, @4}; 27 | NSLog(@"array encoding type: %s", @encode(typeof(b))); 28 | 29 | //结果: 30 | Demo[2858:246573] array encoding type: [3f] 31 | Demo[2858:246573] array encoding type: [4@] 32 | ``` 33 | 另外,还有些编码类型,@encode虽然不会直接返回它们,但它们可以作为协议中声明的方法的类型限定符。可以参考[Type Encoding](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1)。 34 | 35 | 对于属性而言,还会有一些特殊的类型编码,以表明属性是只读、拷贝、retain等等,详情可以参考[Property Type String](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW6)。 36 | 37 | ### 成员变量、属性 38 | Runtime中关于成员变量和属性的相关数据结构并不多,只有三个,并且都很简单。不过还有个非常实用但可能经常被忽视的特性,即关联对象,我们将在这小节中详细讨论。 39 | 40 | ### 基础数据类型 41 | 42 | - Ivar 43 | Ivar是表示实例变量的类型,其实际上是一个指向objc_ivar结构体的指针,定义如下: 44 | 45 | ``` 46 | struct objc_ivar { 47 | char * _Nullable ivar_name OBJC2_UNAVAILABLE; 48 | char * _Nullable ivar_type OBJC2_UNAVAILABLE; 49 | int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移字节 50 | #ifdef __LP64__ 51 | int space OBJC2_UNAVAILABLE; 52 | #endif 53 | } 54 | ``` 55 | 56 | - objc_property_t 57 | objc_property_t是表示Objective-C声明的属性的类型,其实际是指向objc_property结构体的指针,其定义如下: 58 | 59 | ``` 60 | typedef struct objc_property *objc_property_t; 61 | 62 | ``` 63 | objc_property_attribute_t定义了属性的特性(attribute),它是一个结构体,定义如下: 64 | 65 | ``` 66 | typedef struct { 67 | constchar *name;// 特性名 68 | constchar *value;// 特性值 69 | } objc_property_attribute_t; 70 | ``` 71 | 72 | - 关联对象(Associated Object) 73 | 关联对象是Runtime中一个非常实用的特性,不过可能很容易被忽视。 74 | 75 | 关联对象类似于成员变量,不过是在运行时添加的。我们通常会把成员变量(Ivar)放在类声明的头文件中,或者放在类实现的@implementation后面。但这有一个缺点,我们不能在分类中添加成员变量。如果我们尝试在分类中添加新的成员变量,编译器会报错。 76 | 77 | 我们可能希望通过使用(甚至是滥用)全局变量来解决这个问题。但这些都不是Ivar,因为他们不会连接到一个单独的实例。因此,这种方法很少使用。 78 | 79 | Objective-C针对这一问题,提供了一个解决方案:即关联对象(Associated Object)。 80 | 81 | 我们可以把关联对象想象成一个Objective-C对象(如字典),这个对象通过给定的key连接到类的一个实例上。不过由于使用的是C接口,所以key是一个void指针(const void *)。我们还需要指定一个内存管理策略,以告诉Runtime如何管理这个对象的内存。这个内存管理的策略可以由以下值指定: 82 | >OBJC_ ASSOCIATION_ ASSIGN 83 | > 84 | >OBJC_ ASSOCIATION_ RETAIN_ NONATOMIC 85 | > 86 | >OBJC_ ASSOCIATION_ COPY_ NONATOMIC 87 | > 88 | >OBJC_ ASSOCIATION_ RETAIN 89 | > 90 | >OBJC_ ASSOCIATION_ COPY 91 | 92 | 当宿主对象被释放时,会根据指定的内存管理策略来处理关联对象。如果指定的策略是assign,则宿主释放时,关联对象不会被释放;而如果指定的是retain或者是copy,则宿主释放时,关联对象会被释放。我们甚至可以选择是否是自动retain/copy。当我们需要在多个线程中处理访问关联对象的多线程代码时,这就非常有用了。 93 | 94 | 我们将一个对象连接到其它对象所需要做的就是下面两行代码: 95 | 96 | ``` 97 | static char myKey; 98 | objc_setAssociatedObject(self, &myKey, anObject, OBJC_ASSOCIATION_RETAIN); 99 | ``` 100 | 在这种情况下,self对象将获取一个新的关联的对象anObject,且内存管理策略是自动retain关联对象,当self对象释放时,会自动release关联对象。另外,如果我们使用同一个key来关联另外一个对象时,也会自动释放之前关联的对象,这种情况下,先前的关联对象会被妥善地处理掉,并且新的对象会使用它的内存。 101 | 102 | ``` 103 | id anObject = objc_getAssociatedObject(self, &myKey); 104 | ``` 105 | 我们可以使用objc_removeAssociatedObjects函数来移除一个关联对象,或者使用objc_setAssociatedObject函数将key指定的关联对象设置为nil。 106 | 107 | ### 成员变量、属性的操作方法 108 | - 成员变量 109 | 成员变量操作包含以下函数: 110 | 111 | ``` 112 | // 获取成员变量名 113 | const char * ivar_getName (Ivar v); 114 | 115 | // 获取成员变量类型编码 116 | const char * ivar_getTypeEncoding (Ivar v); 117 | 118 | // 获取成员变量的偏移量 119 | ptrdiff_t ivar_getOffset (Ivar v); 120 | ``` 121 | ivar_getOffset函数,对于类型id或其它对象类型的实例变量,可以调用object_getIvar和object_setIvar来直接访问成员变量,而不使用偏移量。 122 | 123 | - 属性 124 | 属性操作相关函数包括以下: 125 | 126 | ``` 127 | // 获取属性名 128 | const char * property_getName (objc_property_t property); 129 | 130 | // 获取属性特性描述字符串 131 | const char * property_getAttributes (objc_property_t property); 132 | 133 | // 获取属性中指定的特性 134 | char * property_copyAttributeValue (objc_property_t property, const char *attributeName); 135 | 136 | // 获取属性的特性列表 137 | objc_property_attribute_t * property_copyAttributeList (objc_property_t property, unsigned int *outCount); 138 | ``` 139 | property_copyAttributeValue函数,返回的char *在使用完后需要调用free()释放。 property_copyAttributeList函数,返回值在使用完后需要调用free()释放。 140 | 141 | 142 | - 关联对象 143 | 关联对象操作函数包括以下: 144 | 145 | ``` 146 | // 设置关联对象 147 | void objc_setAssociatedObject (id object, const void *key, id value, objc_AssociationPolicy policy); 148 | 149 | // 获取关联对象 150 | id objc_getAssociatedObject (id object, const void *key); 151 | 152 | // 移除关联对象 153 | void objc_removeAssociatedObjects (id object); 154 | ``` 155 | 156 | [南峰子的技术博客](http://southpeak.github.io/blog/2014/10/30/objective-c-runtime-yun-xing-shi-zhi-er-:cheng-yuan-bian-liang-yu-shu-xing/) 157 | -------------------------------------------------------------------------------- /Runtime源码 方法调用的过程.md: -------------------------------------------------------------------------------- 1 | # Runtime源码 方法调用的过程 2 | ## 前言 3 | Objective-C语言的一大特性就是动态的,根据[官方文档](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtHowMessagingWorks.html#//apple_ref/doc/uid/TP40008048-CH104-SW1)的描述:在runtime之前,消息和方法并不是绑定在一起的,编译器会把方法调用转换为```objc_msgSend(receiver, selector)```,如果方法中带有参数则转换为```objc_msgSend(receiver, selector, arg1, arg2, ...)```接下来我们通过源码一窥究竟,在次之前我们先了解几个基本概念 4 | 5 | - SEL 6 | 在objc.h文件中我们可以看到如下代码: 7 | 8 | ``` 9 | /// An opaque type that represents a method selector. 10 | typedef struct objc_selector *SEL; 11 | ``` 12 | SEL其实就是一个不透明的类型它代表一个方法选择子,在编译期,会根据方法名字生成一个ID。 13 | 14 | - IMP 15 | 在objc.h文件中我们可以看到IMP: 16 | 17 | ``` 18 | /// A pointer to the function of a method implementation. 19 | #if !OBJC_OLD_DISPATCH_PROTOTYPES 20 | typedef void (*IMP)(void /* id, SEL, ... */ ); 21 | #else 22 | typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 23 | #endif 24 | ``` 25 | 他是一个函数指针,指向方法实现的首地址。 26 | 27 | - Method 28 | 29 | ``` 30 | /// An opaque type that represents a method in a class definition. 31 | typedef struct objc_method *Method; 32 | 33 | struct objc_method { 34 | SEL _Nonnull method_name OBJC2_UNAVAILABLE; 35 | char * _Nullable method_types OBJC2_UNAVAILABLE; 36 | IMP _Nonnull method_imp OBJC2_UNAVAILABLE; 37 | } OBJC2_UNAVAILABLE; 38 | ``` 39 | 它保存了SEL到IMP和方法类型,所以我们可以通过SEL调用对应的IMP 40 | 41 | ## 方法调用的流程 42 | objc_msgSend的消息分发分为以下几个步骤: 43 | 我们找到objc _msgSend源码,都是汇编,不过注释比较详尽 44 | 45 | ``` 46 | /******************************************************************** 47 | * 48 | * id objc_msgSend(id self, SEL _cmd,...); 49 | * IMP objc_msgLookup(id self, SEL _cmd, ...); 50 | * 51 | * objc_msgLookup ABI: 52 | * IMP returned in r11 53 | * Forwarding returned in Z flag 54 | * r10 reserved for our use but not used 55 | * 56 | ********************************************************************/ 57 | 58 | .data 59 | .align 3 60 | .globl _objc_debug_taggedpointer_classes 61 | _objc_debug_taggedpointer_classes: 62 | .fill 16, 8, 0 63 | .globl _objc_debug_taggedpointer_ext_classes 64 | _objc_debug_taggedpointer_ext_classes: 65 | .fill 256, 8, 0 66 | 67 | ENTRY _objc_msgSend 68 | UNWIND _objc_msgSend, NoFrame 69 | MESSENGER_START 70 | 71 | NilTest NORMAL 72 | 73 | GetIsaFast NORMAL // r10 = self->isa 74 | CacheLookup NORMAL, CALL // calls IMP on success 75 | 76 | NilTestReturnZero NORMAL 77 | 78 | GetIsaSupport NORMAL 79 | 80 | // cache miss: go search the method lists 81 | LCacheMiss: 82 | // isa still in r10 83 | MESSENGER_END_SLOW 84 | jmp __objc_msgSend_uncached 85 | 86 | END_ENTRY _objc_msgSend 87 | 88 | 89 | ENTRY _objc_msgLookup 90 | 91 | NilTest NORMAL 92 | 93 | GetIsaFast NORMAL // r10 = self->isa 94 | CacheLookup NORMAL, LOOKUP // returns IMP on success 95 | 96 | NilTestReturnIMP NORMAL 97 | 98 | GetIsaSupport NORMAL 99 | 100 | // cache miss: go search the method lists 101 | LCacheMiss: 102 | // isa still in r10 103 | jmp __objc_msgLookup_uncached 104 | 105 | END_ENTRY _objc_msgLookup 106 | 107 | 108 | ENTRY _objc_msgSend_fixup 109 | int3 110 | END_ENTRY _objc_msgSend_fixup 111 | 112 | 113 | STATIC_ENTRY _objc_msgSend_fixedup 114 | // Load _cmd from the message_ref 115 | movq 8(%a2), %a2 116 | jmp _objc_msgSend 117 | END_ENTRY _objc_msgSend_fixedup 118 | 119 | ``` 120 | 121 | 就此我们大概可以了解到其调用流程: 122 | 123 | - 判断receiver是否为nil,也就是objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象 124 | - 从缓存里寻找,找到了则分发,否则 125 | - 利用objc-class.mm中_ class _lookupMethodAndLoadCache3方法去寻找selector 126 | 127 | - 如果支持GC,忽略掉非GC环境的方法(retain等) 128 | - 从本class的method list寻找selector,如果找到,填充到缓存中,并返回selector,否则 129 | - 寻找父类的method list,并依次往上寻找,直到找到selector,填充到缓存中,并返回selector,否则 130 | - 调用_class_resolveMethod,如果可以动态resolve为一个selector,不缓存,方法返回,否则 131 | - 转发这个selector,否则 132 | - 报错,抛出异常 133 | 134 | 这里的**_ class _lookupMethodAndLoadCache3**其实就是对**lookUpImpOrForward**方法的调用: 135 | 136 | ``` 137 | /*********************************************************************** 138 | * _class_lookupMethodAndLoadCache. 139 | * Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp(). 140 | * This lookup avoids optimistic cache scan because the dispatcher 141 | * already tried that. 142 | **********************************************************************/ 143 | IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) 144 | { 145 | return lookUpImpOrForward(cls, sel, obj, 146 | YES/*initialize*/, NO/*cache*/, YES/*resolver*/); 147 | } 148 | ``` 149 | 对第五个参数**cache**传值为NO,因为在此之前已经做了一个查找这里**CacheLookup NORMAL, CALL**,这里是对缓存查找的一个优化。 150 | 151 | 接下来看一下lookUpImpOrForward的一些关键实现细节 152 | 153 | - 缓存查找优化 154 | 155 | ``` 156 | // Optimistic cache lookup 157 | if (cache) 158 | methodPC = _cache_getImp(cls, sel); 159 | if (methodPC) return methodPC; 160 | } 161 | ``` 162 | 这里有个判断,是否需要缓存查找,如果cache为NO则进入下一步 163 | 164 | - 检查被释放类 165 | 166 | ``` 167 | // Check for freed class 168 | if (cls == _class_getFreedObjectClass()) 169 | return (IMP) _freedHandler; 170 | ``` 171 | 172 | **_class _getFreedObjectClass**的实现: 173 | 174 | ``` 175 | /*********************************************************************** 176 | * _class_getFreedObjectClass. Return a pointer to the dummy freed 177 | * object class. Freed objects get their isa pointers replaced with 178 | * a pointer to the freedObjectClass, so that we can catch usages of 179 | * the freed object. 180 | **********************************************************************/ 181 | static Class _class_getFreedObjectClass(void) 182 | { 183 | return (Class)freedObjectClass; 184 | } 185 | ``` 186 | 注释写到,这里返回的被释放对象的指针,不是太理解,备注这以后再看看 187 | 188 | - 懒加载+initialize 189 | 190 | ``` 191 | // Check for +initialize 192 | if (initialize && !cls->isInitialized()) { 193 | _class_initialize (_class_getNonMetaClass(cls, inst)); 194 | // If sel == initialize, _class_initialize will send +initialize and 195 | // then the messenger will send +initialize again after this 196 | // procedure finishes. Of course, if this is not being called 197 | // from the messenger then it won't happen. 2778172 198 | } 199 | ``` 200 | 在方法调用过程中,如果类没有被初始化的时候,会调用_class_initialize对类进行初始化,关于+initialize可以看之前的[Runtime源码 +load 和 +initialize](https://www.jianshu.com/p/cea0ace15da8)。 201 | 202 | - 加锁保证原子性 203 | 204 | ``` 205 | // The lock is held to make method-lookup + cache-fill atomic 206 | // with respect to method addition. Otherwise, a category could 207 | // be added but ignored indefinitely because the cache was re-filled 208 | // with the old value after the cache flush on behalf of the category. 209 | retry: 210 | methodListLock.lock(); 211 | 212 | // Try this class's cache. 213 | 214 | methodPC = _cache_getImp(cls, sel); 215 | if (methodPC) goto done; 216 | ``` 217 | 这里又做了一次缓存查找,因为上一步执行了+initialize 218 | >加锁这一部分只有一行简单的代码,其主要目的保证方法查找以及缓存填充(cache-fill)的原子性,保证在运行以下代码时不会有新方法添加导致缓存被冲洗(flush)。 219 | 220 | - 本类的方法列表查找 221 | 222 | ``` 223 | // Try this class's method lists. 224 | 225 | meth = _class_getMethodNoSuper_nolock(cls, sel); 226 | if (meth) { 227 | log_and_fill_cache(cls, cls, meth, sel); 228 | methodPC = method_getImplementation(meth); 229 | goto done; 230 | } 231 | ``` 232 | 这里调用了**log_ and_ fill_cache**这个后面来看,接下里就是 233 | 234 | - 父类方法列表查找 235 | 236 | ``` 237 | // Try superclass caches and method lists. 238 | 239 | curClass = cls; 240 | while ((curClass = curClass->superclass)) { 241 | // Superclass cache. 242 | meth = _cache_getMethod(curClass, sel, _objc_msgForward_impcache); 243 | if (meth) { 244 | if (meth != (Method)1) { 245 | // Found the method in a superclass. Cache it in this class. 246 | log_and_fill_cache(cls, curClass, meth, sel); 247 | methodPC = method_getImplementation(meth); 248 | goto done; 249 | } 250 | else { 251 | // Found a forward:: entry in a superclass. 252 | // Stop searching, but don't cache yet; call method 253 | // resolver for this class first. 254 | break; 255 | } 256 | } 257 | 258 | // Superclass method list. 259 | meth = _class_getMethodNoSuper_nolock(curClass, sel); 260 | if (meth) { 261 | log_and_fill_cache(cls, curClass, meth, sel); 262 | methodPC = method_getImplementation(meth); 263 | goto done; 264 | } 265 | } 266 | ``` 267 | 关于消息在列表方法查找的过程,根据官方文档如下: 268 | 269 | ![messaging1](https://upload-images.jianshu.io/upload_images/2598795-0d45c94f8f83fd00.gif?imageMogr2/auto-orient/strip) 270 | 271 | 这里沿着集成体系对父类的方法列表进行查找,找到了就调用**log_ and_ fill_cache** 272 | 273 | log_ and_ fill_cach的实现: 274 | 记录: 275 | 276 | ``` 277 | /*********************************************************************** 278 | * log_and_fill_cache 279 | * Log this method call. If the logger permits it, fill the method cache. 280 | * cls is the method whose cache should be filled. 281 | * implementer is the class that owns the implementation in question. 282 | **********************************************************************/ 283 | static void 284 | log_and_fill_cache(Class cls, Class implementer, Method meth, SEL sel) 285 | { 286 | #if SUPPORT_MESSAGE_LOGGING 287 | if (objcMsgLogEnabled) { 288 | bool cacheIt = logMessageSend(implementer->isMetaClass(), 289 | cls->nameForLogging(), 290 | implementer->nameForLogging(), 291 | sel); 292 | if (!cacheIt) return; 293 | } 294 | #endif 295 | _cache_fill (cls, meth, sel); 296 | } 297 | ``` 298 | 299 | 内部调用了**_cache _fill**,填充缓存: 300 | 301 | ``` 302 | /*********************************************************************** 303 | * _cache_fill. Add the specified method to the specified class' cache. 304 | * Returns NO if the cache entry wasn't added: cache was busy, 305 | * class is still being initialized, new entry is a duplicate. 306 | * 307 | * Called only from _class_lookupMethodAndLoadCache and 308 | * class_respondsToMethod and _cache_addForwardEntry. 309 | * 310 | * Cache locks: cacheUpdateLock must not be held. 311 | **********************************************************************/ 312 | bool _cache_fill(Class cls, Method smt, SEL sel) 313 | { 314 | uintptr_t newOccupied; 315 | uintptr_t index; 316 | cache_entry **buckets; 317 | cache_entry *entry; 318 | Cache cache; 319 | 320 | cacheUpdateLock.assertUnlocked(); 321 | 322 | // Never cache before +initialize is done 323 | if (!cls->isInitialized()) { 324 | return NO; 325 | } 326 | 327 | // Keep tally of cache additions 328 | totalCacheFills += 1; 329 | 330 | mutex_locker_t lock(cacheUpdateLock); 331 | 332 | entry = (cache_entry *)smt; 333 | 334 | cache = cls->cache; 335 | 336 | // Make sure the entry wasn't added to the cache by some other thread 337 | // before we grabbed the cacheUpdateLock. 338 | // Don't use _cache_getMethod() because _cache_getMethod() doesn't 339 | // return forward:: entries. 340 | if (_cache_getImp(cls, sel)) { 341 | return NO; // entry is already cached, didn't add new one 342 | } 343 | 344 | // Use the cache as-is if it is less than 3/4 full 345 | newOccupied = cache->occupied + 1; 346 | if ((newOccupied * 4) <= (cache->mask + 1) * 3) { 347 | // Cache is less than 3/4 full. 348 | cache->occupied = (unsigned int)newOccupied; 349 | } else { 350 | // Cache is too full. Expand it. 351 | cache = _cache_expand (cls); 352 | 353 | // Account for the addition 354 | cache->occupied += 1; 355 | } 356 | 357 | // Scan for the first unused slot and insert there. 358 | // There is guaranteed to be an empty slot because the 359 | // minimum size is 4 and we resized at 3/4 full. 360 | buckets = (cache_entry **)cache->buckets; 361 | for (index = CACHE_HASH(sel, cache->mask); 362 | buckets[index] != NULL; 363 | index = (index+1) & cache->mask) 364 | { 365 | // empty 366 | } 367 | buckets[index] = entry; 368 | 369 | return YES; // successfully added new cache entry 370 | } 371 | ``` 372 | 373 | 这里还没找到实现则进入下一步,动态方法解析和消息转发,关于消息转发的细节我们下篇再看。 374 | 375 | ## 方法缓存 376 | 在上面截出的源码中我们多次看到了**cache**,下面我们就来看看这个,在```runtime.h```和```objc-runtime-new```cache的定义如下 377 | 378 | ``` 379 | struct objc_cache { 380 | unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE; 381 | unsigned int occupied OBJC2_UNAVAILABLE; 382 | Method _Nullable buckets[1] OBJC2_UNAVAILABLE; 383 | }; 384 | ``` 385 | ``` 386 | struct cache_t { 387 | struct bucket_t *_buckets; 388 | mask_t _mask; 389 | mask_t _occupied; 390 | ... 391 | } 392 | ``` 393 | 这就是cache在runtime层面的表示,里面的字段和代表的含义类似 394 | 395 | - buckets 396 | 数组表示的hash表,每个元素代表一个方法缓存 397 | - mask 398 | 当前能达到的最大index(从0开始),,所以缓存的size(total)是mask+1 399 | - occupied 400 | 被占用的槽位,因为缓存是以散列表的形式存在的,所以会有空槽,而occupied表示当前被占用的数目 401 | 402 | 而在_ buckets中包含了一个个的**cache_entry**和**bucket_t**(objc2.0的变更): 403 | 404 | ``` 405 | typedef struct { 406 | SEL name; // same layout as struct old_method 407 | void *unused; 408 | IMP imp; // same layout as struct old_method 409 | } cache_entry; 410 | ``` 411 | cache_entry定义也包含了三个字段,分别是: 412 | 413 | - name,被缓存的方法名字 414 | - unused,保留字段,还没被使用。 415 | - imp,方法实现 416 | 417 | ``` 418 | struct bucket_t { 419 | private: 420 | cache_key_t _key; 421 | IMP _imp; 422 | ... 423 | } 424 | ``` 425 | 而bucket_t则没有了老的unused,包含了两个字段: 426 | 427 | - key,方法的标志(和之前的name对应) 428 | - imp, 方法的实现 429 | 430 | ## 后记 431 | 从runtime的源码我们知道了方法调用的流程和方法缓存,有些附带的问题答案也就呼之欲出了: 432 | 433 | - 方法缓存在元类的上,由第一节([Runtime源码 类、对象、isa](https://www.jianshu.com/p/174a94704aa0))我们就知道在objc_class的isa指向了他的元类,所以每个类都只有一份方法缓存,而不是每一个类的object都保存一份。 434 | - 在方法调用的父类方法列表查找过程中,如果命中了也会调用```_cache_fill (cls, meth, sel);```,所以即便是从父类取到的方法,也会存在类本身的方法缓存里。而当用一个父类对象去调用那个方法的时候,也会在父类的metaclass里缓存一份。 435 | - 缓存容量限制,在上面的代码中我们注意到这个判断: 436 | 437 | ``` 438 | // Use the cache as-is if it is less than 3/4 full 439 | mask_t newOccupied = cache->occupied() + 1; 440 | mask_t capacity = cache->capacity(); 441 | if (cache->isConstantEmptyCache()) { 442 | // Cache is read-only. Replace it. 443 | cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); 444 | } 445 | else if (newOccupied <= capacity / 4 * 3) { 446 | // Cache is less than 3/4 full. Use it as-is. 447 | } 448 | else { 449 | // Cache is too full. Expand it. 450 | cache->expand(); 451 | } 452 | ``` 453 | 当cache为空时创建;当新的被占用槽数小于等于其容量的3/4时,直接使用;否则调用```cache->expand();```扩充容量: 454 | 455 | ``` 456 | void cache_t::expand() 457 | { 458 | cacheUpdateLock.assertLocked(); 459 | 460 | uint32_t oldCapacity = capacity(); 461 | uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; 462 | 463 | if ((uint32_t)(mask_t)newCapacity != newCapacity) { 464 | // mask overflow - can't grow further 465 | // fixme this wastes one bit of mask 466 | newCapacity = oldCapacity; 467 | } 468 | 469 | reallocate(oldCapacity, newCapacity); 470 | } 471 | ``` 472 | 473 | - 为什么类的方法列表不直接做成散列表呢,做成list,还要单独缓存,多费事? 474 | 散列表是没有顺序的,Objective-C的方法列表是一个list,是有顺序的 475 | 这个问题么,我觉得有以下三个原因: 476 | - Objective-C在查找方法的时候会顺着list依次寻找,并且category的方法在原始方法list的前面,需要先被找到,如果直接用hash存方法,方法的顺序就没法保证。 477 | - list的方法还保存了除了selector和imp之外其他很多属性。 478 | - 散列表是有空槽的,会浪费空间。 479 | 480 | 相关资料: 481 | 美团酒旅博文:[深入理解Objective-C:方法缓存](https://tech.meituan.com/DiveIntoMethodCache.html) 482 | 官方文档:[Messaging](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtHowMessagingWorks.html#//apple_ref/doc/uid/TP40008048-CH104-SW1) 483 | 484 | -------------------------------------------------------------------------------- /Runtime源码 类、对象、isa.md: -------------------------------------------------------------------------------- 1 | # Runtime源码 类、对象、isa 2 | OC做为一门动态语言,runtime是其最大的特点,它是一套底层的 C 语言 API,是 iOS 系统的核心之一。开发者在编码过程中,可以给任意一个对象发送消息,在编译阶段只是确定了要向接收者发送这条消息,而接受者将要如何响应和处理这条消息,那就要看运行时来决定了。在日常开发过程中类、对象、属性算是最常见的了,今天以对象为切入点,分析一下对象和类在runtime层面的表示。 3 | ## 类 4 | 类在runtime的表示为: 5 |
  6 | struct objc_object {
  7 | private:
  8 |     isa_t isa;
  9 |     ...
 10 | }
 11 | 
12 | 最关键的就是**isa_t**这个类型和一系列的构造函数,点击查看**isa_t**发现这是一个联合体: 13 |
 14 | union isa_t 
 15 | {
 16 |     isa_t() { }
 17 |     isa_t(uintptr_t value) : bits(value) { }
 18 |     Class cls;
 19 |  }
 20 | 
21 | 可以看到这个联合体里面有个Class类型的属性cls,看起来里面应该是关于这个对象的类的相关信息,那我们再看看Class包含了哪些内容。 22 |
 23 | struct objc_class : objc_object {
 24 |     // Class ISA;
 25 |     Class superclass;
 26 |     cache_t cache;             // formerly cache pointer and vtable
 27 |     class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
 28 |     ...
 29 | }
 30 | 
31 | 由此可见Class是一个objcclass类型的结构体,而objcclas继承字objc_object,说明类也是一个对象,只是比普通的对象多了一些属性,比如superclass等。 32 | 先不看这些属性,这里还有一个很奇怪的问题,既然类也是一个objc_object,那就是说类也有一个isa指针,那类的isa指针指向哪里呢?查看了不少资料,这篇讲的挺好:[Classes and metaclasses](http://www.sealiesoftware.com/blog/archive/2009/04/14/objc_explain_Classes_and_metaclasses.html) 33 | 大致意思是在class之上还有叫做元类(meta class)的存在,而class的isa指针就是指向对应的meta class。 34 | 35 | 我们都知道class中存储的是描述对象的相关信息,那么相应的meta class中存放的就是描述class相关信息。说的更直白一点,在我们写代码时,通过对象来调用的方法(实例方法)都是存储在class中的,通过类名来调用的方法(类方法)都是存储在meta class中的。 36 | 37 | 到这里对象和类的关系已经比较清楚了,但是如果细细思考一下,会发现还有一个问题,就是meta class也是有isa指针的,那么这个isa又指向了哪里呢?在上面给出的那篇文章里面有这么一张图: 38 | ![class diagram](https://upload-images.jianshu.io/upload_images/4670835-0b5ed7e3cd00094a.jpeg) 39 | 由图可知:meta class的isa指向root meta class(绝大部分情况下是NSObject),root meta class的isa指针指向自己。 40 | ## isa_t 41 | 先看看完整的**isa_t**,因为运行环境是osx,所以只截取x86_64部分,arm64的区别只在于部分字段的位数不同,字段是完全相同的: 42 | 43 | ``` 44 | union isa_t 45 | { 46 | isa_t() { } 47 | isa_t(uintptr_t value) : bits(value) { } 48 | Class cls; 49 | uintptr_t bits; 50 | # __x86_64__ 51 | # define ISA_MASK 0x00007ffffffffff8ULL 52 | # define ISA_MAGIC_MASK 0x001f800000000001ULL 53 | # define ISA_MAGIC_VALUE 0x001d800000000001ULL 54 | struct { 55 | uintptr_t nonpointer : 1; 56 | uintptr_t has_assoc : 1; 57 | uintptr_t has_cxx_dtor : 1; 58 | uintptr_t shiftcls : 44; 59 | uintptr_t magic : 6; 60 | uintptr_t weakly_referenced : 1; 61 | uintptr_t deallocating : 1; 62 | uintptr_t has_sidetable_rc : 1; 63 | uintptr_t extra_rc : 8; 64 | # define RC_ONE (1ULL<<56) 65 | # define RC_HALF (1ULL<<7) 66 | }; 67 | } 68 | ``` 69 | 看这个定义只能大概看出个框架,下面从isa的初始化过程来看看isa_t究竟是如何存储类或者元类的相关信息。 70 | 71 | ``` 72 | inline void 73 | objc_object::initInstanceIsa(Class cls, bool hasCxxDtor) 74 | { 75 | initIsa(cls, true, hasCxxDtor); 76 | } 77 | 78 | inline void 79 | objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 80 | { 81 | if (!nonpointer) { 82 | isa.cls = cls; 83 | } else { 84 | isa_t newisa(0); 85 | newisa.bits = ISA_MAGIC_VALUE; 86 | newisa.has_cxx_dtor = hasCxxDtor; 87 | newisa.shiftcls = (uintptr_t)cls >> 3; 88 | isa = newisa; 89 | } 90 | } 91 | 92 | ``` 93 | 上来就看不懂,nonpointer是个什么,为什么在这里传的是true?在之前那位大神的另一篇文章中也有解释:[Non-pointer isa](http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html)。 94 | 95 | 大概的意思是在64位系统中,为了降低内存使用,提升性能,isa中有一部分字段用来存储其他信息。这也解释了上面isa_t的那部分结构体。 96 | 97 | >这有点像taggedPointer,两者之间有什么区别?备注一下后面再研究。 98 | 99 | 现在知道了nonpointer为什么是true,那么把initIsa方法先简化一下: 100 | 101 | ``` 102 | inline void 103 | objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 104 | { 105 | isa_t newisa(0); 106 | newisa.bits = ISA_MAGIC_VALUE; 107 | newisa.has_cxx_dtor = hasCxxDtor; 108 | newisa.shiftcls = (uintptr_t)cls >> 3; 109 | isa = newisa; 110 | } 111 | # define ISA_MAGIC_VALUE 0x001d800000000001ULL 112 | ``` 113 | 114 | 一共三部分: 115 | 116 | 1. newisa.bits = ISA_MAGIC_VALUE; 117 | 从ISA_MAGIC_VALUE的定义中可以看到这个字段初始化了两个部分,一个是magic字段(6位:111011),一个是nonpointer字段(1位:1),magic字段用于校验,nonpointer之前已经详细分析过了。 118 | 119 | 2. newisa.has_cxx_dtor = hasCxxDtor; 120 | 这个字段存储类是否有c++析构器。 121 | 122 | 3. newisa.shiftcls = (uintptr_t)cls >> 3; 123 | 将cls右移3位存到shiftcls中,从isa_t的结构体中也可以看到低3位都是用来存储其他信息的,既然可以右移三位,那就代表类地址的低三位全部都是0,否则就出错了,补0的作用应该是为了字节对齐。 124 | 125 | 因为nonpointer的缘故,isa并不只是用来存储类地址了,所以需要提供一个额外的方法来返回真正的地址: 126 | 127 | ``` 128 | inline Class 129 | objc_object::ISA() 130 | { 131 | return (Class)(isa.bits & ISA_MASK); 132 | } 133 | 134 | # define ISA_MASK 0x00007ffffffffff8ULL 135 | ``` 136 | 其实就是取isa_t结构体的shiftcls字段。 137 | 138 | ### 其他字段 139 | 还有一些其他的字段,把上面那篇文章中相关部分翻译过来放在下面: 140 | 141 | > // 是否曾经或正在被关联引用,如果没有,可以快速释放内存 142 | uintptr_t has_assoc : 1; 143 | 144 | > // 对象是否曾经或正在被弱引用,如果没有,可以快速释放内存 145 | uintptr_t weakly_referenced : 1; 146 | 147 | > // 对象是否正在释放内存 148 | uintptr_t deallocating : 1; 149 | 150 | >// 对象的引用计数太大,无法存储 151 | uintptr_t has_sidetable_rc : 1; 152 | 153 | >// 对象的引用计数超过1,比如10,则此值为9 154 | uintptr_t extra_rc : 8; 155 | 156 | 资料:[对象、类和isa](https://www.jianshu.com/p/a8eade8a1c6d) 157 | [isa和class](https://www.jianshu.com/p/9d649ce6d0b8) 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /SwiftGuide.md: -------------------------------------------------------------------------------- 1 | Swift是一种支持多编程范式和编译式的编程语言,由苹果与2014年WWDC(苹果开发者大会)发布,至今的4年里不断完善和发展,现如今已大面积普及;4月份,在TIOBE排行榜Swift排名15,完全超越了Objective-C,可见学习和掌握的必要性。学习一门语言,看官方文档应该是最好的方式之一,这样可以有更扎实的基础,当然,这可能会耗时一点,但应该是值得的。 2 | 3 | 本文是阅读官方文档的产物,详细记录了官方文档的知识点可作为开发手册,目前已完善: 4 | "TheBasic",//基础 5 | "BasicOperators",//基本操作 6 | "StringsAndCharacters",//字符和字符串 7 | "CollectionTypes",//集合类型 8 | "ControlFlow",//流程控制 9 | "Functions",//函数 10 | "Closures",//闭包 11 | "Enumerations",//枚举 12 | "ClassesAndStructures",//类和结构体 13 | "Properties",//属性 14 | "Methods",//方法 15 | "Subscripts",//下标 16 | "Inheritance",//继承 17 | "Initialization",//构造过程 18 | "Deinitialization",//析构 19 | "OptionalChaining",//可选调用 20 | "ErrorHandling",//错误处理 21 | "TypeCasting",//类型捕获 22 | "NestedTypes",//嵌套类型 23 | "Extensions",//扩展 24 | "Protocols",//协议 25 | "Generics",//泛型 26 | AutomaticReferenceCounting",//自动引用计数 27 | "MemorySafety",//内存安全 28 | "AccessControl",//访问控制 29 | "AdvancedOperators", 30 | 持续更新,希望能对您有所帮助! 31 | ![效果.gif](https://upload-images.jianshu.io/upload_images/2598795-4ce3d02b79bf5555.gif?imageMogr2/auto-orient/strip) 32 | 33 | [GitHub地址](https://github.com/qingfengiOS/SwiftDemo.git),如果觉得可以希望顺手点个Star. -------------------------------------------------------------------------------- /UITableView代理执行顺序.md: -------------------------------------------------------------------------------- 1 | #iOS不同系统版本tableView代理方法的执行顺序 2 | 今天意外发现在低版本(9.3)的iOS系统上出现崩溃问题,最后定位到的问题是UITableView的代理方法执行顺序导致的,特此用Demo对tableView的代理方法的调用顺序做了测试。 3 | 4 | ###一、前置条件: 5 | tableView有2个section,每个section包含3个row 6 | ###二、结果: 7 | ####iOS9.3系统: 8 | ``` 9 | 641 CoreAnimation系列[55359:1224886] -[ViewController numberOfSectionsInTableView:] 10 | 642 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForHeaderInSection:] 11 | 642 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForHeaderInSection:] 12 | 642 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForFooterInSection:] 13 | 642 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForFooterInSection:] 14 | 642 CoreAnimation系列[55359:1224886] -[ViewController tableView:numberOfRowsInSection:] 15 | 643 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 16 | 643 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 17 | 643 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 18 | 643 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForHeaderInSection:] 19 | 643 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForHeaderInSection:] 20 | 643 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForFooterInSection:] 21 | 643 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForFooterInSection:] 22 | 644 CoreAnimation系列[55359:1224886] -[ViewController tableView:numberOfRowsInSection:] 23 | 644 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 24 | 644 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 25 | 644 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 26 | 645 CoreAnimation系列[55359:1224886] -[ViewController numberOfSectionsInTableView:] 27 | 645 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForHeaderInSection:] 28 | 645 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForHeaderInSection:] 29 | 645 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForFooterInSection:] 30 | 645 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForFooterInSection:] 31 | 646 CoreAnimation系列[55359:1224886] -[ViewController tableView:numberOfRowsInSection:] 32 | 646 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 33 | 646 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 34 | 646 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 35 | 646 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForHeaderInSection:] 36 | 646 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForHeaderInSection:] 37 | 646 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForFooterInSection:] 38 | 646 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForFooterInSection:] 39 | 647 CoreAnimation系列[55359:1224886] -[ViewController tableView:numberOfRowsInSection:] 40 | 647 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 41 | 647 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 42 | 647 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 43 | 651 CoreAnimation系列[55359:1224886] -[ViewController numberOfSectionsInTableView:] 44 | 651 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForHeaderInSection:] 45 | 651 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForHeaderInSection:] 46 | 651 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForFooterInSection:] 47 | 651 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForFooterInSection:] 48 | 651 CoreAnimation系列[55359:1224886] -[ViewController tableView:numberOfRowsInSection:] 49 | 651 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 50 | 651 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 51 | 651 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 52 | 651 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForHeaderInSection:] 53 | 652 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForHeaderInSection:] 54 | 652 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForFooterInSection:] 55 | 652 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForFooterInSection:] 56 | 652 CoreAnimation系列[55359:1224886] -[ViewController tableView:numberOfRowsInSection:] 57 | 652 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 58 | 652 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 59 | 652 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 60 | 653 CoreAnimation系列[55359:1224886] -[ViewController tableView:cellForRowAtIndexPath:] 61 | 653 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 62 | 654 CoreAnimation系列[55359:1224886] -[ViewController tableView:cellForRowAtIndexPath:] 63 | 654 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 64 | 654 CoreAnimation系列[55359:1224886] -[ViewController tableView:cellForRowAtIndexPath:] 65 | 654 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 66 | 655 CoreAnimation系列[55359:1224886] -[ViewController tableView:cellForRowAtIndexPath:] 67 | 655 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 68 | 655 CoreAnimation系列[55359:1224886] -[ViewController tableView:cellForRowAtIndexPath:] 69 | 655 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 70 | 655 CoreAnimation系列[55359:1224886] -[ViewController tableView:cellForRowAtIndexPath:] 71 | 656 CoreAnimation系列[55359:1224886] -[ViewController tableView:heightForRowAtIndexPath:] 72 | 656 CoreAnimation系列[55359:1224886] -[ViewController tableView:viewForHeaderInSection:] 73 | 656 CoreAnimation系列[55359:1224886] -[ViewController tableView:viewForFooterInSection:] 74 | 656 CoreAnimation系列[55359:1224886] -[ViewController tableView:viewForHeaderInSection:] 75 | 656 CoreAnimation系列[55359:1224886] -[ViewController tableView:viewForFooterInSection:] 76 | 77 | ``` 78 | 可以看到在iOS9.3系统上: 79 | #####1)、轮回执行 80 | 1、执行一次```numberOfSectionsInTableView```; 81 | 2、依次执行```heightForHeaderInSection ```和```heightForFooterInSection ```各两次; 82 | 3、执行一次```numberOfRowsInSection```; 83 | 4、执行三次```heightForRowAtIndexPath```; 84 | 5、执行```heightForHeaderInSection ```和```heightForFooterInSection ```各两次; 85 | 6、执行一次```numberOfRowsInSection ```; 86 | 7、执行三次```heightForRowAtIndexPath```; 87 | #####2)、组合执行 88 | 以上7步为一个轮回;执行三个轮回之后, 89 | 8、执行```cellForRowAtIndexPath```和```heightForRowAtIndexPath```两个方法一起共6次; 90 | 9、最后```viewForHeaderInSection```和```viewForFooterInSection ```一起两次 91 | ####iOS11.3: 92 | ``` 93 | .827590+0800 CoreAnimation系列[54215:1200550] -[ViewController numberOfSectionsInTableView:] 94 | .827974+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:numberOfRowsInSection:] 95 | .828114+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:numberOfRowsInSection:] 96 | .828288+0800 CoreAnimation系列[54215:1200550] -[ViewController numberOfSectionsInTableView:] 97 | .828375+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:numberOfRowsInSection:] 98 | .828476+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:numberOfRowsInSection:] 99 | .830575+0800 CoreAnimation系列[54215:1200550] -[ViewController numberOfSectionsInTableView:] 100 | .830683+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:numberOfRowsInSection:] 101 | .830762+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:numberOfRowsInSection:] 102 | .831455+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:cellForRowAtIndexPath:] 103 | .831948+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForRowAtIndexPath:] 104 | .832127+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForFooterInSection:] 105 | .836103+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:cellForRowAtIndexPath:] 106 | .836396+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForRowAtIndexPath:] 107 | .836750+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:cellForRowAtIndexPath:] 108 | .836994+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForRowAtIndexPath:] 109 | .837325+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:cellForRowAtIndexPath:] 110 | .837518+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForRowAtIndexPath:] 111 | .837595+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForFooterInSection:] 112 | .837889+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:cellForRowAtIndexPath:] 113 | .838063+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForRowAtIndexPath:] 114 | .838374+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:cellForRowAtIndexPath:] 115 | .838609+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForRowAtIndexPath:] 116 | .838796+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForHeaderInSection:] 117 | .838888+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:viewForHeaderInSection:] 118 | .839054+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:viewForFooterInSection:] 119 | .839197+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForHeaderInSection:] 120 | .839270+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:viewForHeaderInSection:] 121 | .839377+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:viewForFooterInSection:] 122 | .839478+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForRowAtIndexPath:] 123 | .839566+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForRowAtIndexPath:] 124 | .839638+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForRowAtIndexPath:] 125 | .839720+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForRowAtIndexPath:] 126 | .839797+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForRowAtIndexPath:] 127 | .839871+0800 CoreAnimation系列[54215:1200550] -[ViewController tableView:heightForRowAtIndexPath:] 128 | ``` 129 | 在iOS11.3系统上: 130 | 1、执行一次```numberOfSectionsInTableView```+两次```numberOfRowsInSection ```,一起轮回三次 131 | 2、执行```cellForRowAtIndexPath``` + ```heightForRowAtIndexPat``` + ```heightForFooterInSection```各一次 132 | 3、执行```cellForRowAtIndexPath``` + ```heightForRowAtIndexPat```各一次,轮回三次 133 | 4、执行一次```heightForFooterInSection``` 134 | 5、执行```cellForRowAtIndexPath``` + ```heightForRowAtIndexPat```各一次,轮回两次 135 | 6、执行```heightForHeaderInSection ```+```viewForHeaderInSection ```+```viewForFooterInSection```各一次,轮回两次 136 | 7、执行```heightForRowAtIndexPath```六次 137 | ###三、结论: 138 | iOS11.0之后对tableview的数据源调用有了很大改变,改变了调用顺序,所以如果你出现在不同版本的iOS系统上出现数据源问题的话,可以考虑代理方法的调用顺序,从而查找原因。 139 | -------------------------------------------------------------------------------- /iOS 一个轻量级的组件化思路 .md: -------------------------------------------------------------------------------- 1 | # iOS 一个轻量级的组件化思路 2 | ## 前言 3 | 说起组件化大家应该都不陌生,不过也再提一下,由于业务的复杂度扩展,各个模块之间的耦合度越来越高,不但造成了“牵一发动全身”的尴尬境地,还增加了测试的重复工程,此时,组件化就值得考虑了。组件化就是将APP拆分成各个组件(或者说模块),同时解除这些组件之间的耦合,然后通过路由中间件将项目所需要的组件结合起来。这样做的好处有: 4 | 5 | - 解耦合,增强可移植性,不用再自身业务模块中大量引入其他业务的头文件。 6 | - 提高复用性,如果其他项目中有类似的功能,直接将模块引入稍作修改就能使用了。 7 | - 减少测试成本,当修改或者迭代某个小组件的过程中就不用进行大规模的回归测试。 8 | 9 | 网上关于组件化的方案不少,流传最广的是[蘑菇街组件化的技术方案](https://limboy.me/tech/2016/03/10/mgj-components.html)和[iOS应用架构谈 组件化方案](https://casatwy.com/iOS-Modulization.html)这里不对大佬们的方案妄加评价,感兴趣的同学可以自己看看。这里我们聊聊另外的一种方式[Protocol-Moudle](https://github.com/qingfengiOS/QFMoudle) 10 | 11 | ## 思路 12 | 在iOS中,[协议(Protocol)](https://juejin.im/post/5bda59da5188257f8f1e1a02)**定义了一个纲领性的接口,所有类都可以选择实现。它主要是用来定义一套对象之间的通信规则。protocol也是我们设计时常用的一个东西,相对于直接继承的方式,protocol则偏向于组合模式。他使得两个毫不相关的类能够相互通信,从而实现特定的目标。** 13 | 14 | 在之前的一篇文章[ResponderChain+Strategy+MVVM实现一个优雅的TableView](https://juejin.im/post/5bd6c734e51d45410c10eb54)中我们用到了protocol来为View提供公共的方法: 15 | 16 | ```- (void)configCellDateByModel:(id)model;``` 17 | 18 | 为Model提供公共的方法: 19 | 20 | ``` 21 | - (NSString *)identifier; 22 | - (CGFloat)height; 23 | ``` 24 | 那么我们也可以以此来构建一个轻量级的路由中间件,定义一套各个组件的通信规则,各自管理和维护各自的模块,对外提供必要的接口。 25 | 26 | ## 实践 27 | 首先看一下这个Demo的结构图和运行效果 28 | ![结构](https://user-gold-cdn.xitu.io/2018/11/8/166f372c2073a148?w=540&h=1270&f=png&s=211387) 29 | 30 | 31 | 32 | ### 路由 33 | 好了我们看看路由的一些细节,它只需要提供两个关键的东西: 34 | 35 | 1. 提供路由器单例 36 | 2. 获取对应的Moudle 37 | - 通过Protocol获取 38 | - 通过URL获取 39 | 40 | 首先提供单例: 41 | 42 | ``` 43 | + (instancetype)router { 44 | 45 | static dispatch_once_t onceToken; 46 | dispatch_once(&onceToken, ^{ 47 | _router = [[self alloc]init]; 48 | }); 49 | return _router; 50 | } 51 | ``` 52 | 这样的单例可能没有人不会写,but,这其实这仅仅是个“假的”单例,不信你可以使用```[QFRouter router]``` 和```[[QFRouter alloc]init]```以及```[router copy]```试试他们会不会生成三个内存地址不同的实例。可能你会说,谁会无聊这么干?但是如果设计的时候能更严谨的规避这种坑不会更好么。 那么怎么才能才能做一个严谨的单例呢?可以重写他的```alloc```和```copy```方法避免创建多个实例,你可以在[Demo工程](https://github.com/qingfengiOS/QFMoudle)中看到细节,这里不做展开。 53 | 54 | 回归正题,我们看看如何获取Module 55 | 56 | 通过Runtime的反射机制,我们可以通过NSString获取一个class进而创建对应的对象,而Protocol又可以得到一个NSString,那么是否可以由此入手呢?答案是可以的: 57 | 58 | ``` 59 | - (Class)classForProtocol:(Protocol *)protocol { 60 | NSString *classString = NSStringFromProtocol(protocol); 61 | return NSClassFromString(classString); 62 | } 63 | ``` 64 | 这里传入一个protocol即可获取对应的Module的class,再通过class即可以得到对应的Module的object。 65 | 66 | 通过Protocol或者URL获取对应的Module: 67 | 68 | ``` 69 | #pragma mark - Public 70 | - (id)interfaceForProtocol:(Protocol *)protocol { 71 | Class class = [self classForProtocol:protocol]; 72 | return [[class alloc]init]; 73 | } 74 | 75 | - (id)interfaceForURL:(NSURL *)url { 76 | id result = [self interfaceForProtocol:objc_getProtocol(url.scheme.UTF8String)]; 77 | NSURLComponents *cp = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; 78 | [cp.queryItems enumerateObjectsUsingBlock:^(NSURLQueryItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 79 | [result setValue:obj.value forKey:obj.name];//KVC设置 80 | }]; 81 | return result; 82 | } 83 | 84 | ``` 85 | 这里有个瑕疵就是调用方(外部组件)需要知道这个目标组件对外暴露的协议名称,不过既然是协议,对外公开应该不算大问题吧,调用实例如下: 86 | wayOne: 87 | 88 | ``` 89 | id homeMoudle = [[QFRouter router]interfaceForProtocol:@protocol(MoudleHome)]; 90 | ``` 91 | 这样就拿到了对应的目标组件实例,通过对外暴露的属性可以对其进行传值,通过其回调block则可以拿到回调参数。 92 | 93 | wayTwo: 94 | 95 | ``` 96 | id meMoudle = [[QFRouter router]interfaceForURL:[NSURL URLWithString:@"MoudleMe://?paramterForMe=ModuleMe"]]; 97 | ``` 98 | 这里通过url传入,通过KVC设置其属性值。同样地,通过其回调block则可以拿到回调参数。 99 | 100 | ### 公共协议 101 | 通过上面我们了解到:通过Protocol可以获取对应的组件实例,那么这个协议放在哪儿?如何管理呢? 102 | 在日常开发过程中,跨组件的交互场景最多的应该就是:从组件A附带参数跳转到组件B的某个页面,组件B的这个页面中做一些操作,再回到组件A(可能有回调参数,也可能不回调参数),那么我们的协议应该能处理这两个最常见和基础的操作,所以给protocol定义了两个属性: 103 | 104 | ``` 105 | typedef void(^QFCallBackBlock)(id parameter); 106 | 107 | #pragma mark - 基础协议 108 | @protocol QFMoudleProtocol 109 | 110 | /// 暴露给组件外部的控制器,一般为该组件的主控制器 111 | @property (nonatomic, weak) UIViewController *interfaceViewController; 112 | /// 回调参数 113 | @property (nonatomic, copy) QFCallBackBlock callbackBlock; 114 | 115 | @end 116 | ``` 117 | 118 | >这里的interfaceViewController为何声明成了weak属性?这个问题先留一下,后面会聊到这一点。 119 | 120 | 有了这里的两个属性我们即可完成,对应的跳转和参数回调,但是如何正向传值呢? 121 | 122 | 应该还需要对应的属性来做入参,但是组件何其多,入参何其多,如果都把正向的属性写入这里面,那么随着时间和业务的增长,这个协议可能会十分杂乱和臃肿。 123 | 124 | 所以这里把这个协议定为基础协议,对应的组件都继承自它,然后定义各自的需要的入参属性: 125 | 126 | 首页组件: 127 | 128 | ``` 129 | #pragma mark - ”首页“组件 130 | @protocol MoudleHome 131 | 132 | /// 组件“Home”首页所需要的参数 133 | @property (nonatomic, copy) NSString *paramterForHome; 134 | 135 | /// 组件“Home”中详情页面所需要的参数 136 | @property (nonatomic, copy) NSString *titleString; 137 | 138 | /// 组件“Home”中详情页面所需要的参数 139 | @property (nonatomic, copy) NSString *descString; 140 | 141 | /// 组件“Home”所需要暴露的特殊接口,比如其他组件也要跳转到该页面 142 | @property (nonatomic, weak) UIViewController *detailViewController; 143 | 144 | @end 145 | 146 | ``` 147 | 可以看到,由于首页组件需要对外暴露一个主页面 ```QFHomeViewController``` 和详细页面 ``QFDetailViewController``所以参数会多一点。 148 | 149 | 我的组件: 150 | 151 | ``` 152 | #pragma mark - “我的”组件 153 | @protocol MoudleMe 154 | 155 | /// 组件“Me”所需要的参数 156 | @property (nonatomic, copy) NSString *paramterForMe; 157 | 158 | @end 159 | ``` 160 | 而“我的”组件,只对外提供一个```QFMeViewController```页面,参数比较简单。 161 | 162 | 这样,基本算是达成了对协议的处理,但是无可避免的问题就是: 这个公共协议中定义了各个组件的协议,所以需要对多个开发团队可见,感觉这也是组件化过程中的一个普遍问题,目前没找到好的解决方式。 163 | 164 | 165 | ### Module 166 | 上面我们说到了公开protocol中定义了一些属性,比如```interfaceViewController``` 那么这些属性由谁提供呢?没错,就是Module,通过上面的步骤我们可以获取到对应的Module实例,但是我们跳转需要的是Controller,所以,在此时就需要Module的帮助了, Module通过公共协议定义的属性为外部提供Controller接口: 167 | 168 | ``` 169 | - (UIViewController *)interfaceViewController { 170 | QFHomeViewController *homeViewController = [[QFHomeViewController alloc]init]; 171 | homeViewController.interface = self; 172 | interfaceViewController = (UIViewController *)homeViewController; 173 | return interfaceViewController; 174 | } 175 | ``` 176 | 因为Module是在对应的组件中,所以可以随意引用自己组件内部的头文件完成初始化, 177 | 而对应的控制器中,需要组件外部的参数,所以这里把Module实例也暴露给对应的控制器实例,也就是```homeViewController.interface = self;```所做的事情。 178 | 179 | >在上面说协议的时候我们提到为什么要使用weak,至此,应该比较明朗了 ———— 打破循环强引用。 180 | 181 | 通过公共协议解耦获取到Module,Module完成为组件内和组件外的搭桥铺路工作,由此,使得跨组件传值、调用、参数回调得以实现。更多细节请看[QFMoudle](https://github.com/qingfengiOS/QFMoudle.git)。 182 | 183 | 由于时间关系,如何制作私有库就不再赘述了,有需要欢迎留言,我们一起手把手创建一个属于你自己的pod私有库。 184 | 185 | ## 后记 186 | 在这种组件化的实践中,一般会把对应的组件、路由以及公共协议做成pod私有库,而需要多个团队知道的也就只有这个公共协议库,把所有的协议放入这个公开协议库中,每次升级也只需要更新这个库就好了。 187 | 188 | 关于对组件化的态度:上面说了那么多好处,下面说说弊端(亲身体会) 189 | 190 | 如果团队规模较小业务复杂度较低的话,不太建议做私有库,原因: 191 | 192 | 1. 它的 修改 -> 升级 -> 集成 -> 测试是一个比较耗时的过程,可能一点小小的改动(比如改个颜色,换个背景)需要耗费成倍的时间。 193 | 2. 增加项目的调用复杂度,增加新成员的熟悉成本。 194 | 195 | 任何事情都是双面的,有利有弊,望各位看官自行取舍。最后由于笔者文笔有限,若给您造成了困扰,实在抱歉,有任何疑问or意见or建议,欢迎交流讨论,当然,如果对您有用,希望顺手给个Star,点个关注,您的支持是我最大的动力! 196 | 197 | -------------------------------------------------------------------------------- /iOS事件处理.md: -------------------------------------------------------------------------------- 1 | ## 一、起始阶段 2 | 1、cpu处于睡眠状态,等待事件的发生 3 | 2、手指触摸屏幕 4 | ## 二、系统响应阶段 5 | 1、屏幕硬件感应到事件,并且将感应到的事件传递给输入输出驱动IOKit 6 | 2、IOKit.framework封装整个触摸事件为IOHIDEvent对象 7 | 3、IOKit.framework通过IPC将事件转发给SpringBoard.app 8 | 9 | 此阶段是系统层的响应,系统感应到外界的输入,并将响应的输入封装成比较概括的IOHITEvent对象,然后UIKit通过IOHITEvent的类型,判断出相应事件应该由SpringBoard.app处理,通过mach port(IPC进程间通信)转发给SpringBoard.app。 10 | 11 | SpringBoard.app就是iOS的系统桌面,当触摸事件发生时,只有负责管桌面的SpringBoard.app才知道如何正确地相应。因为触摸发生时,有可能用户正在桌面翻页找App,也有可能正处于在微信中刷朋友圈。 12 | ## 三、桌面响应阶段 13 | 1、SpringBoard.app主线程Runloop收到IOKit.framework转发来的消息苏醒,并触发对应mach port的Source1 回调__IOHIDEvebtSystemClientQueueCallback() 14 | 2、如果SpringBoard.app监测到有APP(记录为:xxxxx.app)在前台活动,SpringBoard.app通过mach port(IPC 进程间通信)转发给xxxxx.app,如果SpringBoard.app监测到无前台APP,则SpringBoard.app进入APP内部相应的第二阶段,触发source0回调 15 | ## 四、内部响应阶段 16 | 1、前台app主线程Runloop收到SpringBoard.app转发来的消息苏醒,触发mach port的source1回调__IOHIDEnventSystemClientQueueCallback()。 17 | 2、soure1回调内部触发source0,回调_UIApplicationHandleEventQueue() 18 | 3、source0回调内部,封装IOHIDEvent为UIEvent 19 | 4、source0回调内部,调用UIApplication的sendEvent:方法,将方法传递给UIWindow 20 | 5、通过递归调用UIView层的hitTest(:with:),结合point(inside:with)找到UIEvent中每一个UITouch所属的UIView(其实是想找到离触摸事件点最近的那个UIView)这个过程是从UIView层级的最顶层往最底层递归查询,但这不是UIResponder响应链,事件响应是在UIEvent中每一个UITouch所属的UIView都确定之后方才开始。 21 | 22 | ## 五、响应过程 23 | ### 寻找最佳响应者 24 | 这一过程主要来确定由哪个视图来首先处理 UITouch 事件。当你点击一个 view,事件传到 UIWindow 这一步之后,会去遍历 view 层级,直至找到那个合适的 view 来处理这个事件,这一过程也叫做 Hit-Testing。 25 | 26 | 系统会根据添加 view 的前后顺序,确定 view 在 subviews 数组中的顺序。然后根据这个顺序将视图层级转化为图层树,针对这个树,使用倒着进行前序深度遍历的算法,进行遍历。 27 | 28 | ### [hitTest:withEvent:] 方法实现原理 29 | UIWindow 拿到事件之后,会先将事件传递给图层树中距离最靠近 UIWindow 那一层最后一个 view,然后调用其 [hitTest:withEvent:]。注意这里是**先将视图传递给 view,再调用其 [hitTest:withEvent:] 方法。并遵循这样的原则: 30 | 31 | - 如果点不在这个视图内,则去遍历其他视图。 32 | - 如果点击在这个视图内,但是其还有自视图,那么将事件传递给自视图,并且调用自视图的 [hitTest:withEvent:]. 33 | - 如果点击在这个视图内,并且这个视图没有子视图,那么 return self,即它就是那个最合适的视图。 34 | - 如果点击在这个视图内,并且这个视图没有子视图,但是不想作为处理事件的 view,可以 return nil,事件由父视图处理。 35 | 36 | 但需要注意,以下三种情况UIView以及其子View的hitTest(_:with:)不会被调用,而之后响应事件是下向上传递的,这直接导致以下三种情况的UIView及其子UIView不接收任何触摸事件: 37 | 38 | userInteractionEnabled = NO 39 | hidden = YES 40 | alpha = 0.0~0.01之间 41 | >UIImageView的userInteractionEnabled默认为NO,因此UIImageView以及它的子控件默认是不接收触摸事件的。 42 | 43 | 44 | ### 事件响应链 45 | touch 事件处理的传递过程与 Hit-Testing 过程正好相反。Hit-Tesing 过程是从上向下(从父视图到子视图)遍历;touch 事件处理传递是从下向上(从子视图到父视图)传递。这也就是传说中的 Response Chain。最有机会处理事件的对象就是通过 Hit-Testing 找到的视图或者第一响应者,如果两者都能处理,则传递给下一个响应者,之后依次传递。 46 | 47 | 总流程图: 48 | ![uitouchflow.png](https://upload-images.jianshu.io/upload_images/2598795-382dd4e18c4251ac.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 49 | [更多请看](http://qingmo.me/2017/03/04/FlowOfUITouch/) -------------------------------------------------------------------------------- /iOS仿滴滴预约用车时间选择器.md: -------------------------------------------------------------------------------- 1 | # iOS仿滴滴预约用车时间选择器 2 | 3 | ## 从需求说起 4 | 前几天接到一个版本,里面包含了一个和滴滴预约用车选择时间的picker一样,需要选择当前时间的后面几天内的时间,包含了日期,小时和分钟数,分钟数的间隔是以10分钟为单位,如下图所示: 5 | ![timerPicker](https://user-gold-cdn.xitu.io/2018/11/23/1673f5bbf8c1817a?w=856&h=528&f=png&s=92805) 6 | 7 | 当接到这个需求时,我的心里是有点小拒绝的,看着就是一个pickerView但是里面东西还是有的东西的,包含: 8 | 9 | 1. 时间数据源获取,获取当前时间到3天后。 10 | 2. 自定义时间数据源,分钟时间刻度单位为10分钟,不足10分钟的向上取整。 11 | 3. 选择当天对当前小时数据和分钟数据的处理。 12 | 4. 选择当前小时情况下对分钟数据源的处理。 13 | 5. pickerView自定义展示(颜色,字体大小) 14 | 15 | 个人认为,能自己做的尽量都少用三方库,减少对三方库的依赖,(PS:目前项目用了百度地图,iOS12删除了百度SDK用到的系统库,各种麻烦),所以决定自己造一个轮子。 16 | 17 | ## 过程 18 | #### 获取天数 19 | 这里采用NSDate的```dateWithTimeIntervalSinceNow```函数再转成字符串,值得一提的是```NSDateFormatter```,根据[官方文档](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/DataFormatting/Articles/dfDateFormatting10_4.html)的描述: 20 | >Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, it is typically more efficient to cache a single instance than to create and dispose of multiple instances. One approach is to use a static variable. 21 | 22 | 创建一个formatter实例的代价是比较高,频繁使用时要考虑缓存,个人的做法是: 23 | 24 | ``` 25 | + (void)load { 26 | if (!_dateFormatter) { 27 | _dateFormatter = [[NSDateFormatter alloc] init]; 28 | [_dateFormatter setDateFormat:@"YYYY-MM-dd HH:mm"]; 29 | } 30 | } 31 | ``` 32 | 保证只创建一个NSDateFormatter实例,关于```+load```不做多说,想了解更多的可以看看之前的[Runtime源码 +load 和 +initialize](https://juejin.im/post/5bda5a02e51d45685f4435e6) 33 | 34 | ``` 35 | for (NSInteger i = 0; i < kDays; i++) { 36 | NSString *dateString = [self distanceDate:beginTime aDay:i];//获取第i天的日期 37 | NSString *week = [self currentWeek:dateString type:NO];//获取星期几 38 | } 39 | ``` 40 | 这里用到了 41 | ``` 42 | static NSInteger const kDays = 3;//从今天起能选择多少天 默认3天 43 | ``` 44 | 因为#define在编译的预处理阶段有一个宏替换操作,大量地使用#define会拖慢编译速度,而且宏没有类型,不做任何类型检查。Apple官方也是使用了更多的const。 45 | 46 | ### 分钟数向上取整 47 | 需求是当分钟数不为整10分钟时,向上取整,比如,16->20,41->50,所以对初始的数据源还有一步向上取整的操作: 48 | 49 | ``` 50 | NSString *beginTime = [self getTimerAfterCurrentTime:kBenginTimeDely];//开始时间(也就是当前时间20分钟后) 51 | NSInteger currentMin = [self getMString:beginTime]; 52 | if (currentMin % kTimeInterval != 0) { 53 | beginTime = [self getTimerAfterTime:beginTime periodMin:(kTimeInterval - currentMin % kTimeInterval)];//开始时间向上取整 54 | } 55 | ``` 56 | 这里把这三个数据抽取出来,提高灵活性,比如天数要5天之后or时间间隔要改成5分钟or最早时间是30分钟后这里只需要修改对应常量即可。 57 | 58 | ### 选中数据的处理 59 | 第一次的做法是在```- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component```中切换日期或者小时的时候重新计算数据源,但是发现这样的效果并不好,有明显的卡顿现象,想起来这样的真的是很愚蠢的办法。应该初始化的时候计算好数据源,而不是每次都重新计算。 60 | 61 | 在切换日期或者小时数的时候切换数据源,具体实现: 62 | 63 | ``` 64 | - (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component { 65 | if (component == 0) { 66 | return self.dataSourceModel.dateArray.count; 67 | } else if (component == 1) { 68 | if (self.selectedDateIndex == 0) {//选中的今天 69 | return self.dataSourceModel.todayHourArray.count; 70 | } else { 71 | return self.dataSourceModel.hourArray.count; 72 | } 73 | } else { 74 | if (self.selectedHourIndex == 0 && self.selectedDateIndex == 0) {//选中的当天的第一个小时 75 | return self.dataSourceModel.todayMinuteArray.count; 76 | } else { 77 | return self.dataSourceModel.minuteArray.count; 78 | } 79 | } 80 | } 81 | ``` 82 | 关于数据源的计算,比较直观,这里就不贴出来了,详情请看[QFDatePickerView](https://github.com/qingfengiOS/QFDatePickerView)中```QFTimerUtil```文件```+ (QFTimerDataSourceModel *)configDataSource```方法 83 | 84 | ### pickerView自定义展示 85 | 可以直接通过默认的代理方法```- (nullable NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component __TVOS_PROHIBITED```实现日期显示,但是这样的展示效果却和设计图差距较大,所以实现了```- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UILabel *)recycledLabel ```自定义展示: 86 | 87 | ``` 88 | - (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UILabel *)recycledLabel { 89 | if (!recycledLabel) { 90 | recycledLabel = [[UILabel alloc] init]; 91 | } 92 | recycledLabel.textAlignment = NSTextAlignmentCenter; 93 | [recycledLabel setFont:[UIFont systemFontOfSize:18]]; 94 | recycledLabel.textColor = [UIColor colorWithRed:34.0f / 255.0f green:34.0f / 255.0f blue:34.0f / 255.0f alpha:1.0f]; 95 | ... 96 | recycledLabel.text = minModel.showMinuteString; 97 | return recycledLabel; 98 | } 99 | ``` 100 | 101 | ## 使用方式 102 | 手动拖入文件夹 或者 ```pod 'QFDatePicker'``` 103 | 导入QFTimerPicker头文件,在对应的地方调用picker的初始化方法和show方法: 104 | 105 | ``` 106 | /** 107 | 初始化时间选择 108 | 109 | @param block 回调block 参数即是选择的日期 110 | @return 时间选择器实例 111 | */ 112 | - (instancetype)initWithResponse:(ReturnBlock)block; 113 | 114 | /** 115 | 初始化时间选择 116 | 117 | @param superView 时间选择器的父View,若为空,将时间选择器加载在window上面 118 | @param block 回调block 参数即是选择的日期 119 | @return 时间选择器实例 120 | */ 121 | - (instancetype)initWithSuperView:(UIView *)superView response:(ReturnBlock)block; 122 | ``` 123 | 注释比较清楚了,通过```superView```参数,控制这个picker加载在什么视图上,当其为空的时候加载在window上。 124 | 125 | 选中的时间再block中回调(PS:这里如果把picker设置为属性时,考虑循环强引用的问题) 126 | 127 | 具体调用案例: 128 | 129 | ``` 130 | QFTimerPicker *picker = [[QFTimerPicker alloc]initWithSuperView:self.view response:^(NSString *selectedStr) { 131 | NSLog(@"%@",selectedStr); 132 | [sender setTitle:selectedStr forState:UIControlStateNormal]; 133 | }]; 134 | [picker show]; 135 | ``` 136 | 137 | ## 总结 138 | 1. 数据源的预加载处理 139 | 2. 对define和const取舍 140 | 3. NSDateFormatter的缓存 141 | 4. 日期类的计算,比如获取当前时间,计算星期几等 142 | 143 | [演示Demo](https://github.com/qingfengiOS/QFDatePickerView) 144 | [cocoaPods安装](https://github.com/qingfengiOS/QFDatePicker) 145 | -------------------------------------------------------------------------------- /iOS各种时间选择Picker.md: -------------------------------------------------------------------------------- 1 | # iOS各种时间选择Picker 2 | ## 前言 3 | 在日常开发过程中,时间选择器的使用场景应该是比较高的,而且各个场景的具体需求也各式各样,比如一些场景中,只需要选择年月,有的需要包含“至今”,有的选择时间,有的需要选择年月日和时分,然而这些都需要自定义数据源,这里把自己遇到过的类型做了一个总结和记录。 4 | 5 | ## 年份选择 6 | 数据源1970至今,最后根据具体需求判断是否需要添加"至今"数据源,效果如下: 7 | 8 | ![year](https://upload-images.jianshu.io/upload_images/2598795-e3ecd95c3700ab82.png) 9 | 10 | ## 年和月份选择 11 | 年份数据源1970至今,月份为当年的最大月份,当切换到其他年份时,月份数据源变更为1~12月,当选择到“至今”时,月份数据为空,效果如下: 12 | 13 | ![year&month](https://upload-images.jianshu.io/upload_images/2598795-496f71b93cbd8ad2.png) 14 | 15 | ## 时和分选择 16 | 这里的起止时间节点根据初始化时传入的数据进行配置,分钟的间隔数也由调用者动态配置 17 | ![hour&min](https://upload-images.jianshu.io/upload_images/2598795-207f1ba8fdd35d41.png) 18 | 19 | ## 仿滴滴时间选择 20 | 这个picker是这几种类型中相对最复杂的一种,牵涉到数据源的完全计算,NSdate和NSString的相互转换,pickView样式的高度自定义。之前已经写过一篇,这里就不做copy了,详情请看:[iOS仿滴滴预约用车时间选择器](https://juejin.im/post/5bf7a4ce51882550d05ca29a) 21 | 22 | ## 使用方式 23 | 各个picker的数据源,基本没有难点,有兴趣的可自行查看[源码](https://github.com/qingfengiOS/QFDatePickerView.git)。 24 | 25 | 稍微说说使用方式,下载源码,拖入工程,或者直接使用cocoapods:pod 'QFDatePicker' 26 | 27 | 调用对应的初始化方法(init...)和show方法,以年份为例: 28 | 29 | ``` 30 | QFDatePickerView *datePickerView = [[QFDatePickerView alloc]initYearPickerWithView:self.view response:^(NSString *str) { 31 | NSString *string = str; 32 | NSLog(@"str = %@",string); 33 | }]; 34 | 35 | [datePickerView show]; 36 | ``` 37 | 选中的时间在block中处理,其他调用类似,相信睿智的您一眼就能看明白。 38 | 这篇主要是对一年前的文章做一次整理(之前的确实有点辣眼睛),希望看到的朋友轻喷。 39 | 40 | [演示Demo](https://github.com/qingfengiOS/QFDatePickerView) 41 | [cocoaPods安装](https://github.com/qingfengiOS/QFDatePicker) 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /iOS启动优化.md: -------------------------------------------------------------------------------- 1 | #启动优化 2 | ##pre-main阶段 3 | exec() 是一个系统调用。系统内核把应用映射到新的地址空间,且每次起始位置都是随机的(因为使用 ASLR)。并将起始位置到 0x000000 这段范围的进程权限都标记为不可读写不可执行。如果是 32 位进程,这个范围至少是 4KB;对于 64 位进程则至少是 4GB。NULL 指针引用和指针截断误差都是会被它捕获。 4 | 5 | ###1.Load dylibs 6 | 这一阶段dylib会分析应用依赖的dylib,找到mach-o文件,打开和读取这些文件并验证有效性,接着会找到代码签名注册到内核,最后都对dylib的每一个segment调用mmap()。一般情况下,iOS应用会加载100-400个dylibs,其中大部分是系统库,这部分dylib的加载系统已经做了优化。 7 | 8 | 所以,依赖的dylib越少越好。在这一步,我们可以做的优化有: 9 | 10 | 1.尽量不使用内嵌(embedded)的dylib,加载内嵌dylib性能开销较大 11 | 2.合并已有的dylib和使用静态库(static archives),减少dylib的使用个数 12 | 3.懒加载dylib,但是要注意dlopen()可能造成一些问题,且实际上懒加载做的工作会更多 13 | ###2.Rebase/Bind 14 | 在dylib的加载过程中,为了安全考虑一如了ASLR(Address Space Layout Randomization)技术和代码签名。由于ASLR的原因,镜像(Image,包括可执行文件、dylib和bundle)会在随机的地址上加载dyld需要修正这个偏差,来指向正确的地址。 15 | Rebase在前,Bind在后,Rebase做的是将镜像读入内存,修正镜像内部的指针,性能消耗主要在IO。Bind做的是查询符号表,设置指向镜像外部的指针,性能消耗主要在CPU计算。 16 | 17 | 所以,指针数量越少越好。在这一步,我们可以做的优化有: 18 | 19 | 1.减少ObjC类(class)、方法(selector)、分类(category)的数量 20 | 2.减少C++虚函数的的数量(创建虚函数表有开销) 21 | 3.使用Swift structs(内部做了优化,符号数量更少) 22 | ###3.ObjC setup 23 | 大部分ObjC初始化工作已经在Rebase/Bind阶段做完了,这一步dyld会注册所有声明过的ObjC类,将分类插入到类的方法列表里,再检查每个selector的唯一性。 24 | 25 | 在这一步倒没什么优化可做的,Rebase/Bind阶段优化好了,这一步的耗时也会减少。 26 | ###4.initializers 27 | 到了这一阶段,dyld开始运行程序的初始化函数,调用每个ObjC类和分类的load方法,调用C/C++中的构造函数和创建非基本类型的C++静态全局变量。initializers阶段执行完后,dyld开始调用manin()函数 28 | 29 | 在这一步,我们可以做的优化有: 30 | 31 | 1.少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize 32 | 2.减少构造器函数个数,在构造器函数里少做些事情 33 | 3.减少C++静态全局变量的个数 34 | ##main阶段 35 | 这一阶段的优化主要是减少didFinishLaunchingWithOptions方法里的工作,在didFinishLaunchingWithOptions方法里,我们会创建应用的window,指定其rootViewController,调用window的makeKeyAndVisible方法让其可见。由于业务需要,我们会初始化各个二方/三方库,设置系统UI风格,检查是否需要显示引导页、是否需要登录、是否有新版本等,由于历史原因,这里的代码容易变得比较庞大,启动耗时难以控制。 36 | 37 | 所以,满足业务需要的前提下,didFinishLaunchingWithOptions在主线程里做的事情越少越好。在这一步,我们可以做的优化有: 38 | 39 | 1.梳理各个二方/三方库,找到可以延迟加载的库,做延迟加载处理,比如放到首页控制器的viewDidAppear方法里。 40 | 2.梳理业务逻辑,把可以延迟执行的逻辑,做延迟执行处理。比如检查新版本、注册推送通知等逻辑。 41 | 3.避免复杂/多余的计算。 42 | 4.避免在首页控制器的viewDidLoad和viewWillAppear做太多事情,这2个方法执行完,首页控制器才能显示,部分可以延迟创建的视图应做延迟创建/懒加载处理。 43 | 5.采用性能更好的API。 44 | 6.首页控制器用纯代码方式来构建。 45 | -------------------------------------------------------------------------------- /iOS崩溃类型.md: -------------------------------------------------------------------------------- 1 | #iOS中的崩溃类型 2 | iOS崩溃是让iOS开发人员比较头痛的事情,app崩溃了,说明代码写的有问题,在这里了解一下XCode用来表示各种崩溃类型的术语,补充一些这方面的各知识。崩溃通常是指操作系统向正在运行的程序发送的信号,所以我们在查看崩溃日志时,常常看到如下错误摘要:Application received signal SIGSEGV。一般来说,常见的崩溃类型有以下几种: 3 | ## 一、EXC_BAD_ACCESS 4 | 野指针引起的崩溃,访问了一个已经释放的内存而导致,向已经释放的对象或向它发送消息时,EXC_BAD_ACCESS就会出现。造成EXC_BAD_ACCESS最常见的原因是,在初始化方法中初始化变量时用错了所有权修饰符,这会导致对象过早地被释放。举个例子,在viewDidLoad方法中为UIViewController创建了一个包含元素的NSArray,却将该数组的所有权修饰符设成了assign而不是strong。现在在viewWillAppear中,若要访问已经释放掉的对象时,就会得到名为EXC_BAD_ACCESS的崩溃。 5 | 6 | 这个崩溃发生时,查看崩溃日志,却往往得不到有用的栈信息。还好,有一个方法用来解决这个问题:NSZombieEnabled。 7 | 8 | 这是一个环境变量,用来调试与内存相关的问题,跟踪对象的释放过程。启用了NSZombieEnabled的话,它会用一个僵尸实现来去你的默认的dealloc实现,也就是在引用计数降到0时,该僵尸实现会将该对象转换成僵尸对象。僵尸对象的作用是在你向它发送消息时,它会显示一段日志并自动跳入调试器。 9 | 10 | 所以,当在应用中启用NSZombie而不是让应用直接崩溃时,一个错误的内存访问就会变成一条无法识别的消息发送给僵尸对象。僵尸对象会显示接收到的消息,然后跳入调试器,这样你就可以查看到底哪时出了问题。 可以在Xcode的scheme页面中设置NSZombieEnabled环境变量。点击Product-Edit Scheme打开该页面,然后勾选Enable Zombie Objects复选框,如图所示: 11 | ![Zombie Objects](https://upload-images.jianshu.io/upload_images/2598795-4cbec1ca16b84f5e.png) 12 | 13 | 僵尸在RAC出现以前作用很大。但自从有了ARC,如果你在对象的所有权方面比较注意,那么通常不会碰到内存相关的崩溃。 14 | 15 | ## 二、SIGSEGV 16 | 段错误信息(SIGSEGV)是操作系统产生的一个更严重的问题。当硬件出现错误、访问不可读的内存地址或向受保护的内存地址写入数据时,就会发生这个错误。 17 | 18 | 硬件错误这一情况并不常见。当要读取保存在RAM中的数据,而该位置的RAM硬件有问题时,你会收到SIGSEGV。SIGSEGV更多是出现在后两种情况。默认情况下,代码页不允许进行写操作。当应用中的某个指针指向代码页并试图修改指向位置的值时,你会收到SIGSEGV。当要读取一个指针的值,而它被初始化成指向无效内存地址的垃圾值时,你也会收到SIGSEGV。 19 | 20 | SIGSEGV错误调试起来更困难,而导致SIGSEGV的最常见原因是不正确的类型转换。要避免过度使用指针或尝试手动修改指针来读取私有数据结构。如果你那样做了,而在修改指针时没有注意内存对齐和填充问题,就会收到SIGSEGV。 21 | 22 | ## 三、SIGBUS 23 | 24 | 总线错误信号(SIGBUG)代表无效内存访问,即访问的内存是一个无效的内存地址。也就是说,那个地址指向的位置根本不是物理内存地址(它可能是某个硬件芯片的地址)。 25 | 26 | ## 四、SIGTRAP 27 | 28 | SIGTRAP代表陷阱信号。它并不是一个真正的崩溃信号。它会在处理器执行trap指令发送。LLDB调试器通常会处理此信号,并在指定的断点处停止运行。如果你收到了原因不明的SIGTRAP,先清除上次的输出,然后重新进行构建通常能解决这个问题。 29 | 30 | ## 五、EXC_ARITHETIC 31 | 32 | 当要除零时,应用会收到EXC_ARITHMETIC信号。这个错误应该很容易解决。 33 | 34 | ## 六、SIGILL 35 | 36 | SIGILL代表signal illegal instruction(非法指令信号)。当在处理器上执行非法指令时,它就会发生。执行非法指令是指,将函数指针会给另外一个函数时,该函数指针由于某种原因是坏的,指向了一段已经释放的内存或是一个数据段。有时你收到的是EXC_BAD_INSTRUCTION而不是SIGILL,虽然它们是一回事,不过EXC_*等同于此信号不依赖体系结构。 37 | 38 | ## 七、SIGABRT 39 | 40 | SIGABRT代表SIGNAL ABORT(中止信号)。当操作系统发现不安全的情况时,它能够对这种情况进行更多的控制;必要的话,它能要求进程进行清理工作。在调试造成此信号的底层错误时,并没有什么妙招。Cocos2d或UIKit等框架通常会在特定的前提条件没有满足或一些糟糕的情况出现时调用C函数abort(由它来发送此信号)。当SIGABRT出现时,控制台通常会输出大量的信息,说明具体哪里出错了。由于它是可控制的崩溃,所以可以在LLDB控制台上键入bt命令打印出回溯信息。 41 | 42 | ## 八、看门狗超时 43 | 44 | 这种崩溃通常比较容易分辨,因为错误码是固定的0x8badf00d。在iOS上,它经常出现在执行一个同步网络调用而阻塞主线程的情况。因此,永远不要进行同步网络调用。 45 | 46 | [原文](https://cnbin.github.io/blog/2016/03/15/ioszhong-de-beng-kui-lei-xing/) -------------------------------------------------------------------------------- /iOS编译过程.md: -------------------------------------------------------------------------------- 1 | #iOS编译过程 2 | 3 | ## 前言 4 | iOS 开发中使用的是编译语言,所谓编译语言是在执行的时候,必须先通过编译器生成机器码,机器码可以直接在CPU上执行,所以执行效率较高。他是使用 Clang / LLVM 来编译的。LLVM是一个模块化和可重用的编译器和工具链技术的集合,Clang 是 LLVM 的子项目,是 C,C++ 和 Objective-C 编译器,目的是提供惊人的快速编译。下面我们来看看编译过程,总的来说编译过程分为几个阶段: 5 | **预处理 -> 词法分析 -> 语法分析 -> 静态分析 -> 生成中间代码和优化 -> 汇编 -> 链接** 6 | 7 | ## 具体过程 8 | 9 | ### 一、预处理 10 | 我们以一个实际例子来看看,预处理的过程,源码: 11 | 12 | ``` 13 | #import "AppDelegate.h" 14 | 15 | #define NUMBER 1 16 | int main(int argc, char * argv[]) { 17 | @autoreleasepool { 18 | 19 | NSLog(@"%d",NUMBER); 20 | 21 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 22 | } 23 | } 24 | ``` 25 | 使用终端到main.m所在文件夹,使用命令:```clang -E main.m```,结果如下: 26 | 27 | ``` 28 | @interface AppDelegate : UIResponder 29 | 30 | @property (strong, nonatomic) UIWindow *window; 31 | 32 | @end 33 | # 11 "main.m" 2 34 | 35 | int main(int argc, char * argv[]) { 36 | @autoreleasepool { 37 | 38 | NSLog(@"%d",1); 39 | 40 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 41 | } 42 | } 43 | ``` 44 | 也可以使用Xcode的```Product->Perform Action -> Preprocess```得到相同的结果 45 | 这一步编译器所做的处理是: 46 | 47 | - 宏替换 48 | 在源码中使用的宏定义会被替换为对应#define的内容) 49 | 50 | >建议大家不要在需要预处理的代码中加入内联代码逻辑。 51 | - 头文件引入(#include,#import) 52 | 使用对应文件.h的内容替换这一行的内容,所以尽量减少头文件中的#import,使用@class替代,把#import放到.m文件中。 53 | - 处理条件编译指令 (#if,#else,#endif) 54 | 55 | ### 二、词法解析 56 | 使用```clang -Xclang -dump-tokens main.m```词法分析结果如下: 57 | 58 | ``` 59 | int 'int' [StartOfLine] Loc= 60 | identifier 'main' [LeadingSpace] Loc= 61 | l_paren '(' Loc= 62 | int 'int' Loc= 63 | identifier 'argc' [LeadingSpace] Loc= 64 | comma ',' Loc= 65 | char 'char' [LeadingSpace] Loc= 66 | star '*' [LeadingSpace] Loc= 67 | identifier 'argv' [LeadingSpace] Loc= 68 | l_square '[' Loc= 69 | r_square ']' Loc= 70 | r_paren ')' Loc= 71 | l_brace '{' [LeadingSpace] Loc= 72 | at '@' [StartOfLine] [LeadingSpace] Loc= 73 | identifier 'autoreleasepool' Loc= 74 | l_brace '{' [LeadingSpace] Loc= 75 | identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc= 76 | l_paren '(' Loc= 77 | at '@' Loc= 78 | string_literal '"%d"' Loc= 79 | comma ',' Loc= 80 | numeric_constant '1' Loc=> 81 | r_paren ')' Loc= 82 | semi ';' Loc= 83 | return 'return' [StartOfLine] [LeadingSpace] Loc= 84 | identifier 'UIApplicationMain' [LeadingSpace] Loc= 85 | l_paren '(' Loc= 86 | identifier 'argc' Loc= 87 | comma ',' Loc= 88 | identifier 'argv' [LeadingSpace] Loc= 89 | comma ',' Loc= 90 | identifier 'nil' [LeadingSpace] Loc= 91 | comma ',' Loc= 92 | identifier 'NSStringFromClass' [LeadingSpace] Loc= 93 | l_paren '(' Loc= 94 | l_square '[' Loc= 95 | identifier 'AppDelegate' Loc= 96 | identifier 'class' [LeadingSpace] Loc= 97 | r_square ']' Loc= 98 | r_paren ')' Loc= 99 | r_paren ')' Loc= 100 | semi ';' Loc= 101 | r_brace '}' [StartOfLine] [LeadingSpace] Loc= 102 | r_brace '}' [StartOfLine] Loc= 103 | eof '' Loc= 104 | ``` 105 | 这一步把源文件中的代码转化为特殊的标记流,源码被分割成一个一个的字符和单词,在行尾Loc中都标记出了源码所在的对应源文件和具体行数,方便在报错时定位问题。 106 | ### 三、语法分析 107 | 执行 ```clang 命令 clang -Xclang -ast-dump -fsyntax-only maim.m```得到如下结果: 108 | 109 | ``` 110 | |-FunctionDecl 0x7f9fa085a9b8 line:14:5 main 'int (int, char **)' 111 | | |-ParmVarDecl 0x7f9fa085a788 col:14 used argc 'int' 112 | | |-ParmVarDecl 0x7f9fa085a8a0 col:27 used argv 'char **':'char **' 113 | | `-CompoundStmt 0x7f9fa1002240 114 | | `-ObjCAutoreleasePoolStmt 0x7f9fa1002230 115 | | `-CompoundStmt 0x7f9fa1002210 116 | | `-CallExpr 0x7f9fa085aec0 'void' 117 | | |-ImplicitCastExpr 0x7f9fa085aea8 'void (*)(id, ...)' 118 | | | `-DeclRefExpr 0x7f9fa085ac90 'void (id, ...)' Function 0x7f9fa085ab38 'NSLog' 'void (id, ...)' 119 | | |-ImplicitCastExpr 0x7f9fa085aef8 'id':'id' 120 | | | `-ObjCStringLiteral 0x7f9fa085ae08 'NSString *' 121 | | | `-StringLiteral 0x7f9fa085acf8 'char [3]' lvalue "%d" 122 | | `-IntegerLiteral 0x7f9fa085ae28 'int' 1 123 | |-FunctionDecl 0x7f9fa085ab38 col:9 implicit used NSLog 'void (id, ...)' extern 124 | | |-ParmVarDecl 0x7f9fa085abd0 <> 'id':'id' 125 | | `-FormatAttr 0x7f9fa085ac38 Implicit NSString 1 2 126 | |-FunctionDecl 0x7f9fa085af60 <> line:19:16 implicit used UIApplicationMain 'int ()' 127 | `-FunctionDecl 0x7f9fa085b098 <> col:51 implicit used NSStringFromClass 'int ()' 128 | ``` 129 | 这一步是把词法分析生成的标记流,解析成一个抽象语法树(abstract [syntax tree -- AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html)),同样地,在这里面每一节点也都标记了其在源码中的位置。 130 | ### 四、静态分析 131 | 把源码转化为抽象语法树之后,编译器就可以对这个树进行分析处理。静态分析会对代码进行错误检查,如出现方法被调用但是未定义、定义但是未使用的变量等,以此提高代码质量。当然,还可以通过使用 Xcode 自带的静态分析工具(Product -> Analyze) 132 | 133 | - 类型检查 134 | 在此阶段clang会做检查,最常见的是检查程序是否发送正确的消息给正确的对象,是否在正确的值上调用了正常函数。如果你给一个单纯的 NSObject* 对象发送了一个 hello 消息,那么 clang 就会报错,同样,给属性设置一个与其自身类型不相符的对象,编译器会给出一个可能使用不正确的警告。 135 | 136 | >一般会把类型分为两类:动态的和静态的。动态的在运行时做检查,静态的在编译时做检查。以往,编写代码时可以向任意对象发送任何消息,在运行时,才会检查对象是否能够响应这些消息。由于只是在运行时做此类检查,所以叫做动态类型。 137 | 138 | >至于静态类型,是在编译时做检查。当在代码中使用 ARC 时,编译器在编译期间,会做许多的类型检查:因为编译器需要知道哪个对象该如何使用。 139 | - 其他分析 140 | ```ObjCUnusedIVarsChecker.cpp```是用来检查是否有定义了,但是从未使用过的变量。 141 | ```ObjCSelfInitChecker.cpp```是检查在 你的初始化方法中中调用 self 之前,是否已经调用 [self initWith...] 或 [super init] 了。 142 | 143 | ![checkers](https://user-gold-cdn.xitu.io/2018/12/17/167bb9570949934d?w=1030&h=364&f=png&s=147454) 144 | 更多请看:[clang静态分析](https://github.com/llvm-mirror/clang/tree/master/lib/StaticAnalyzer) 145 | 146 | ### 五、中间代码生成和优化 147 | 使用```clang -O3 -S -emit-llvm main.m -o main.ll```生成main.ll文件,打开并查看转化结果: 148 | 149 | ``` 150 | ; ModuleID = 'main.m' 151 | source_filename = "main.m" 152 | target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128" 153 | target triple = "x86_64-apple-macosx10.13.0" 154 | 155 | %struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 } 156 | 157 | @__CFConstantStringClassReference = external global [0 x i32] 158 | @.str = private unnamed_addr constant [3 x i8] c"%d\00", section "__TEXT,__cstring,cstring_literals", align 1 159 | @_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i32 0, i32 0), i64 2 }, section "__DATA,__cfstring", align 8 160 | 161 | ; Function Attrs: ssp uwtable 162 | define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 { 163 | %3 = tail call i8* @objc_autoreleasePoolPush() #2 164 | notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), i32 1) 165 | tail call void @objc_autoreleasePoolPop(i8* %3) 166 | ret i32 0 167 | } 168 | 169 | declare i8* @objc_autoreleasePoolPush() local_unnamed_addr 170 | 171 | declare void @NSLog(i8*, ...) local_unnamed_addr #1 172 | 173 | declare void @objc_autoreleasePoolPop(i8*) local_unnamed_addr 174 | 175 | attributes #0 = { ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } 176 | attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" } 177 | attributes #2 = { nounwind } 178 | 179 | !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6} 180 | !llvm.ident = !{!7} 181 | 182 | !0 = !{i32 1, !"Objective-C Version", i32 2} 183 | !1 = !{i32 1, !"Objective-C Image Info Version", i32 0} 184 | !2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"} 185 | !3 = !{i32 4, !"Objective-C Garbage Collection", i32 0} 186 | !4 = !{i32 1, !"Objective-C Class Properties", i32 64} 187 | !5 = !{i32 1, !"wchar_size", i32 4} 188 | !6 = !{i32 7, !"PIC Level", i32 2} 189 | !7 = !{!"Apple LLVM version 9.1.0 (clang-902.0.39.2)"} 190 | ``` 191 | 192 | 接下来 LLVM 会对代码进行编译优化,例如针对全局变量优化、循环优化、尾递归优化等,最后输出汇编代码。 193 | 194 | 使用```xcrun clang -S -o - main.m | open -f```生成汇编代码: 195 | 196 | ``` 197 | .section __TEXT,__text,regular,pure_instructions 198 | .macosx_version_min 10, 13 199 | .globl _main ## -- Begin function main 200 | .p2align 4, 0x90 201 | _main: ## @main 202 | .cfi_startproc 203 | ## BB#0: 204 | pushq %rbp 205 | Lcfi0: 206 | .cfi_def_cfa_offset 16 207 | Lcfi1: 208 | .cfi_offset %rbp, -16 209 | movq %rsp, %rbp 210 | Lcfi2: 211 | .cfi_def_cfa_register %rbp 212 | subq $32, %rsp 213 | movl $0, -4(%rbp) 214 | movl %edi, -8(%rbp) 215 | movq %rsi, -16(%rbp) 216 | callq _objc_autoreleasePoolPush 217 | leaq L__unnamed_cfstring_(%rip), %rsi 218 | movl $1, %edi 219 | movl %edi, -20(%rbp) ## 4-byte Spill 220 | movq %rsi, %rdi 221 | movl -20(%rbp), %esi ## 4-byte Reload 222 | movq %rax, -32(%rbp) ## 8-byte Spill 223 | movb $0, %al 224 | callq _NSLog 225 | movq -32(%rbp), %rdi ## 8-byte Reload 226 | callq _objc_autoreleasePoolPop 227 | xorl %eax, %eax 228 | addq $32, %rsp 229 | popq %rbp 230 | retq 231 | .cfi_endproc 232 | ## -- End function 233 | .section __TEXT,__cstring,cstring_literals 234 | L_.str: ## @.str 235 | .asciz "%d" 236 | 237 | .section __DATA,__cfstring 238 | .p2align 3 ## @_unnamed_cfstring_ 239 | L__unnamed_cfstring_: 240 | .quad ___CFConstantStringClassReference 241 | .long 1992 ## 0x7c8 242 | .space 4 243 | .quad L_.str 244 | .quad 2 ## 0x2 245 | 246 | .section __DATA,__objc_imageinfo,regular,no_dead_strip 247 | L_OBJC_IMAGE_INFO: 248 | .long 0 249 | .long 64 250 | 251 | 252 | .subsections_via_symbols 253 | 254 | ``` 255 | 前面的三行: 256 | 257 | ``` 258 | .section __TEXT,__text,regular,pure_instructions 259 | .macosx_version_min 10, 13 260 | .globl _main ## -- Begin function main 261 | .p2align 4, 0x90 262 | ``` 263 | 他们是汇编指令而不是汇编代码。 264 | 265 | - ```.section```指令指定了接下来会执行哪一个段 266 | - ```.globl```指令说明```_main```是一个外部符号。这就是我们的main()函数。这个函数对外部是可见的,因为系统要调用它来运行可执行文件。 267 | - ```.p2align```指令指出了后面代码的对齐方式。在我们的代码中,后面的代码会按照 16(2^4) 字节对齐,如果需要的话,用 0x90 补齐。 268 | 269 | 想要了解更多可以看一下这篇文章:[《LLVM 全时优化》](https://blog.csdn.net/dashuniuniu/article/details/50385528)。 270 | 271 | ### 六、汇编 272 | 在这一阶段,汇编器将上一步生成的可读的汇编代码转化为机器代码。最终产物就是 以 .o 结尾的目标文件。使用Xcode构建的程序会在DerivedData目录中找到这个文件。如图: 273 | 274 | ![.o](https://user-gold-cdn.xitu.io/2018/12/18/167c00b045143702?w=3934&h=572&f=png&s=605402) 275 | 276 | ### 七、链接 277 | 这一阶段是将上个阶段生成的目标文件和引用的静态库链接起来,最终生成可执行文件,链接器解决了目标文件和库之间的链接。 278 | 279 | 使用```clang main.m```生成可执行文件a.out(不指定名字默认为a.out),使用```file a.out```可以看到其类型信息: 280 | 281 | ``` 282 | a.out: Mach-O 64-bit executable x86_64 283 | ``` 284 | 可以看出可执行文件类型为 Mach-O 类型,在 MAC OS 和 iOS 平台的可执行文件都是这种类型。因为我使用的是模拟器,所以处理器指令集为 x86_64。 285 | 286 | 至此,编译过程结束。 287 | 288 | ## Mach-O文件 289 | 290 | #### Mach-O简介 291 | 根据[官方文档](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/MachOOverview.html#//apple_ref/doc/uid/20001860-100029-TPXREF104)的描述: 292 | 293 | Mach-O是OS X中二进制文件的原生可执行格式,是传送代码的首选格式。可执行格式决定了二进制文件中的代码和数据读入内存的顺序。代码和数据的顺序会影响内存使用和分页活动,从而直接影响程序的性能。 294 | 295 | Mach-O二进制文件被组织成段。每个部分包含一个或多个部分。段的大小由它所包含的所有部分的字节数来度量,并四舍五入到下一个虚拟内存页边界。因此,一个段总是4096字节或4千字节的倍数,其中4096字节是最小大小。 296 | 297 | #### Mach-O结构 298 | Mach-O文件的结构如下: 299 | ![Mach-O](https://user-gold-cdn.xitu.io/2018/12/18/167c0a81d53802c6?w=365&h=401&f=jpeg&s=20162) 300 | 301 | 1. Header 302 | 保存了Mach-O的一些基本信息,包括了平台、文件类型、LoadCommands的个数等等。 303 | 使用```otool -v -h a.out```查看其内容: 304 | 305 | ![Mach-o Header](https://user-gold-cdn.xitu.io/2018/12/18/167c0a684d4a0378?w=2096&h=124&f=png&s=104502) 306 | 307 | 2. Load commands 308 | 这一段紧跟Header,加载Mach-O文件时会使用这里的数据来确定内存的分布 309 | 3. Data 310 | 包含 Load commands 中需要的各个 segment,每个 segment 中又包含多个 section。当运行一个可执行文件时,虚拟内存 (virtual memory) 系统将 segment 映射到进程的地址空间上。 311 | 312 | 使用```xcrun size -x -l -m a.out ```查看segment中的内容: 313 | 314 | ![](https://user-gold-cdn.xitu.io/2018/12/18/167c0a82a405cca6?w=1118&h=474&f=png&s=269150) 315 | 316 | - Segment __PAGEZERO。 317 | 大小为 4GB,规定进程地址空间的前 4GB 被映射为不可读不可写不可执行。 318 | - Segment __TEXT。 319 | 包含可执行的代码,以只读和可执行方式映射。 320 | - Segment __DATA。 321 | 包含了将会被更改的数据,以可读写和不可执行方式映射。 322 | - Segment __LINKEDIT。 323 | 包含了方法和变量的元数据,代码签名等信息。 324 | 325 | 资料: 326 | [Mach-O Executable Format](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/MachOOverview.html#//apple_ref/doc/uid/20001860-100029-TPXREF104) 327 | [编译器](https://objccn.io/issue-6-2/) 328 | [Mach-O 可执行文件](https://objccn.io/issue-6-3/) -------------------------------------------------------------------------------- /iOS设计模式.md: -------------------------------------------------------------------------------- 1 | # iOS设计模式 2 | 3 | ## 前言 4 | 5 | 设计模式是有用的抽象化工具,用于解决工程和建筑等领域的设计问题。出于同样的目的,软件开发领域借用了这一概念,设计模式是一个对象或类的设计模板,用于解决特定领域经常发生的问题。本篇共分8部分涉及22种设计模式: 6 | 7 | - 对象创建(1~6) 8 | - 原型模式 9 | - 简单工厂模式 10 | - 工厂模式 11 | - 抽象工厂模式 12 | - 建造者(生成器)模式 13 | - 单例模式 14 | - 接口适配 (7~9) 15 | - 适配器模式 16 | - 桥接模式 17 | - 外观模式 18 | - 对象去耦 (10~11) 19 | - 中介者模式 20 | - 观察者模式 21 | - 抽象集合 (12~13) 22 | - 组合模式 23 | - 迭代器模式 24 | - 行为扩展 (14~16) 25 | - 访问者模式 26 | - 装饰者模式 27 | - 责任链模式 28 | - 算法封装 (17~19) 29 | - 模板方法模式 30 | - 策略模式 31 | - 命令模式 32 | - 性能与对象访问 (20~21) 33 | - 享元模式 34 | - 代理模式 35 | - 对象状态 (22) 36 | - 备忘录模式 37 | 38 | ![Design Partten](https://user-gold-cdn.xitu.io/2019/1/15/1684f4cdee47c8cc?w=1796&h=1686&f=png&s=347552) 39 | ## 对象创建 40 | ### 一、原型模式 41 | 1. 什么是原型模式? 42 | 使用原型实例指定创建对象的种类,并通过复制这个对象创建新的对象。 43 | >原型模式其实是通过一个对象为模板创建另外一个可定制的对象,而且不需要知道任何的创建细节。 44 | 45 | 2. 什么时候使用原型模式? 46 | - 需要创建的对象应该独立于其类型和创建方式。 47 | - 需要实例化的类是在运行时决定的。 48 | - 不想要与产品层次相对应的工厂层次。 49 | - 不同类的实例间的差异仅是状态的若干组合。因此复制相应数量的原型比手工实例化更加方便。 50 | - 类不容易创建,比如每个组件可把其他组件作为子节点的组合对象。复制已有的组合对象并对副本进行修改会更加容易。 51 | 52 | 3. 深拷贝与浅拷贝 53 | - 浅拷贝,指针级拷贝,拷贝出的新实例依旧指向源内存空间,不论修改原来的实例或者拷贝出的实例,都会影响到另一个(因为指针指向同一块内存)。 54 | - 深拷贝,内存级拷贝,会开辟新的内存空间,修改原来的实例或者拷贝的实例,另一个不受到影响。 55 | 56 | >iOS中默认的```-copy```是浅拷贝,若要深拷贝,需要遵守``````协议,重写```-copyWithZone```方法。 57 | 58 | ### 二、简单工厂模式 59 | 1. 什么是简单工厂模式? 60 | 简单工厂模式(Simple Factory Pattern),又称为静态工厂方法(Static Factory Method)模式,在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。 61 | 2. 简单工厂模式的优点 62 | - 将对象的创建和对象本身业务处理分离可以降低系统的耦合度,使得两者修改起来都相对容易。 63 | - 当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。 64 | - 在调用工厂类的工厂方法时,由于工厂方法是静态方法,使用起来很方便,可通过类名直接调用,而且只需要传入一个简单的参数即可,修改参数时无须修改任何源代码。 65 | 3. 简单工厂模式的缺点 66 | - 简单工厂模式最大的问题在于工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,这一点与开闭原则是相违背的。 67 | 68 | [简单工厂模式Demo(计算器)](https://github.com/qingfengiOS/QFSimpleFactory.git) 69 | 70 | ### 三、工厂模式 71 | 1. 什么是工厂模式? 72 | 定义创建对象的接口,让子类决定实例化哪一个类。工厂方法使得一个类的实例化延迟到了子类。 73 | 74 | 2. 什么时候使用工厂模式? 75 | - 编译时无法准确预期需要创建对象的类。 76 | - 类想要其子类决定在运行时创建什么类型的实例。 77 | - 类有若干辅助类为其子类,而你想将返回哪个子类这种信息局部化。 78 | 3. 工厂模式的优势 79 | 和直接创建具体对象相比,使用工厂方法创建对象算是最佳的做法。 80 | 工厂方法让客户端可以要求由工厂方法创建的对象拥有一组共同的行为。 81 | 因此,向类层次结构中引入新的具体产品并不需要修改客户端代码,因为返回的任何具体对象的接口跟客户端一直使用的接口相同。 82 | 4. Cocoa Touch中的工厂方法 83 | 工厂方法在Cocoa Touch中使用极为广泛,以NSNumber为例,它提供了很多的创建方法,比如```-numberWithBool```和```-numberWithInt```他们传入不同类型的参数,获得NSNumber实例。 84 | 85 | [工厂模式Demo(计算器)](https://github.com/qingfengiOS/QFFactory.git) 86 | 87 | ### 四、抽象工厂 88 | 1. 什么是抽象工厂? 89 | 提供一个创建一系列相关或者相互依赖对象的接口,而无需指定他们的具体类。 90 | 91 | 如果有多个类共有相同的行为,但实际实现不同,则可能需要某种抽象类型做为他们的父类,抽取其他们共同的行为到父类中。 92 | 93 | 例如,我们知道普通的披萨饼是什么样子,在点餐的时候能预计到端上来的是什么。当我们说"出去吃披萨"时,这里的“披萨”其实就是一个抽象类型,定义了披萨饼应该共同具有的特征。但是,从不同的店我们得到同一披萨饼(比如意大利披萨饼、腊肠披萨饼)的味道大不相同。因为有太多类型的披萨饼,我们简单地将其叫做“披萨饼”以称呼这种特定类型的食品。 94 | 95 | 父类的类方法```-getFactory```仅仅只是返回具体的(合适需求的)工厂类。其子类不应该重载这个方法。```-getFactory```方法根据配置返回一个具体的工厂实例。 96 | 97 | 2. 抽象工厂和工厂模式的比较 98 | 这两者在很多方面都很相似,他们都用于相同的目的:创建对象而不让客户端知晓创建细节,他们的对比如下: 99 | 100 | | | 抽象工厂 | 工厂模式 | 101 | | --- | :---: | :---: | 102 | | 创建方式 | 对象组合创建抽象产品 | 类继承创建抽象产品 | 103 | | 创建种类 | 创建多系列产品 | 创建一种产品 | 104 | | 如何扩展 | 修改父类的接口才支持新产品 | 子类创建者重载工厂方法以创建新的产品 | 105 | 106 | [抽象工厂Demo(地图引擎)](https://github.com/qingfengiOS/FactoryMapView.git) 107 | ### 五、建造者(生成器)模式 108 | 109 | 1. 什么是建造者模式 110 | 将一个复杂对象的构建与他的表现分离,使得同样的构建过程可以创建不同的表现。 111 | 112 | >它可以将一个产品的内部表象与产品的生成过程分割开来,从而可以使一个建造过程生成具有不同内部表象的产品对象。 113 | >如果我们用了建造者模式,那么用户只需要指定需要建造的类型就可以得到他们的对象实例,而无需关心建造过程和细节。 114 | 115 | 在此模式中,除了用户和其所需的产品,还有两个重要的角色**Director(指导者)**和**Builder(生成器)**。 116 | - Director知道Builder应该建造什么,以参数向其提供缺少的信息来创建特定产品。 117 | - Builder是一个抽象接口,声明了一个-build方法,该方法由ConcreBuilder实现,以构造实际的产品,ConcreBuilder有一个-getResult方法,向客户端返回建造完毕的结果。 118 | 119 | 2. 何时使用建造者模式 120 | - 需要创建涉及各种部件的复杂对象。创建对象的算法应该独立于部件的装配方式。常见例子是构建组合对象。 121 | - 构建过程需要以不同的方式(比如,部件或者表现不同的组合)构建对象。 122 | 123 | 3. 建造者模式和抽象工厂的对比 124 | 抽象工厂和建造者模式在 抽象对象创建方有许多相似之处,但是,二者却大不相同。 125 | - 建造者模式关注的是分步骤创建复杂对象,很多时候,同一类型的对象可以以不同的方式创建。建造者模式在多步创建过程的最后一步返回产品。 126 | - 抽象工厂的重点在于创建简单或者复杂产品的套件。抽象工厂立即返回产品。 127 | 128 | | | 建造者模式 | 抽象工厂 | 129 | | --- | :---: | :---: | 130 | |对象的复杂程度 | 构建复杂对象 | 构建简单或者复杂对象 | 131 | | 需要的步骤 | 多步创建 | 单一步骤创建 | 132 | | 创建方式种类 | 多种方式创建 | 单一方式创建 | 133 | | 返回对象的时机| 创建过程的最后一步| 立即返回产品| 134 | | 创建结果| 专注一个特定的产品 | 强调创建一套产品| 135 | 136 | 4. 总结 137 | 生成器模式能帮助构建涉及部件与表现的各种组合的对象。没有这一模式,知道构建对象所需细节的Director可能会最终变成一个极其复杂的类。带有无数用于构建同一类的各种表现的内嵌算法。而这些算法本应该独立于该对象的组成部分以及他们的装配过程。 138 | 139 | [建造者模式Demo(画卡通人)](https://github.com/qingfengiOS/QFBuilder) 140 | ### 六、单例模式 141 | 142 | 1. 何为单例模式 143 | 144 | 单例模式:保证一个类仅有一个实例,并且提供一个访问它的全局访问点。 145 | 146 | 单例模式几乎是设计模式中最简单的了。它的意图是使得类的一个对象成为系统中的唯一实例。要实现这一点,可以从客户端对其进行实例化开始。因此,需要用一种只允许生成对象类唯一实例的机制来“阻止”所有想要生成对象的访问。 147 | 148 | 2. 何时使用单例模式 149 | - 类只能有一个实例,而且必须从一个为人熟知的访问点对其进行访问,(比如工厂方法)。 150 | - 这个唯一的实例只能通过子类化进行扩展,而且扩展的对象不会破坏客户端代码。 151 | 3. Objective-C实现单例模式 152 | 在OC中,单例模式的实现目前分为两种: 153 | - 原始实现 154 | 遵守``````协议重写```-alloc```方法和```-copy```方法,考虑线程安全。 155 | - GCD实现 156 | 借助GCD的```dispatch_once```实现 157 | 158 | [单例模式Demo(一个严谨的单例)](https://github.com/qingfengiOS/Singleton.git) 159 | ## 接口适配 160 | ### 七、适配器 161 | 1. 何为适配器模式 162 | 163 | 适配器模式,用于连接两种不同种类的对象,使其毫无问题地协同工作。其思想比较简单:适配器实现客户端所需要的某种接口的行为,同时,它又连接到一个具有(完全)不同接口行为的对象。一边是客户端懂得如何使用的目标接口,另一边是客户端一无所知的被适配者,适配器在二者之间。它的主要作用是把被适配者的行为传递给管道另一端的客户端。 164 | 165 | 定义:**将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本接口不兼容而不能在一起工作的类可以一起工作了。** 166 | 167 | 2. 适配器分类 168 | - 类适配器 169 | 它是通过类继承实现的,而Objective-C有着协议(Protocol)这一语言特性,所以在Objective-C中,类可以实现协议,同时又继承自父类,从而达到C++中多继承的效果。 170 | 要在OC中实现类适配器,首先需要有定义了客户端要使用的一套行为的协议,然后用具体的适配器类来实现这个协议,适配器类同时也继承被适配者。 171 | 172 | - 对象适配器 173 | 与上面的类适配器不同,对象适配器不继承被适配者,而是组合了一个对他的引用。 174 | 3. 类适配器与对象适配器的比较 175 | 176 | | | 类适配器 | 对象适配器 | 177 | | --- | --- | --- | 178 | | | 只针对单一具体的Adaptee类,把Adaptee适配到target | 可以适配多个Adaptee及子类 | 179 | | | 易于重载Adaptee的行为,以为是通过直接的子类化进行的适配 | 难以重载,需要借助子类的对象而非Adaptee本身 | 180 | | | 只有一个Adaptee对象,无需额外的这镇间接访问Adaptee | 需要额外的指针间接访问Adaptee并适配其行为 | 181 | 182 | Adaptee:被适配者 183 | Target:目标接口 184 | 185 | 4. 何时使用适配器模式 186 | - 已有类的接口与需求不匹配 187 | - 想要一个可复用的类,该类能够同可能带有不兼容接口的其他类协作。 188 | - 需要适配一个类的几个不同子类,可是让每一个子类去子类化一个类适配又不现实,那么可以使用对象适配器(委托)来适配其父类的接口。 189 | 190 | ### 八、桥接模式 191 | 1. 何为桥接模式? 192 | **将抽象部分与它的实现部分分离,使它们都可以独立地变化。** 193 | 194 | >所谓抽象与它的实现分离,这并不是说,让抽象类与其派生类分离,因为这没有任何意义。实现指的是抽象类和它的派生类用来实现自己的对象。 195 | 196 | >实现系统时可能有多角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来,让他们独立变化,减少他们之间的耦合。 197 | 198 | 2. 合成/聚合复用原则 199 | - 继承的弊端 200 | 对象的继承关系在编译时就已经定好了,所以无法在运行时改变从父类继承的实现。子类的实现与其父类有着非常紧密的依赖关系,以至于父类实现中任何变化必然会导致子类发生变化。当需要复用子类时,如果继承下来的实现不适合新的问题,则父类必须重写或者被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。 201 | - 合成/聚合复用原则 202 | 合成(也叫做组合,Composition)和聚合(Aggregation)都是关联的特殊种类。聚合表示一种弱的“拥有“关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分;合成则是一种强的”拥有“关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样。 203 | 204 | >比如:大雁有两个翅膀,翅膀与大雁是部分和整体的关系,并且他们的生命周期相同,于是大雁和翅膀就是合成关系。大雁是群居动物,每只大雁都属于一个雁群,一个雁群可以有多只大雁,所以,大雁和雁群是聚合关系。 205 | 206 | **优先使用对象的组合/聚合将有助于保持每个类被封装,并集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。** 207 | 208 | 3. 何时使用桥接模式 209 | - 不想在抽象与实现之间形成固定的绑定关系(这样可以在运行时按需切换实现) 210 | - 抽象及其实现都应可以通过子类化独立扩展。 211 | - 对抽象的实现进行修改不应该影响客户端代码。 212 | - 如果每个实现需要额外的子类以细化抽象,则说明有必要把他们分成两个部分。 213 | - 想在带有不同抽象接口的多个对象之间共享一个实现。 214 | 215 | [桥接模式Demo(手机软件系统)](https://github.com/qingfengiOS/QFBridge) 216 | 217 | ### 九、外观模式 218 | 1. 何为外观模式 219 | 定义:**为系统中的一组接口提供一个统一的接口,外观定义一个高层接口,让子系统更易于使用。** 220 | 221 | 外观模式为子系统中一组不同的接口提供一个统一的接口。外观定义了上层接口,通过降低复杂度和隐藏子系统之间的通信及依存关系,让子系统更易于使用。 222 | 2. 何时使用外观模式 223 | - 子系统正在逐渐变得复杂。应用模式的过程中演化出许多类。可以使用外观模式为这些子系统类提供一个更简单的接口。 224 | - 可以使用外观对子系统进行分层。每个子系统级别有一个外观作为入口点。让他们通过其外观进行通信,可以简化他们的依赖关系。 225 | 226 | 3. 总结 227 | 当程序逐渐变大变复杂的时候,会有越来越多小型的类从设计和应用模式中演化出来。如果没有一种简化的方式来使用这些类,客户端代码会变得越来越复杂和难以理解,而且难以维护,外观模式有助于提供一中更为简单的方法来使用子系统中的这些类。处理这些子系统的默认行为,可能只是定义在外观中的简单方法,而不必直接使用这些类。 228 | 229 | [外观模式Demo(乘客乘车案例)](https://github.com/qingfengiOS/QFFacade) 230 | 231 | ## 对象去耦 232 | ### 十、中介者模式 233 | 1. 何为中介者模式 234 | 定义:**用一个对象来封装一系列对象的交互方式。中介者使各个对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互** 235 | 236 | 面向对象的设计鼓励把行为分散到不同的对象中,这种分散可能导致对象之间的相互关联。在最糟糕的情况下,所有对象都彼此之间了解和相互操作。 237 | 238 | 虽然把行为分散到不同对象增强了可复用性,但还是增加的相互关联有减少了获得的益处。增加的关联使得独享很难或者不能在不依赖其他对象的情况下工作。应用程序的整体行为可能难以改动,因为他分布于许多对象。 239 | 240 | 中介者模式用于解决此类问题,它定义了一个集中的场所,对象之间的交互可以在一个中介者对象中处理,其他对象不必彼此交互,因此减少了他们之间的依存关系。 241 | 242 | 2. 何时使用中介者模式 243 | - 对象之间的交互虽然定义明确但是非常复杂,导致一组对象彼此相互依赖而且难以理解。 244 | - 因为对象引用了许多其他对象并且与其通讯,导致对象难以复用。 245 | - 想要定制一个分布在多个类中的逻辑或行为,又不想生成太多子类。 246 | 247 | 3. 中介者模式的优缺点 248 | 中介者模式很容易在系统中使用,也很容易误用。当系统出现了“多对多”交互复杂的对象群时,不要急于使用中介者模式,而要先反思系统的设计是否合理。 249 | 250 | - 优点: 251 | 1、中介者的存在,减少了各个具体类的耦合度,使各个具体类和中介者可以独立地改变和复用; 252 | 253 | 2、由于把对象如何协作进行了抽象,将中介者作为一个独立的概念并将其封装在一个对象中,这样关注的对象就从对象各个本身的行为转移到了他们之间的交互上来,也就是站在一个更宏观的角度去看待系统。 254 | 255 | - 缺点: 256 | 由于中介者控制了集中化,于是把交互复杂性变为了中介者的复杂性,这就使得中介者会变得比任何一个具体类都复杂。 257 | 258 | 4. 总结 259 | 中介者模式的应用十分广泛,组件化应该算是最贴切的中介者模式的应用场景了,各个组件,独立开发,维护,组件之间使用中间件进行通讯。详见:[iOS 一个轻量级的组件化思路](https://juejin.im/post/5be433f1f265da616c65198d) 260 | 261 | 虽然对于处理系统的行为分散于不同对象并且对象相互依存的情况,中介者模式非常有用,但是应该注意避免让中介者类过于庞大而难以维护。如果已经如此了,可以考虑使用另外的设计模式把它分解。 262 | 263 | [中介者模式Demo(同事对话)](https://github.com/qingfengiOS/QFMediator) 264 | 265 | ### 十一、观察者模式 266 | 1. 何为观察者模式 267 | 定义:**定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并且被自动更新** 268 | 269 | 观察者模式也叫做发布——订阅模式。Observer从Subject订阅通知。ConcreteObserver实现抽象Observer并重载其update方法。一旦Subject的实例需要通知Observer任何的变更,Subject会发送update消息来通知存储在内部列表中所有注册的Observer。在ConcreteObserver的update方法的实际实现中,Subject的内部状态可被取得并在以后进行处理。 270 | 271 | 2. 何时使用观察者模式 272 | - 有两种抽象类型相互依赖。将它们封装在各自的对象中,就可以对它们单独进行改变和复用。 273 | - 对一个对象的改变需要同时改变其他对象,而不知道具体有多少对象有待改变。 274 | - 一个对象必须通知其他对象,而它又不需要知道其他对象是什么。 275 | 276 | 3. Cocoa Touch框架中的观察者模式 277 | - 通知 278 | Cocoa Touch框架中使用NSNotificationCenter和NSNotification对象实现了一对多的发布——订阅模式,他们允许主题与观察者以一种松耦合的方式通信。两者在通信时对另外一方无需多少了解。 279 | - 键—值观察 280 | Cocoa提供了另外一种称为键-值观察的机制,对象之间可以通过它得到其他对象特定的变更通知。这种机制在MVC(Model-View-Controller)模式的场景中尤其重要,它让视图对象可以经由控制器层观察模型对象的变更。 281 | 282 | || 通知 | 键-值观察 | 283 | | :---:| :---: | :---: | 284 | || 一个中心对象为所有观察者提供变更通知 | 被观察的对象直接向观 察者发送通知 | 285 | || 主要从广义上关注程序事件 | 绑定于特定对象属性的值 | 286 | 287 | 288 | ## 抽象集合 289 | 290 | ### 十二、组合模式 291 | 1. 什么是组合模式 292 | 定义:**将对象组合成树形结构以表示“部分—整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。** 293 | 294 | 组合模式让我们可以把相同基类型(base type)的对象组合到树状结构中,其中父节点包含同类型的子节点。换句话说,这种树状结构形成"部分-整体"的层次结构。 295 | 296 | 树形结构是既包含对象的组合(容器)又包含作为叶节点(基元)的单个对象对的一种层次结构。每个组合体包含的节点可以是叶节点也可以是其他组合体。这种关系在这个层次结构中递归重复。客户端对组合结点和叶节点拥有相同的操作,客户端在使用时可以忽略他们之间的差别。 297 | 298 | 2. 何时使用组合模式 299 | - 想获得对象抽象的树形结构(部分-整体层次结构)。 300 | - 想让客户端统一处理组合结构中的所有对象。 301 | 302 | 3. 透明方式与安全方式 303 | - 透明方式 304 | 在叶节点中也有```-add```和```-remove```方法,然而叶节点上不需要这些行为;这样做的目的是为了让他们完全相同的接口,调用方完全用处理这种逻辑,这就是透明方式。 305 | 306 | - 安全方式 307 | 如果不希望叶节点上存在上面的方法,那么在最基本结构(Component)中就不声明```-add```和```-remove```方法,而是另外在声明一个结构(Composite)用来管理子类对象的方法,这样就能避免透明代理出现的问题,但同时,由于不够透明,所以树叶和树枝拥有不同的接口,客户端的调用需要做相应的判断,给调用方带来了不便。 308 | 309 | 两种方式没有绝对的优劣,看个人的理解和取舍(类比简单工厂和工厂模式)。 310 | 311 | [组合模式Demo(公司组织架构案例)](https://github.com/qingfengiOS/QFComposite.git) 312 | 313 | ### 十三、迭代器模式 314 | 1. 何为迭代器模式 315 | 定义:**提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露独享的内部表示。** 316 | 317 | 迭代器提供一种顺序访问聚合对象(集合)中元素的方法,而无需暴露结构的底层表示和细节。遍历集合中元素的职能从集合本身转移到迭代器对象。迭代器定义了一个用于访问集合元素并记录当前元素的接口。不同迭代器可以执行不同的遍历策略。 318 | 319 | 2. 何时使用迭代器模式 320 | - 需要访问组合对象的内容,而又不暴露内部表示。 321 | - 需要通过多种方式遍历组合对象。 322 | - 需要提供一个统一接口,用来遍历各种类型的组合对象。 323 | 324 | 3. Cocoa Touch中的迭代器模式 325 | - NSEnumerator 326 | 从iOS2.0开始,可以使用NSEnumerator来枚举NSArray、NSDictionary、NSSet对象中的元素。 327 | - 基于block的枚举 328 | 在iOS4中引入了基于块的枚举(Block-Based Enumeratoration) 329 | - 快速枚举(for-in) 330 | - 内部枚举 331 | NSArry有个实例方法```-makeObjectsPerformSelector:```,它允许客户端向数组中每个元素发送一个消息,让其每个元素执行指定的方法。 332 | 333 | [迭代器模式Demo(乘客买票案例)](https://github.com/qingfengiOS/QFIterator) 334 | 335 | ## 行为扩展 336 | ### 十四、访问者模式 337 | 1. 何为访问者模式 338 | 定义:**表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。** 339 | 340 | 2. 何时使用访问者模式 341 | - 一个复杂的对象结构包含很多其他对象,他们有不同的接口,这个对象的实施依赖于其具体类型的操作。 342 | - 需要对一个组合结构体中的对象进行很多不相关的操作,但是不想让这些操作“污染”这些对象的类,可以将相关的操作集中起来,定义在一个访问者类中,并在需要在访问者中定义的操作时使用它。 343 | - 定义复杂结构的类很少修改,单经常需要向其添加新的操作。 344 | 345 | 3. 总结 346 | 访问者模式是扩展组合结构功能的一种强有力的方式。如果组合结构具有精心设计的基本操作,而且结构相对稳定就可以使用访问者模式。 347 | 348 | [访问者模式Demo(男人女人案例)](https://github.com/qingfengiOS/QFVisitor.git) 349 | 350 | 351 | ### 十五、装饰者模式 352 | 353 | 1. 何为装饰着模式 354 | 装饰者模式:**动态地给一个对象添加一些额外的职责。就扩展功能来说,装饰者模式相比生成子类更加灵活。** 355 | 356 | Component是定义一个对象接口,可以给这些对象动态地添加职责。ConcreteComponent是定义了一个具体的对象,也可以给这个对象添加一些职责。Decorator,装饰者抽象类,继承了Component,从外类来扩展Component类的功能,但对于COmponent来说,是无需知道Decorator的存在的。至于ConcreteDecorator就是具体的装饰对象,起到给Component添加职责的功能。 357 | 358 | 在日常开发过程中,应该减少类继承的使用,过多地使用类的继承会导致类数目过于庞大而变得难以维护,而使用组合可以让我们的系统更具弹性,更加容易修改和扩展。 359 | 360 | 2. 何时使用装饰者模式 361 | - 想要在不影响其他对象的情况下,以动态,透明的方式给单个对象添加职责。 362 | - 想要扩展一个类的行为,却做不到。类定义可能被隐藏,无法进行子类化;或者,对类的每个行为的扩展,为支持各种功能组合,将产生大量的子类。 363 | - 对类的职责的扩展是可选的。 364 | 365 | 3. 改变对象的“外表”和“内容” 366 | 367 | | "外表"变更(装饰者) | “内容”变更(策略) | 368 | | :---: | :---: | 369 | | 从外部变更 | 从内部变更 | 370 | | 每个节点不知道变更 | 每个节点知道一组预定义的变更方式 | 371 | [装饰者模式Demo(给图片加滤镜)](https://github.com/qingfengiOS/QFDecorator.git) 372 | 373 | ### 十六、责任链模式 374 | 1. 何为责任链模式 375 | 376 | 定义:**使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间发生耦合。此模式将这些对象连城一天链,并且沿这条链传递请求,直到一个对象处理它为止。** 377 | 378 | 使用责任链模式可以随意地增加或修改处理一个请求的结构,增强了对对象指派职责的灵活性。但是可能一个请求到了责任链的末端都得不到处理,或者因为没有正确地配置而得不到处理,需要考虑全面。 379 | 380 | 2. 何时使用责任链模式 381 | - 有多个对象可以处理请求,而处理程序只有在运行时才能确定。 382 | - 向一组对象发出请求,而不想显示指定处理请求的特定处理程序。 383 | 384 | 3. 总结 385 | 386 | 责任链模式能很好地解决大量分支判断,有效降低了客户端调用的逻辑复杂度。以上的3个设计模式,都是在扩展对象行为的同时,对对象进行最少修改甚至不修改。对越来越复杂的系统扩展具有极大的借鉴意义。 387 | 388 | [责任链模式Demo(加薪案例)](https://github.com/qingfengiOS/QFResponsbilityChain) 389 | 390 | ## 算法封装 391 | ### 十七、模板方法模式 392 | 1. 何为模板方法模式 393 | 394 | 定义:**定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使子类可以重新定义算法的某些y特定步骤而不改变改算法的结构。** 395 | 396 | 其基本思想是在抽象类的一个方法中定义“标准”算法。这个方法的实现由子类重载实现。这个方法被称为“模板”,因为方法定义的算法缺少一些特定的操作。子类重载基本操作以提供独特操作供模板方法使用。 397 | 398 | 2. 何时使用模板方法模式 399 | 400 | - 需要一次性实现算法的不变部分,并将可变的行为留给子类实现。 401 | - 子类的共同行为应该被提取出来,放到公共类中, 以避免代码重复。现有代码的差别应该被分离为新的操作。然后用一个调用这些新操作的模板方法来替换这些不同的代码。 402 | - 需要控制子类的扩展,可以定义一个在特定点调用“钩子(hook)”操作的模板方法。子类可以通过对钩子的操作实现在这些点扩展功能。 403 | 404 | 3. 模板方法 vs 适配器 405 | 406 | | 模板方法 | 适配器(委托)模式 | 407 | | :---: | :---: | 408 | | 父类定义一个一般算法,但缺少某些特定/可选的信息或者算法,它通过这些缺少的信息或算法起到一个算法的“食谱”作用 | 适配器与预先定好的委托接口一起定义一个特定的算法 | 409 | | 缺少的信息有子类通过继承实现 | 特定算法由任何对象通过组合来提供 | 410 | 411 | [模板方法模式Demo(制作多种三明治)](https://github.com/qingfengiOS/QFTemplate.git) 412 | 413 | ### 十八、策略模式 414 | 415 | 1. 何为策略模式 416 | **策略模式定义了算法家族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化不会影响到使用算法的客户端** 417 | 418 | 策略模式中的一个关键角色是策略类,它为所有支持的相关算法声明了一个共同接口。另外,还有使用策略接口来实现先关算法的具体策略类。场景(Context)类的对象配置有一个具体策略对象的实例,场景对象使用策略接口调用由具体策略类定义的算法。 419 | 420 | 2. 何时使用策略模式 421 | - 一个类在其操作中使用多个条件语句来定义许多行为。我们可以把相关的条件分支移到他们自己的策略类中。 422 | - 需要算法的各种变体。 423 | - 需要把复杂的、与算法相关的数据结构暴露给客户端。 424 | 3. MVC中的策略模式 425 | 模型-视图-视图控制器(MVC)模式中,控制器决定视图对模型数据的显示内容和时机。视图本身知道如何绘图,但需要控制器告诉他要显示的内容。一个视图如果与不同的控制器合作,数据的输出格式可能一样,但数据的类型和格式可能随不同控制器的不同输出而不同。因此,这种情况下的控制器是视图的策略。控制器与视图之间是一种基于策略模式的关系。 426 | 427 | 4. 总结 428 | 策略模式和装饰者模式有些类似。装饰器从外部扩展对象的行为,而各种策略则被封装在对象之中。所以说装饰器改变对象的“外表”而策略改变对象的“内容”。 429 | 430 | [策略模式Demo(商场打折案例)](https://github.com/qingfengiOS/QFStrategy.git) 431 | 432 | ### 十九、命令模式 433 | 1. 何为命令模式 434 | 435 | 命令对象封装了如何对目标执行指令的信息,因此,客户端或调用者不必了解目标的任何细节,却仍可以对它执行任何已知的操作。通过吧请求封装成对象,客户端可以把它参数化并置入队列或者日志中,也能支持撤销的操作。命令对象将一个或多个动作绑定到特定的接收器。命令模式消除了作为对象的动作和执行它的接收器之间的绑定。 436 | 437 | 定义:**将请求封装为一个对象,从而可用不同的请求对客户端进行参数化,队请求排队或记录请求日志,以支持可撤销操作。命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分割开** 438 | 439 | 2. 何时使用命令模式 440 | 441 | - 想让应用程序支持撤销和恢复。 442 | - 想用对象参数化一个动作以执行操作,并用不同的命令对象来代替回调函数。 443 | - 想要在不同时刻对请求进行指定、排列和执行。 444 | - 想记录修改日志,这样在系统故障时,这些修改刻在后来重做一遍。 445 | - 想让系统支持事务,事务封装了对数据的一些列修改。事务可以建模为命令对象。 446 | 447 | 3. 命令模式的优点 448 | 1. 比较容易地设计一个命令队列(waiter的commandList数组) 449 | 2. 在需要的情况下,可以很容易地将命令记录日志(在Waiter的setOrder记录) 450 | 3. 允许接收方命令的一方决定是否否决该命令(Cooker类可以通知Waiter无货源) 451 | 4. 可以对请求进行撤销、修改和重做(可以修改命令的数量) 452 | 5. 由于新加的具体命令类不影响其他类,因此增加新的具体命令类很容易 453 | 454 | [命令模式Demo(烧烤店的订单操作)](https://github.com/qingfengiOS/QFCommand) 455 | 456 | ## 性能与对象访问 457 | ### 二十、享元模式 458 | 459 | 1. 何为享元模式 460 | 461 | 定义:**运用共享技术有效地支持大量细粒度的对象** 462 | 463 | 实现享元模式需要两个关键的组件,通常是可共享的享元对象和保存他们的池。某种中央对象维护这个池,并从它返回适当的对象实例。工厂是这一角色的理想候选,它通过一个工厂方法返回各种类型的具体享元对象。 464 | 465 | 享元模式可以避免大量非常相似类的开销。在程序设计中,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例除了几个参数外基本相同,有时能够大幅度地减少需要实例化类的数量。如果能把那些参数移到类实例的外面,在方法调用时将他们传递进来,就可以通过共享大幅度地减少实例的数目。 466 | 467 | 2. 何时使用享元模式 468 | 469 | - 应用程序使用很多对象 470 | - 在内存中保存对象会影响性能 471 | - 对象的多数特有状态可以放到外部而轻量化 472 | - 移除了外在状态后,可以用较少的共享对象替换原来的那组对象 473 | - 应用程序不依赖对象标识,因为共享对象不能提供唯一标识 474 | 475 | ### 二十一、代理模式 476 | 477 | 1. 何为代理模式 478 | 479 | 定义:**为其他对象提供一种代理以控制对这个对象的访问。** 480 | 481 | 代理模式的思想是使用一个基本跟实体对象行为相同的代理,客户端可以”透明地“使用代理,即不必知道所面对的只是一个代理而不是实体对象。在iOS中,使用代理来解耦合。 482 | >使用代理把View层的事件传递到Controller中 483 | 484 | >把tableView的delegate和dataSource实现到一个单独的Model或者ViewModel中 485 | 486 | 2. 代理的分类 487 | 488 | - 远程代理(remote proxy) 489 | 为位于不同地址空间或者网络上的对象提供本地代表 490 | - 虚拟代理(virtual proxy) 491 | 需要根据需要创建重型对象 492 | - 保护代理(protection proxy) 493 | 根据访问权限控制对原对象的访问 494 | - 智能引用代理(samrt-reference proxy) 495 | 通过对真正对象的引用进行技术来管理内存,也用于锁定真正对象,让其他对象不能对其进行修改。 496 | 497 | ## 八、对象状态 498 | ### 二十二、备忘录模式 499 | 500 | 1. 何为备忘录模式 501 | 502 | 定义:**在不破坏封装性的前提下,捕捉一个对象的内部状态,并在改对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。** 503 | 504 | 备忘录模式有是三个角色:原发器(originator)、备忘录(memento)、 管理者(caretaker) 505 | 506 | - 原发器(originator) 507 | 负责创建一个备忘录Memento,用来记录当前时刻的内部状态,并可使用备忘录恢复内部状态。originator可根据需要决定Memento存储originator的内部状态 508 | - 备忘录(memento) 509 | 负责存储originator对象的内部状态,并防止originator以外的对象访问备忘录。 510 | - 管理者(caretaker) 511 | 负责保存好备忘录Memento,不能对备忘录的内容进行检查或者修改。 512 | 513 | 2. 合适使用备忘录模式 514 | - 需要保存一个对象(或某部分)在某一时刻的状态,提供以后恢复到这个时刻的状态。 515 | - 用于获取状态的接口会暴露实现的细节,需要对外隐藏实现细节。 516 | 517 | [备忘录模式Demo(游戏进度保存于恢复)](https://github.com/qingfengiOS/QFMemento) 518 | 519 | -------------------------------------------------------------------------------- /iOS静态库与动态库的使用.md: -------------------------------------------------------------------------------- 1 | ##前言 2 | 3 | ###1.静态库和动态库有什么异同? 4 | 5 | 静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。利用静态函数库编译成的文件比较大,因为整个 函数库的所有数据都会被整合进目标代码中,他的优点就显而易见了,即编译后的执行程序不需要外部的函数库支持,因为所有使用的函数都已经被编译进去了。当然这也会成为他的缺点,因为如果静态函数库改变了,那么你的程序必须重新编译。 6 | 7 | 动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。 8 | 9 | 静态库和动态库都是闭源库,只能拿来满足某个功能的使用,不会暴露内部具体的代码信息,而从github上下载的第三方库大多是开源库 10 | 11 | 静态库和动态库都是由*.o目标文件生成 12 | 13 | 使用静态库的好处: 14 | 15 | - 模块化,分工合作 16 | - 避免少量改动经常导致大量的重复编译连接 17 | - 也可以重用,注意不是共享使用 18 | 19 | 动态库使用有如下好处: 20 | 21 | - 可以将最终可执行文件体积缩小 22 | - 多个应用程序共享内存中得同一份库文件,节省资源 23 | - 可以不重新编译连接可执行程序的前提下,更新动态库文件达到更新应用程序的目的。 24 | - 将整个应用程序分模块,团队合作,进行分工,影响比较小。 25 | 26 | 其实动态库应该叫共享库,那么从这个意义上来说,苹果禁止iOS开发中使用动态库就可以理解了: 因为在现在的iPhone,iPodTouch,iPad上面程序都是单进程的,也就是某一时刻只有一个进程在运行,那么你写个共享库 27 | 28 | --共享给谁?(你使用的时候只有你一个应用程序存在,其他的应该被挂起了,即便是可以同时多个进程运行,别人能使用你的共享库里的东西吗?你这个是给你自己的程序定制的。) 29 | --目前苹果的AppStore不支持模块更新,无法更新某个单独文件(除非自己写一个更新机制:有自己的服务端放置最新动态库文件) 30 | 至于苹果为啥禁止ios开发使用动态库我就猜到上面俩原因 31 | 32 | 2 这两种库都有哪些文件格式? 33 | 34 | 静态库:.a和.framework (windows:.lib , linux: .a) 35 | 36 | 动态库:.dylib和.framework(系统提供给我们的framework都是动态库!)(windows:.dll , linux: .so) 37 | 38 | 注意:两者都有framework的格式,但是当你创建一个framework文件时,系统默认是动态库的格式,如果想做成静态库,需要在buildSetting中将Mach-O Type选项设置为Static Library就行了! 39 | 40 | 3..a文件和.framework文件的区别? 41 | 42 | - .a是一个纯二进制文件,不能直接拿来使用,需要配合头文件、资源文件一起使用。 43 | 44 | - 将静态库打包的时候,只能打包代码资源,图片、本地json文件和xib等资源文件无法打包进去,使用.a静态库的时候需要三个组成部分:.a文件+需要暴露的头文件+资源文件; 45 | 46 | - .framework中除了有二进制文件之外还有资源文件,可以拿来直接使用。 47 | 48 | 4.制作静态库需要注意的几点: 49 | 50 | - 注意理解:无论是.a静态库还.framework静态库,我们需要的都是二进制文件+.h+其它资源文件的形式,不同的是,.a本身就是二进制文件,需要我们自己配上.h和其它文件才能使用,而.framework本身已经包含了.h和其它文件,可以直接使用。 51 | - 图片资源的处理:两种静态库,一般都是把图片文件单独的放在一个.bundle文件中,一般.bundle的名字和.a或.framework的名字相同。.bundle文件很好弄,新建一个文件夹,把它改名为.bundle就可以了,右键,显示包内容可以向其中添加图片资源。 52 | category是我们实际开发项目中经常用到的,把category打成静态库是没有问题的,但是在用这个静态库的工程中,调用category中的方法时会有找不到该方法的运行时错误(selector not recognized),解决办法是:在使用静态库的工程中配置other linkerflags的值为-ObjC。 53 | - 如果一个静态库很复杂,需要暴露的.h比较多的话,就可以在静态库的内部创建一个.h文件(一般这个.h文件的名字和静态库的名字相同),然后把所有需要暴露出来的.h文件都集中放在这个.h文件中,而那些原本需要暴露的.h都不需要再暴露了,只需要把.h暴露出来就可以了。 54 | 55 | 5.framework动态库的主要作用: 56 | 57 | framework本来是苹果专属的内部提供的动态库文件格式,但是自从2014年WWDC之后,开发者也可以自定义创建framework实现动态更新(绕过apple store审核,从服务器发布更新版本)的功能,这与苹果限定的上架的app必须经过apple store的审核制度是冲突的,所以含有自定义的framework的app是无法在商店上架的,但是如果开发的是企业内部应用,就可以考虑尝试使用动态更新技术来将多个独立的app或者功能模块集成在一个app上面!(笔者开发的就是企业内部使用的app,我们将企业官网中的板块开发成4个独立的app,然后将其改造为framework文件集成在一款平台级的app当中进行使用) 58 | 59 | 目前 iOS 上的动态更新方案主要有以下 4 种: 60 | 61 | HTML 5 62 | lua(wax)hotpatch 63 | react native 64 | framework 65 | 前面三种都是通过在应用内搭建一个运行环境来实现动态更新(HTML 5 是原生支持),在用户体验、与系统交互上有一定的限制,对开发者的要求也更高(至少得熟悉 lua 或者 js)。 66 | 67 | 使用 framework 的方式来更新可以不依赖第三方库,使用原生的 OC/Swift 来开发,体验更好,开发成本也更低。 68 | 69 | 由于 Apple 不希望开发者绕过 App Store 来更新 app,因此只有对于不需要上架的应用,才能以 framework 的方式实现 app 的更新。 70 | 71 | ##主要思路 72 | 73 | 将 app 中的某个模块(比如一个 tab)的内容独立成一个 framework 的形式动态加载,在 app 的 main bundle 中,当 app 启动时从服务器上下载新版本的 framework 并加载即可达到动态更新的目的。 74 | 75 | ##实战 76 | 77 | 创建一个普通工程 DynamicUpdateDemo,其包含一个 framework 子工程 Module。也可以将 Module 创建为独立的工程,创建工程的过程不再赘述。 78 | 79 | ##依赖 80 | 81 | 在主工程的 Build Phases > Target Dependencies 中添加 Module,并且添加一个 New Copy Files Phase。 82 | 83 | ![](https://upload-images.jianshu.io/upload_images/2598795-b63ebaf0eefc348c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 84 | 85 | 86 | 这样,打包时会将生成的 Module.framework 添加到 main bundle 的根目录下。 87 | 88 | ##使用动态库 89 | 90 | 通过以下方式将刚生成的framework添加到工程中: 91 | 92 | Targets-->Build Phases-->Link Binary With Libraries 93 | 同时设置将framework作为资源文件拷贝到Bundle中: 94 | 95 | Targets-->Build Phases-->Copy Bundle Resources 96 | 如图所示: 97 | 98 | ![](https://upload-images.jianshu.io/upload_images/2598795-3d8b89d72b4784a3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 99 | 100 | 101 | 仅仅这样做是不够的,还需要为动态库添加链接依赖。 102 | 103 | ###自动链接动态库 104 | 105 | 添加完动态库后,如果希望动态库在软件启动时自动链接,可以通过以下方式设置动态库依赖路径: 106 | 107 | Targets-->Build Setting-->Linking-->Runpath Search Paths 108 | 由于向工程中添加动态库时,将动态库设置了Copy Bundle Resources,因此就可以将Runpath Search Paths路径依赖设置为main bundle,即沙盒中的FrameworkDemo.app目录,向Runpath Search Paths中添加下述内容: 109 | 110 | @executable_path/ 111 | 如图所示: 112 | 113 | ![](https://upload-images.jianshu.io/upload_images/2598795-8500ac56b3e51c92.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 114 | 115 | 116 | 其中的@executable_path/表示可执行文件所在路径,即沙盒中的.app目录,注意不要漏掉最后的/。 117 | 118 | 如果你将动态库放到了沙盒中的其他目录,只需要添加对应路径的依赖就可以了。 119 | 120 | ###需要的时候链接动态库 121 | 动态库的另一个重要特性就是即插即用性,我们可以选择在需要的时候再加载动态库。 122 | 123 | - 更改设置 124 | 如果不希望在软件一启动就加载动态库,需要将 125 | 126 | Targets-->Build Phases-->Link Binary With Libraries 127 | 中Dylib.framework对应的Status由默认的Required改成Optional;或者更干脆的,将Dylib.framework从Link Binary With Libraries列表中删除即可。 128 | 129 | - 使用dlopen加载动态库 130 | 以Dylib.framework为例,动态库中真正的可执行代码为Dylib.framework/Dylib文件,因此使用dlopen时如果仅仅指定加载动态库的路径为Dylib.framework是没法成功加载的。 131 | 132 | 示例代码如下: 133 | 134 | ``` 135 | - (IBAction)onDlopenLoadAtPathAction1:(id)sender 136 | { 137 | NSString *documentsPath = [NSString stringWithFormat:@"%@/Documents/Dylib.framework/Dylib",NSHomeDirectory()]; 138 | [self dlopenLoadDylibWithPath:documentsPath]; 139 | } 140 | 141 | - (void)dlopenLoadDylibWithPath:(NSString *)path 142 | { 143 | libHandle = NULL; 144 | libHandle = dlopen([path cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW); 145 | if (libHandle == NULL) { 146 | char *error = dlerror(); 147 | NSLog(@"dlopen error: %s", error); 148 | } else { 149 | NSLog(@"dlopen load framework success."); 150 | } 151 | } 152 | 153 | ``` 154 | 以dlopen方式使用动态库应该不能通过苹果审核。 155 | 156 | - 使用NSBundle加载动态库 157 | 也可以使用NSBundle来加载动态库,实现代码如下: 158 | 159 | ``` 160 | - (IBAction)onBundleLoadAtPathAction1:(id)sender 161 | { 162 | NSString *documentsPath = [NSString stringWithFormat:@"%@/Documents/Dylib.framework",NSHomeDirectory()]; 163 | [self bundleLoadDylibWithPath:documentsPath]; 164 | } 165 | 166 | - (void)bundleLoadDylibWithPath:(NSString *)path 167 | { 168 | _libPath = path; 169 | NSError *err = nil; 170 | NSBundle *bundle = [NSBundle bundleWithPath:path]; 171 | if ([bundle loadAndReturnError:&err]) { 172 | NSLog(@"bundle load framework success."); 173 | } else { 174 | NSLog(@"bundle load framework err:%@",err); 175 | } 176 | } 177 | 178 | ``` 179 | 180 | ###使用动态库中代码 181 | 182 | 通过上述任一一种方式加载的动态库后,就可以使用动态库中的代码文件了,以Dylib.framework中的Person类的使用为例: 183 | 184 | 185 | ``` 186 | - (IBAction)onTriggerButtonAction:(id)sender 187 | { 188 | Class rootClass = NSClassFromString(@"Person"); 189 | if (rootClass) { 190 | id object = [[rootClass alloc] init]; 191 | [(Person *)object run]; 192 | } 193 | } 194 | 195 | ``` 196 | 197 | 注意,如果直接通过下属方式初始化Person类是不成功的: 198 | 199 | ``` 200 | - (IBAction)onTriggerButtonAction:(id)sender 201 | { 202 | Person *object = [[Person alloc] init]; 203 | if (object) { 204 | [object run]; 205 | } 206 | } 207 | 208 | ``` 209 | ##监测动态库的加载和移除 210 | 211 | 我们可以通过下述方式,为动态库的加载和移除添加监听回调: 212 | 213 | ``` 214 | + (void)load 215 | { 216 | _dyld_register_func_for_add_image(&image_added); 217 | _dyld_register_func_for_remove_image(&image_removed); 218 | } 219 | 220 | ``` 221 | github上有一个完整的[示例代码](https://github.com/ddeville/ImageLogger), 222 | 223 | 从这里看出,原来就算空白工程软件启动的时候也会加载多达一百二十多个动态库,如果这些都是静态库,那该有多可怕!! 224 | 225 | ##加载 226 | 227 | 主要的代码如下: 228 | 229 | ``` 230 | - (UIViewController *)loadFrameworkNamed:(NSString *)bundleName { 231 | NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); 232 | NSString *documentDirectory = nil; 233 | if ([paths count] != 0) { 234 | documentDirectory = [paths objectAtIndex:0]; 235 | } 236 | 237 | NSFileManager *manager = [NSFileManager defaultManager]; 238 | NSString *bundlePath = [documentDirectory stringByAppendingPathComponent:[bundleName stringByAppendingString:@".framework"]]; 239 | 240 | // Check if new bundle exists 241 | if (![manager fileExistsAtPath:bundlePath]) { 242 | NSLog(@"No framework update"); 243 | bundlePath = [[NSBundle mainBundle] 244 | pathForResource:bundleName ofType:@"framework"]; 245 | 246 | // Check if default bundle exists 247 | if (![manager fileExistsAtPath:bundlePath]) { 248 | UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Oooops" message:@"Framework not found" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil]; 249 | [alertView show]; 250 | return nil; 251 | } 252 | } 253 | 254 | // Load bundle 255 | NSError *error = nil; 256 | NSBundle *frameworkBundle = [NSBundle bundleWithPath:bundlePath]; 257 | if (frameworkBundle && [frameworkBundle loadAndReturnError:&error]) { 258 | NSLog(@"Load framework successfully"); 259 | }else { 260 | NSLog(@"Failed to load framework with err: %@",error); 261 | return nil; 262 | } 263 | 264 | // Load class 265 | Class PublicAPIClass = NSClassFromString(@"PublicAPI"); 266 | if (!PublicAPIClass) { 267 | NSLog(@"Unable to load class"); 268 | return nil; 269 | } 270 | 271 | NSObject *publicAPIObject = [PublicAPIClass new]; 272 | return [publicAPIObject performSelector:@selector(mainViewController)]; 273 | } 274 | ``` 275 | 代码先尝试在 Document 目录下寻找更新后的 framework,如果没有找到,再在 main bundle 中寻找默认的 framework。 其中的关键是利用 OC 的动态特性 NSClassFromString 和 performSelector 加载 framework 的类并且执行其方法。 276 | 277 | ##framework 和 host 工程资源共用 278 | 279 | ###第方三库 280 | 281 | Class XXX is implemented in both XXX and XXX. One of the two will be used. Which one is undefined. 282 | 这是当 framework 工程和 host 工程链接了相同的第三方库或者类造成的。 283 | 284 | 为了让打出的 framework 中不包含 host 工程中已包含的三方库(如 cocoapods 工程编译出的 .a 文件),可以这样: 285 | 286 | 删除 Build Phases > Link Binary With Libraries 中的内容(如有)。此时编译会提示三方库中包含的符号找不到。 287 | 288 | 在 framework 的 Build Settings > Other Linker Flags 添加 -undefined dynamic_lookup。必须保证 host 工程编译出的二进制文件中包含这些符号。 289 | 290 | ###类文件 291 | 292 | 尝试过在 framework 中引用 host 工程中已有的文件,通过 Build Settings > Header Search Paths 中添加相应的目录,Xcode 在编译的时候可以成功(因为添加了 -undefined dynamic_lookup),并且 Debug 版本是可以正常运行的,但是 Release 版本动态加载时会提示找不到符号: 293 | 294 | ``` 295 | Error Domain=NSCocoaErrorDomain Code=3588 "The bundle “YourFramework” couldn’t be loaded." (dlopen(/var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework, 265): Symbol not found: _OBJC_CLASS_$_BorderedView 296 | Referenced from: /var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework 297 | Expected in: flat namespace 298 | in /var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework) UserInfo=0x174276900 {NSLocalizedFailureReason=The bundle couldn’t be loaded., NSLocalizedRecoverySuggestion=Try reinstalling the bundle., NSFilePath=/var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework, NSDebugDescription=dlopen(/var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework, 265): Symbol not found: _OBJC_CLASS_$_BorderedView 299 | Referenced from: /var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework 300 | Expected in: flat namespace 301 | in /var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework/YourFramework, NSBundlePath=/var/mobile/Containers/Bundle/Application/5691FB75-408A-4D9A-9347-BC7B90D343C1/YourApp.app/YourFramework.framework, NSLocalizedDescription=The bundle “YourFramework” couldn’t be loaded. 302 | 303 | ``` 304 | 因为 Debug 版本暴露了所有自定义类的符号以便于调试,因此你的 framework 可以找到相应的符号,而 Release 版本则不会。 305 | 306 | 目前能想到的方法只有将相同的文件拷贝一份到 framework 工程里,并且更改类名。 307 | 308 | ###访问 framework 中的图片 309 | 310 | 在 storyboard/xib 中可以直接访问图片,代码中访问的方法如下: 311 | 312 | ``` 313 | UIImage *image = [UIImage imageNamed:@"YourFramework.framework/imageName"] 314 | ``` 315 | 注意:使用代码方式访问的图片不可以放在 xcassets 中,否则得到的将是 nil。并且文件名必须以 @2x/@3x 结尾,大小写敏感。因为 imageNamed: 默认在 main bundle 中查找图片。 316 | 317 | ###常见错误 318 | 319 | ###Architecture 320 | 321 | ``` 322 | dlopen(/path/to/framework, 9): no suitable image found. Did find: 323 | /path/to/framework: mach-o, but wrong architecture 324 | ``` 325 | 这是说 framework 不支持当前机器的架构。 通过 326 | 327 | ``` 328 | lipo -info /path/to/MyFramework.framework/MyFramework 329 | ``` 330 | 可以查看 framework 支持的 CPU 架构。 331 | 332 | 碰到这种错误,一般是因为编译 framework 的时候,scheme 选择的是模拟器,应该选择iOS Device。 333 | 334 | 此外,如果没有选择iOS Device,编译完成后,Products 目录下的 .framework 文件名会一直是红色,只有在 Derived Data 目录下才能找到编译生成的 .framework 文件。 335 | 336 | ###关于other linker flag 337 | 338 | 使用静态库或者动态库的时候极易发生链接错误,而且大多发生在加载framework中category的情况!根本原因在于Objective-C的链接器并不会为每个方法建立符号表,而是仅仅为类建立了符号表。这样的话,如果静态库中定义了已存在的一个类的分类,链接器就会以为这个类已经存在,不会把分类和核心类的代码合起来。这样的话,在最后的可执行文件中,就会缺少分类里的代码,这样函数调用就失败了。常见的设置方法就是在other linker flag中添加一个语句:-all_load,但是这样也并不是万能的,具体解析请参考链接:[链接](http://my.oschina.net/u/728866/blog/194741) 339 | 340 | 注意:当flag里面添加了注释却还是无法使用的时候,可能报flag与bitcode冲突的问题尤其是第三方库可能和bitcode冲突),这样的话就需要将bitcode设置为NO! 341 | 342 | bitcode的具体作用不做详谈,可参考:[bitcode](http://www.jianshu.com/p/3e1b4e2d06c6) 343 | 344 | ###签名 345 | 346 | 系统在加载动态库时,会检查 framework 的签名,签名中必须包含 TeamIdentifier 并且 framework 和 host app 的 TeamIdentifier 必须一致。 347 | 348 | 如果不一致,否则会报下面的错误: 349 | 350 | ``` 351 | Error loading /path/to/framework: dlopen(/path/to/framework, 265): no suitable image found. Did find: 352 | /path/to/framework: mmap() error 1 353 | ``` 354 | 此外,如果用来打包的证书是 iOS 8 发布之前生成的,则打出的包验证的时候会没有 TeamIdentifier 这一项。这时在加载 framework 的时候会报下面的错误: 355 | 356 | ``` 357 | [deny-mmap] mapped file has no team identifier and is not a platform binary: 358 | /private/var/mobile/Containers/Bundle/Application/5D8FB2F7-1083-4564-94B2-0CB7DC75C9D1/YourAppNameHere.app/Frameworks/YourFramework.framework/YourFramework 359 | ``` 360 | 可以通过 codesign 命令来验证。 361 | 362 | codesign -dv /path/to/YourApp.app 363 | 如果证书太旧,输出的结果如下: 364 | 365 | ``` 366 | Executable=/path/to/YourApp.app/YourApp 367 | Identifier=com.company.yourapp 368 | Format=bundle with Mach-O thin (armv7) 369 | CodeDirectory v=20100 size=221748 flags=0x0(none) hashes=11079+5 location=embedded 370 | Signature size=4321 371 | Signed Time=2015年10月21日 上午10:18:37 372 | Info.plist entries=42 373 | TeamIdentifier=not set 374 | Sealed Resources version=2 rules=12 files=2451 375 | Internal requirements count=1 size=188 376 | 377 | ``` 378 | 注意其中的 TeamIdentifier=not set。 379 | 380 | 采用 swift 加载 libswiftCore.dylib 这个动态库的时候也会遇到这个问题,对此Apple 官方的解释是: 381 | 382 | ``` 383 | To correct this problem, you will need to sign your app using code signing certificates with the Subject Organizational Unit (OU) set to your Team ID. All Enterprise and standard iOS developer certificates that are created after iOS 8 was released have the new Team ID field in the proper place to allow Swift language apps to run. 384 | 385 | If you are an in-house Enterprise developer you will need to be careful that you do not revoke a distribution certificate that was used to sign an app any one of your Enterprise employees is still using as any apps that were signed with that enterprise distribution certificate will stop working immediately. 386 | ``` 387 | 只能通过重新生成证书来解决这个问题。但是 revoke 旧的证书会使所有用户已经安装的,用该证书打包的 app 无法运行。 388 | 389 | 等等,我们就跪在这里了吗?! 390 | 391 | 现在企业证书的有效期是三年,当证书过期时,其打包的应用就不能运行,那企业应用怎么来更替证书呢? 392 | 393 | __Apple 为每个账号提供了两个证书,这两个证书可以同时生效,这样在正在使用的证书过期之前,可以使用另外一个证书打包发布,让用户升级到新版本。__ 394 | 395 | 也就是说,可以使用另外一个证书来打包应用,并且可以覆盖安装使用旧证书打包的应用。详情可以看 Apple 文档。 396 | 397 | ###深入理解iPhone静态库 398 | 399 | 在实际的编程过程中,通常会把一些公用函数制成函数库,供其它程序使用,一则提搞了代码的复用;二则提搞了核心技术的保密程度。所以在实际的项目开发中,经常会使用到函数库,函数库分为静态库和动态库两种。和多数人所熟悉的动态语言和静态语言一样,这里的所谓静态和动态是相对编译期和运行期的:静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要改静态库;而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。 400 | 401 | ###深入理解framework(框架,相当于静态框架,不是动态库) 402 | 403 | 打包framework还是一个比较重要的功能,可以用来做一下事情: 404 | 405 | 封装功能模块,比如有比较成熟的功能模块封装成一个包,然后以后自己或其他同事用起来比较方便。 406 | 封装项目,有时候会遇到这个情况,就是一家公司找了两个开发公司做两个项目,然后要求他们的项目中的一个嵌套进另一个项目,此时也可以把呗嵌套的项目打包成framework放进去,这样比较方便。 407 | ###我们为什么需要框架(Framework)? 408 | 409 | 要想用一种开发者友好的方式共享库是很麻烦的。你不仅仅需要包含库本身,还要加入所有的头文件,资源等等。 410 | 411 | 苹果解决这个问题的方式是框架(framework)。基本上,这是含有固定结构并包含了引用该库时所必需的所有东西的文件夹。不幸的是,iOS禁止所有的动态库。同时,苹果也从Xcode中移除了创建静态iOS框架的功能。 412 | 413 | Xcode仍然可以支持创建框架的功能,重启这个功能,我们需要对Xcode做一些小小的改动。 414 | 415 | 把代码封装在静态框架是被app store所允许的。尽管形式不同,本质上它仍然是一种静态库。 416 | 417 | ###框架(Framework)的类别 418 | 419 | 大部分框架都是动态链接库的形式。因为只有苹果才能在iOS设备上安装动态库,所以我们无法创建这种类型的框架。 420 | 421 | 静态链接库和动态库一样,只不过它是在编译时链接二进制代码,因此使用静态库不会有动态库那样的问题(即除了苹果谁也不能在iOS上使用动态库)。 422 | 423 | “伪”框架是通过破解Xcode的目标Bundle(使用某些脚本)来实现的。它在表面上以及使用时跟静态框架并无区别。“伪”框架项目的功能几乎和真实的框架项目没有区别(不是全部)。 424 | 425 | “嵌入”框架是静态框架的一个包装,以便Xcode能获取框架内的资源(图片、plist、nib等)。 本次发布包括了创建静态框架和“伪”框架的模板,以及二者的“嵌入”框架。 426 | 427 | __用哪一种模板?__ 428 | 429 | 本次发布有两个模板,每个模板都有“强”“弱”两个类别。你可以选择最适合一种(或者两种都安装上)。 最大的不同是Xcode不能创建“真”框架,除非你安装静态框架文件xcspec在Xcode中。这真是一个遗憾(这个文件是给项目使用的,而不是框架要用的)。 430 | 431 | __简单说,你可以这样决定用哪一种模板:__ 432 | 433 | - 如果你不想修改Xcode,那么请使用“伪”框架版本 434 | - 如果你只是想共享二进制(不是项目),两种都可以 435 | - 如果你想把框架共享给不想修改Xcode的开发者,使用“伪”框架版本 436 | - 如果你想把框架共享给修改过Xcode的开发者,使用“真”框架版本 437 | - 如果你想把框架项目作为另一个项目的依赖(通过workspace或者子项目的方式),请使用“真”框架(或者“伪”框架,使用-framework——见后) 438 | - 如果你想在你的框架项目中加入其他静态库/框架,并把它们也链接到最终结果以便不需要单独添加到用户项目中,使用“伪”框架 439 | 440 | __“伪”框架__ 441 | 442 | “伪”框架是破解的“reloacatable object file”(可重定位格式的目标文件, 保存着代码和数据,适合于和其他的目标文件连接到一起,用来创建一个可执行目标文件或者是一个可共享目标文件),它可以让Xcode编译出类似框架的东西——其实也是一个bundle。 443 | 444 | “伪框架”模板把整个过程分为几个步骤,用某些脚本去产生一个真正的静态框架(基于静态库而不是reloacatable object file)。而且,框架项目还是把它定义为wrapper.cfbundle类型,一种Xcode中的“二等公民”。 445 | 446 | 因此它跟“真”静态框架一样可以正常工作,但当存在依赖关系时就有麻烦了。 447 | 448 | __依赖问题__ 449 | 450 | 如果不使用依赖,只是创建普通的项目是没有任何问题的。但是如果使用了项目依赖(比如在workspace中),Xcode就悲剧了。当你点击“Link Binary With Libraries”下方的’+’按钮时,“伪框架”无法显示在列表中。你可以从你的“伪”框架项目的Products下面将它手动拖入,但当你编辑你的主项目时,会出现警告: 451 | ``` 452 | warning: skipping file '/somewhere/MyFramework.framework' (unexpectedfile type 'wrapper.cfbundle' in Frameworks & Libraries build phase) 453 | ``` 454 | 并伴随“伪”框架中的链接错误。 455 | 456 | 幸运的是,有个办法来解决它。你可以在”Other Linker Flags”中用”-framwork”开关手动告诉linker去使用你的框架进行链接: 457 | 458 | -framework MyFramework 459 | 460 | 警告仍然存在,但起码能正确链接了。 461 | 462 | __添加其他的库/框架__ 463 | 464 | 如果你加入其他静态(不是动态)库/框架到你的“伪”框架项目中,它们将“链接”进你最终的二进制框架文件中。在“真”框架项目中,它们是纯引用,而不是链接。 465 | 466 | 你可以在项目中仅仅包含头文件而不是静态库/框架本身的方式避免这种情况(以便编译通过)。 467 | 468 | __“真”框架__ 469 | 470 | “真”框架各个方面都符合“真”的标准。它是真正的静态框架,正如使用苹果在从Xcode中去除的那个功能所创建的一样。 471 | 472 | 为了能创建真正的静态框架项目,你必需在Xcode中安装一个xcspec文件。 473 | 474 | 如果你发布一个“真”框架项目(而不是编译),希望去编译这个框架的人必需也安装xcspec文件(使用本次发布的安装脚本),以便Xcode能理解目标类型。 475 | 476 | 注意:如果你正在发布完全编译的框架,而不是框架项目,最终用户并不需要安装任何东西。 我已经提交一个报告给苹果,希望他们在Xcode中更新这个文件,但那需要一点时间。 477 | 478 | __加其他静态库/框架__ 479 | 480 | 如果你加入其他静态(不是动态)库/框架到你的“真”框架项目,它们只会被引用,而不会象“伪”框架一样被链接到最终的二进制文件中。 481 | 482 | __从早期版本升级__ 483 | 484 | 如果你是从Mk6或者更早的版本升级,同时使用“真”静态框架,并且使用Xcode4.2.1以前的版本,请运行uninstall_legacy.sh以卸载早期用于Xcode的所有修正。然后再运行install.sh,重启Xcode。如果你使用Xcode4.3以后,只需要运行install.sh并重启Xcode。 485 | 486 | __安装__ 487 | 488 | 分别运行Real Framework目录或Fake Framework目录下的install.sh脚本进行安装(或者两个你都运行)。 489 | 490 | 重启Xcode,你将在新项目向导的Framework&Library下看到StaticiOS Framework(或者Fake Static iOS Framework)。 491 | 492 | 卸载请运行unistall.sh脚本并重启Xcode。 493 | 494 | __创建一个iOS框架项目__ 495 | 496 | 1. 创建新项目。 497 | 2. 项目类型选择Framework&Library下的Static iOS Framework(或者Fake Static iOS Framework)。 498 | 3. 选择“包含单元测试”(可选的)。 499 | 4. 在target中加入类、资源等。 500 | 5. 凡是其他项目要使用的头文件,必需声明为public。进入target的Build Phases页,展开Copy Headers项,把需要public的头文件从Project或Private部分拖拽到Public部分。 501 | 502 | __编译你的 iOS 框架__ 503 | 504 | 1. 选择指定target的scheme 505 | 2. 修改scheme的Run配置(可选)。Run配置默认使用Debug,但在准备部署的时候你可能想使用Release。 506 | 3. 编译框架(无论目标为iOS device和Simulator都会编译出相同的二进制,因此选谁都无所谓了)。 507 | 4. 从Products下选中你的framework,“show in Finder”。 508 | 509 | 在build目录下有两个文件夹:(yourframework).framework and (your framework).embeddedframework. 510 | 511 | 如果你的框架只有代码,没有资源(比如图片、脚本、xib、coredata的momd文件等),你可以把(yourframework).framework 分发给你的用户就行了。如果还包含有资源,你必需分发(your framework).embeddedframework给你的用户。 512 | 513 | 为什么需要embedded framework?因为Xcode不会查找静态框架中的资源,如果你分发(your framework).framework, 则框架中的所有资源都不会显示,也不可用。 514 | 515 | 一个embedded framework只是一个framework之外的附加的包,包括了这个框架的所有资源的符号链接。这样做的目的是让Xcode能够找到这些资源。 516 | 517 | __使用iOS 框架__ 518 | 519 | OS框架和常规的Mac OS动态框架差不多,只是它是静态链接的而已。 520 | 521 | 在你的项目中使用一个框架,只需把它拖仅你的项目中。在包含头文件时,记住使用尖括号而不是双引号括住框架名称。例如,对于框架MyFramework: 522 | 523 | import 524 | 525 | ##使用问题 526 | 527 | __Headers Not Found__ 528 | 529 | 如果Xcode找不到框架的头文件,你可能是忘记将它们声明为public了。参考“创建一个iOS框架项目”第5步。 530 | 531 | No Such Product Type 532 | 533 | 如果你没有安装iOS Universal Framework在Xcode,并企图编译一个universal框架项目(对于“真”框架,不是“假”框架),这会导致下列错误: 534 | 535 | target specifies product type 'com.apple.product-type.framework.static',but there's no such product type for the 'iphonesimulator' platform 536 | 537 | 为了编译“真”iOS静态框架,Xcode需要做一些改动,因此为了编译“真”静态框架项目,请在所有的开发环境中安装它(对于使用框架的用户不需要,只有要编译框架才需要)。 538 | 539 | __The selected run destination is not valid for this action__ 540 | 541 | 有时,Xcode出错并加载了错误的active设置。首先,请尝试重启Xcode。如果错误继续存在,Xcode产生了一个坏的项目(因为Xcode4的一个bug,任何类型的项目都会出现这个问题)。如果是这样,你需要创建一个新项目重来一遍。 542 | 543 | __链接警告__ 544 | 545 | 第一次编译框架target时,Xcdoe会在链接阶段报告找不到文件夹: ld: warning: directory not found for option'-L/Users/myself/Library/Developer/Xcode/DerivedData/MyFramework-ccahfoccjqiognaqraesrxdyqcne/Build/Products/Debug-iphoneos' 此时,可以clean并重新编译target,警告会消除。 546 | 547 | __Core Data momd not found__ 548 | 549 | 对于框架项目和应用程序项目,Xcode会以不同的方式编译momd(托管对象模型文件)。Xcode会简单地在根目录创建.mom文件,而不会创建一个.momd目录(目录中包含VersionInfo.plist和.mom文件)。 550 | 551 | 这意味着,当从一个embedded framework的model中实例化NSManagedObjectModel时,你必需使用.mom扩展名作为model的URL,而不是采用.momd扩展名。 552 | 553 | NSURL *modelURL = [[NSBundle mainBundle]URLForResource:@"MyModel" withExtension:@"mom"]; 554 | 555 | __Unknown class MyClass in Interface Builder file.__ 556 | 557 | 由于静态框架采用静态链接,linker会剔除所有它认为无用的代码。不幸的是,linker不会检查xib文件,因此如果类是在xib中引用,而没有在O-C代码中引用,linker将从最终的可执行文件中删除类。这是linker的问题,不是框架的问题(当你编译一个静态库时也会发生这个问题)。苹果内置框架不会发生这个问题,因为他们是运行时动态加载的,存在于iOS设备固件中的动态库是不可能被删除的。 有两个解决的办法: 558 | 559 | - 让框架的最终用户关闭linker的优化选项,通过在他们的项目的Other Linker Flags中添加-ObjC和-all_load。 560 | 在框架的另一个类中加一个该类的代码引用。例如,假设你有个MyTextField类,被linker剔除了。假设你还有一个MyViewController,它在xib中使用了MyTextField,MyViewController并没有被剔除。你应该这样做: 561 | 562 | 在MyTextField中: 563 | 564 | ``` 565 | -(void)forceLinkerLoad_ { 566 | } 567 | ``` 568 | 在MyViewController中: 569 | 570 | ``` 571 | +(void) initialize { 572 | [MyTextField forceLinkerLoad_]; 573 | } 574 | ``` 575 | 576 | 他们仍然需要添加-ObjC到linker设置,但不需要强制all_load了。 577 | 578 | - 这种方法需要你多做一点工作,但却让最终用户避免在使用你的框架时关闭linker优化(关闭linker优化会导致object文件膨胀)。 579 | 580 | __unexpected file type 'wrapper.cfbundle' in Frameworks &Libraries build phase__ 581 | 582 | 这个问题发生在把“假”框架项目作为workspace的依赖,或者把它当作子项目时(“真”框架项目没有这个问题)。尽管这种框架项目产生了正确的静态框架,但Xcode只能从项目文件中看出这是一个bundle,因此它在检查依赖性时发出一个警告,并在linker阶段跳过它。 583 | 584 | 你可以手动添加一个命令让linker在链接阶段能正确链接。在依赖你的静态框架的项目的OtherLinker Flags中加入: 585 | 586 | -framework MyFramework 587 | 588 | 警告仍然存在, 但不会导致链接失败。 589 | 590 | __Libraries being linked or not being linked into the finalframework__ 591 | 592 | 很不幸, “真”框架和“假”框架模板在处理引入的静态库/框架的工作方式不同的。 593 | 594 | “真”框架模板采用正常的静态库生成步骤,不会链接其他静态库/框架到最终生产物中。 595 | 596 | “假”框架模板采用“欺骗”Xcode的手段,让它认为是在编译一个可重定位格式的目标文件,在链接阶段就如同编译一个可执行文件,把所有的静态代码文件链接到最终生成物中(尽管不会检查是否确实目标代码)。为了实现象“真”框架一样的效果,你可以只包含库/框架的头文件到你的项目中,而不需要包含库/框架本身。 597 | 598 | __Unrecognized selector in (some class with a category method)__ 599 | 600 | 如果你的静态库或静态框架包含了一个模块(只在类别代码中声明,没有类实现),linker会搞不清楚,并把代码从二进制文件中剔除。因为在最终生成的文件中没有这个方法,所以当调用这个类别中定义的方法时,会报一个“unrecognizedselector”异常。 601 | 602 | 要解决这个,在包含这个类别的模块代码中加一个“假的”类。linker发现存在完整的O-C类,会将类别代码链接到模块。 603 | 604 | 我写了一个头文件 LoadableCategory.h,以减轻这个工作量: 605 | 606 | ``` 607 | import "SomeConcreteClass+MyAdditions.h" 608 | 609 | import "LoadableCategory.h" 610 | 611 | MAKE_CATEGORIES_LOADABLE(SomeConcreteClass_MyAdditions); 612 | 613 | @implementation SomeConcreteClass(MyAdditions) ... 614 | 615 | @end 616 | ``` 617 | 在使用这个框架时,仍然还需要在Build Setting的Other Linker Flags中加入-ObjC。 618 | 619 | __执行任何代码前单元测试崩溃__ 620 | 621 | 如果你在Xcode4.3中创建静态框架(或库)target时,勾选了“withunit tests”,当你试图运行单元测试时,它会崩溃: 622 | 623 | Thread 1: EXC_BAD_ACCESS (code=2, address=0x0) 0 0x00000000 --- 15 dyldbootstrap:start(...) 624 | 625 | 这是lldb中的一个bug。你可以用GDB来运行单元测试。编辑scheme,选择Test,在Info标签中将调试器Debugger从LLDB改为GDB。 626 | [原文](https://legacy.gitbook.com/book/leon_lizi/-framework-/details) -------------------------------------------------------------------------------- /公开库命令.md: -------------------------------------------------------------------------------- 1 | #公开库命令 2 | s1: 3 | pod lib create 库名 4 | 5 | s2: 6 | 把需要做成公有库的文件,拖入pods/Development Pods/库名下的class文件夹 7 | 8 | s3: 9 | 终端输入cd到Podfile所在文件夹,pod install 10 | 11 | s4: 12 | Github创建代码库,添加源git remote add origin https://gitee.com/qingfengiOS/QFTools) 13 | 14 | s5:编辑podSpec文件(版本号特别重要) 15 | 16 | s6: 17 | 执行git add .; 18 | 执行git commit -m "初始化”; 19 | git push -u origin master;(如果本地or远程库没有需要执行 20 | s7: 21 | git tag git tag 0.1.0 git push —tags 22 | 23 | s8:提交podSpec文件 24 | pod trunk register 291753906@qq.com 'qingfengiOS' --description='imac' 25 | 26 | pod trunk push --allow-warnings 27 | 28 | //搜索不到时删除search_index.json文件 重新search 29 | -------------------------------------------------------------------------------- /图解HTTP.md: -------------------------------------------------------------------------------- 1 | #图解HTTP 2 | ##第一章 了解Web及网络基础 3 | ###1.1 使用HTTP协议访问Web 4 | ###1.2 HTTP的诞生 5 | - 1.2.1 为知识共享而规划Web 6 | - 1.2.2 Web成长时代 7 | - 1.2.3 驻足不前的HTTP 8 | ###1.3 网络基础TCP/IP 9 | - 1.3.1 TCP/IP协议族 10 | - 1.3.2 TCP/IP的分层管理 11 | TCP/IP协议族里最重要的一点就是分层。TCP/IP协议族按层次分别分为以下4层:应用层、传输层、网络层、数据链路层。 12 | 13 | ###1.4 与HTTP关系最密切的协议:IP、TCP和DNS 14 | - 1.4.1 负责传输的IP协议 15 | - 1.4.2 确保可靠性的TCP协议 16 | 为了准确无误地将数据送达目标处,TCP协议采用了三次握手策略。 17 | 1:发送端首先发送一个带SYN标志的数据包给对方。 18 | 2:接收方收到后,回传一个带有SYN/ACK标志的数据包以示确认信息。 19 | 3:发送端再回传一个带ACK标志的数据包,代表”握手“结束 20 | 21 | ###1.5 负责域名解析的DNS服务 22 | DNS(Domain Name System)服务和HTTP协议一样位于应用层, 它提供域名到IP地址之间的解析服务。 23 | ###1.6 各种协议与HTTP协议的关系 24 | ###1.7 URI和URL 25 | - 1.7.1统一资源标志符(Uniform Resource Identifier) 26 | - 1.7.2 URI格式: 27 | http://user:pass@www.example.jp:80/dir/index.html?uid=1#ch1 28 | http://: 协议方案名 29 | user:pass: 登录信息(认证) 30 | www.example.jp:服务器地址 31 | 80:服务器端口号 32 | dir/index.html:带层次结构的文件路径 33 | uid:查询字符串 34 | ch1:片段标志符 35 | 36 | ##第二章 简单的HTTP协议 37 | ###2.1 HTTP协议用于客户端和服务端之间的通信 38 | ###2.2 通过请求和响应的交换达成通信 39 | ###2.3 HTTP是不保存状态的协议 40 | ###2.4 请求URI定位资源 41 | ###2.5 告知服务器意图的HTTP方法 42 | - GET:获取资源 43 | - POST:传输实体主体 44 | - PUT:传输文件 45 | - HEAD:获得报文首部 46 | - DELETE:删除文件 47 | - OPTIONS:查询支持的方法 48 | - TRACE:追踪路径 49 | - CONNECT:要求使用隧道协议连接代理 50 | 51 | ###2.6 使用方法下达命令 52 | ###2.7 持久连接节省通信量 53 | - 2.7.1 持久连接 54 | - 2.7.2 管线化 55 | 持久连接使得多数请求以管线化方式发送请求成为可能。从前发送请求后需要等待并收到响应,才能发送下一次请求。管线化技术出现后,不用等待响应亦可以直接发送下一个请求。这样就可以做到同时并行发哦送多个请求,而不需要一个接一个地等待响应了。 56 | 57 | ###2.8 使用Cookie的状态管理 58 | 59 | ##第三章 HTTP报文内部的HTTP信息 60 | ###3.1HTTP报文 61 | HTTP报文大致可分为报文首部和报文主体两块。两者由最初出现的空行(CR+LF)来划分。并不一定要有报文主体。 62 | ###3.2请求报文及响应报文的结构 63 | ###3.3编码提升传输速率 64 | - 3.3.1报文主体和实体主体的差异 65 | 报文(message):是HTTP通信中的基本单位,由8位组字节流组成,通过HTTP通信传输; 66 | 实体(entity):作为请求或响应的有效载荷数据(补充项)被传输,器内容由实体首部和实体主体组成。 67 | HTTP报文的主体用于传输请求或者响应的实体主体。 68 | 通常,报文主体等于实体主体。只有当传输过程中进行编码操作时,实体主体的内容发生变化,才导致他和报文主体产生差异。 69 | 70 | - 3.3.2 压缩传输的内容编码 71 | - 3.3.3 分割发送的分块传输编码 72 | 73 | ###3.4 发送多种数据的多部分对象集合 74 | ###3.5 获取部分内容的范围请求 75 | ###3.6 内容协商返回最合适的内容 76 | ##第四章 返回结果的HTTP状态码 77 | ###4.1 状态码告知从服务器返回的请求结果 78 | 状态码的类别: 79 | 1XX: Informational(信息性状态码); 接收的请求正在处理 80 | 2XX: Sucess(成功状态码); 请求正常处理完毕 81 | 3XX: Redirection(重定向状态码); 需要附加操作以完成请求 82 | 4XX: Client Error(客户端错误状态码); 服务器无法处理请求 83 | 5XX: Server Error(服务器错误状态码); 服务器处理请求出错 84 | ###4.2 2XX成功 85 | - 200:OK 86 | - 204:No Content 87 | - 206:Partial Content 88 | 89 | ###4.3 3XX重定向 90 | - 301:Moved Permanently(永久性重定向) 91 | - 302:Found(临时重定向) 92 | - 303:See Other (资源存在另一个URI,应使用GET方法获取) 93 | - 304:Not Modified (不满足附带条件) 94 | - 307:Temporary Redirect(临时重定向,不会从POST变为GET) 95 | 96 | ###4.4 客户端错误 97 | - 400: Bad Request (请求报文语法错误) 98 | - 401: Unauthorized (需要认证) 99 | - 403: Forbidden (禁止访问) 100 | - 404: Not Found (找不到资源or服务器拒绝请求而不想说明原因) 101 | 102 | ###4.5 服务器错误 103 | - 500: Internal Server Error (服务器内部错误) 104 | - 503: Server Unavailable (服务器超负载) 105 | 106 | ##第五章 与HTTP协作的Web服务器 107 | ###5.1 用单台服务器虚拟主机实现多个域名 108 | ###5.2 通信数据转发程序:代理、网关、隧道 109 | ###5.3 保存资源的缓存 110 | 111 | ##第六章 HTTP首部 112 | ###6.1 HTTP报文首部 113 | ###6.2 HTTP首部字段 114 | ###6.3 HTTP/1.1通用首部字段 115 | - Cache-Control(缓存控制) 116 | - Connection (控制不再转发+管理持久连接) 117 | - Date(报文创建的日期和时间) 118 | - Pragma(历史遗留,仅作为HTTP/1.0的向后兼容而定义) 119 | - Trailer(报文主体后记录了哪些字段) 120 | - Transfer-Encoding(传输编码方式) 121 | - Upgrade(检测HTTP协议及其其他协议是否可使用更高的版本通信) 122 | - Via(追踪客户端和服务端之前的请求和响应报文的传输路径) 123 | - Warning(与缓存相关相关的问题的警告) 124 | 125 | ###6.4 请求首部字段 126 | - Accept(可通知服务器,用户代理你能够股处理的媒体类型及媒体类型的相对优先级) 127 | - Accept-Charset(可通知服务器用户代理支持的字符集及字符集的相对优先顺序) 128 | - Accept-Encoding(首部字段用来告知服务器用户代理支持的内容编码及内容编码的优先级顺序) 129 | - Accept-Language(用来告知服务器代理能够处理的自然语言集),以及自然语言的相对优先级。 130 | - Authorization(用来告知服务器,用户代理的认证信息) 131 | - Expect(告知服务器,期望出现的某种特定行为) 132 | - Form(告知服务器使用代理的用户的电子邮件地址) 133 | - Host(告知服务器,请求的资源所处的互联网主机名和端口号) 134 | - If-Match(条件请求,服务器接收到附带条件的请求后,只有判断条件为真时,才会执行请求) 135 | - If-Modified-Since(附带条件之一,它会告知服务器若If-Modified-Since)字段早于资源更新时间,则希望能处理该请求) 136 | - If-None-Match(只有和If-Match相反,用于指定If-None-Match字段值的实体标记ETag值与请求资源的ETag不一致时,告知服务器处理该请求) 137 | - If-Range(若指定的If-Range字段值和请求资源的ETag值或时间一致时,作为范围请求处理,反之,则返回全体资源) 138 | - If-Unmodified-Since(和If-Modified-Since作用相反。它的作用是告知服务器,指定的请求资源只有在字段值内指定的日期之后,未发生更新的情况下,才能处理请求。如果在指定日期时间发生了更新,则以状态码412Precondition Failed作为响应返回) 139 | - Max-Forwards (通过TRACE方法或者OPTIONS方法,发送包含首部字段的Max-Forwards)的请求时,该字段以十进制整数形式指定可经过的服务器最大数目。服务器在往下一个服务器转发请求之前,Max-Fawords的值减1后重新赋值。当服务器接收到Max-Forwards值为0的请求时,则不再进行转发,而是直接响应请求 140 | - Proxy-Authorization(接收到从代理服务器发来的认证质询时,客户端会发送包含首部字段Proxy-Authorization的请求,以告知服务器认证所需要的信息) 141 | - Range(只需要获取部分资源的范围请求,包含Range字段可告知服务器资源的指定范围;接收到附带Range首部字段请求的服务器,会在处理请求之后返回状态码为206的响应,无法处理改范围请求是,则会返回状态码200的响应以及全部资源。) 142 | - Refer(Refer会告知服务器请求的原始资源的URI) 143 | - TE(告知服务器客户端能够处理响应的传输编码方式及相对优先级。他和Accept-Encoding的功能很相像,但是用于传输编码。此外,TE还可以指定伴随trailer字段的分块传输编码的方式。应用于后者时,只需要把trailers赋值给该字段值。) 144 | - User-Agent(用于传达浏览器的种类) 145 | 146 | ###6.5 响应首部字段 147 | - Accept-Range(告知客户端服务器是否能处理范围请求,以指定获取服务器某个部分的资源) 148 | - Age(告知客户端,源服务器在多久之前创建了响应,单位为秒) 149 | - ETag(告知客户端实体标识) 150 | - Location(将响应接收方引导至某个与请求URI位置不同的资源,它会配合3XX:Redirection的响应,提供重定向的URI) 151 | - Proxy-Authenticate(把由代理服务器所要求的认证信息发送给客户端) 152 | - Retry-After(告知客户端应该在多久之后再次发送请求) 153 | - Server(告知客户端当前服务器上安装的HTTP服务器应用程序的信息,不单单会标出服务器上的软件应用名称,还可能包括版本号和安装时启用的可选项) 154 | - Vary(当代理服务器接收到带有Vary首部字段指定获取资源的请求时,如果使用的Accept-Language字段值相同,那么就直接从缓存返回响应。反之,则需要从源服务器获取资源后才能作为响应返回) 155 | - WWW-Authenticate(用于HTTP访问认证) 156 | 157 | ###6.6 实体首部字段 158 | - Allow(通知客户端能够支持Request-URI指定资源的所有HTTP方法。当服务器接收到不支持的HTTP方法时,会以状态码405 Method Not Allowed作为响应返回。与此同时,还会把所有能支持的HTTP方法写入首部字段Allow后返回) 159 | - Content-Encoding(告知客户端服务器对实体的主体部分选用的内容编码方式。内容编码是指在不丢失实体信息的前提下进行的压缩) 160 | - Content-Language(告知客户端,实体主体使用的自然语言) 161 | - Content-Length(表明实体主体部分的大小) 162 | - Content-Location(给出与实体主体部分相对应的URI。和首部字段Location不同,Content-Location表示的是报文主体返回资源对应的URI。) 163 | - Content-MD5(由MD5算法生成的值,目的在于检查报文主体在传输过程中是否保持完整,以确认传输到达) 164 | - Content-Range(针对范围请求,返回响应时使用的Content-Range,能告知客户端作为响应返回的实体的哪个部分符合范围请求,以字节为单位,表示当前发送部分及整个实体的大小) 165 | - Content-Type(说明了实体主体内对象的媒体类型,和首部字段Accept一样,字段用type/subtype形式赋值) 166 | - Expires(将资源失效的日期告知客户端) 167 | - Last-Modified(指明资源最终修改的时间) 168 | 169 | ### 6.7为Cookie服务的首部字段 170 | - Set-Cookie 171 | - Cookie 172 | 173 | ### 6.8其他首部字段 174 | - X-Frame-Options(属于HTTP响应首部,用于控制网站内容在其他Web网站的Frame标签内的显示问题。主要是为了防止点击劫持攻击) 175 | - X-XSS-Protection(属于HTTP响应首部,是针对跨站脚本攻击的一种对策,用于控制浏览器XXS防护机制的开关) 176 | - DNT(属于HTTP请求首部,DNT是Do Not Track的简称,意为拒绝个人信息被收集,是表示拒绝被精准广告追踪的一种方法) 177 | - P3P(属于HTTP响应首部,通过利用P3P(The Platform for Privacy Preferences,在线隐私偏好平台技术)让Web网站上的个人隐私变成一种仅供程序可理解的形式,达到保护用户隐私的目的) 178 | 179 | ##第七章 确保Web安全的HTTPS 180 | ###7.1 HTTP的缺点 181 | HTTP的不足: 182 | 183 | - 通信使用明文(不加密),内容可能遭遇窃听 184 | - 不验证通信方身份,有可能遭遇伪装 185 | - 无法证明报文的完整性,有意有可能已遭篡改 186 | 187 | ###7.2 HTTP + 加密 + 认证 + 完整性保护 = HTTPS 188 | - HTTP加上加密处理和认证以及完整性保护后即是HTTPS 189 | - HTTP是身披SSL外壳的HTTP 190 | HTTPS并非应用层的一种新协议。只是HTTP通信接口部分用SSL(Secure Socket Layer)和TLS(Transport Layer Security)协议代替而已。 191 | 通常,HTTP直接和TCP通信,当使用SSL时,则先演变成先和SSL通信,再由SSL和TCP通信了,简言之,所谓HTTPS,其实就是身披SSL协议这层外壳的HTTP。 192 | 在采用SSL后,HTTP就拥有了HTTPS的加密、认证和完整性保护的功能了。 193 | - 相互交换密钥的公开密钥加密技术 194 | - 证明公开密钥正确性的证书 195 | - HTTPS的安全通信机制 196 | 197 | ##第八章 确认访问用户身份的认证 198 | ###8.1 何为认证 199 | 核对信息通常是这些: 200 | 201 | - 密码:只有本人才会知道的字符串信息 202 | - 动态令牌:仅限本人持有的设备内显示的一次性密码 203 | - 数字证书:仅限本人(终端)持有的信息 204 | - 生物认证:指纹和虹膜等本人的生理信息 205 | - IC卡认证:仅限本人持有的信息 206 | 207 | HTTP使用的认证方式: 208 | - BASIC认证(基本认证) 209 | - DIGEST认证(摘要认证) 210 | - SSL客户端认证 211 | - FormBase认证(表单认证) 212 | ###8.2 BASIC认证 213 | step1:当请求的资源需要BASIC认证时,服务器会随状态码401 Authorization Requested,返回带WWW-Authenticate首部字段的响应。该字段包含认证方式(BASIC)及Request-URI安全域字符串(realm)。 214 | 215 | step2:接收到状态码401的客户端为了通过BASIC认证,需要将用户ID和密码发送给服务器。发送的字符串内容是有用户ID和密码构成,中间以冒号连接后,再经由 Base64编码处理。 216 | 217 | step3:接收到包含首部字段Authorization请求的服务器,会对认证信息进行验证。验证通过,则返回一条包含Request-URI资源的响应。 218 | ###8.3 DIGEST认证 219 | step1:当请求的资源需要BASIC认证时,服务器会随状态码401 Authorization Requested,返回带WWW-Authenticate首部字段的响应。该字段包含质问响应方式认证所需的临时质询码(随机数,nonce)。 220 | 首部字段WWW-Authenticate内必须包含realm和nonce这两个字段的信息,客户端就是依靠向服务端回送这两个值进行认证的。 221 | 222 | step2:接收到状态码401的客户端,返回的响应中包含DIGEST认证必须的首部字段Authorization信息。 223 | 首部字段Authorization内必须包含username、realm、nonce、uri和response的字段信息。其中,realm和nonce就是之前从服务器接收到的响应中的字段。 224 | username是realm限定范围内可进行认证的用户名。 225 | 226 | step3:接收到包含首部字段Authentication请求的服务器,会确认认证信息的正确性。认证通过后则返回包含Request-URI资源的响应。 227 | ###8.4 SSL客户端认证 228 | - SSL客户端认证步骤 229 | 230 | step1:接收到需要认证资源的请求,服务器会发送Certificate Request报文,要求客户端提供客户端证书。 231 | 232 | step2:用户选择将客户端证书后,客户端会把客户端证书信息以Client Certificate报文方式发送给服务器。 233 | 234 | step3:服务器验证客户端证书通过以后可领取证书内容客户端的公开密钥,然后开始HTTPS加密通信 235 | - SSL客户端认证采用双因素认证 236 | - SSL客户端认证必要的费用 237 | 238 | ###8.5 基于表单认证 239 | 基于表单认证的方法并不是在HTTP协议中定义的,客户端会向服务器上的Web应用程序发送登录信息,按登录信息的验证结果认证。 240 | - 认证多半为基于表单认证 241 | - Session管理及Cookie应用 242 | 243 | ##第九章 244 | ###9.1 基于HTTP的协议 245 | ###9.2 消除HTTP瓶颈的SPDY 246 | - HTTP的瓶颈 247 | 1、一条连接只可以发送一个请求 248 | 2、请求只能从客户端发出 249 | 3、请求/响应首部未经压缩就发送。首部信息越多延迟越大 250 | 4、发送冗长的首部,每次互相发送相同的首部造成的浪费较多 251 | 5、可任意选择数据压缩的格式。非强制压缩发送 252 | Ajax的解决方法:有效利用JavaScript和DOM(Document Object Model)的操作,以达到局部Web页面替换加载的异步通信手段。 253 | Commet的解决方法:一旦服务器有内容更新了,Comet不会让请求等待,而是直接给客户端返回响应。这是一种通过延迟应答,模拟实现服务器向客户端推送(Server Push)的功能。 254 | - SPDY的设计与功能 255 | SPDY没有完全改写HTTP协议,而是在TCP/IP的应用层和传输层之间通过新加会话层的形式运作。同时,考虑到安全性的问题,SPDY规定通信中使用SSL。 256 | 1、多路复用 257 | 2、赋予请求优先级 258 | 3、压缩HTTP首部 259 | 4、推送功能 260 | 5、服务器提示功能 261 | - SPDY消除Web瓶颈了吗 262 | SPDY基本只是将单个域名(IP地址)的通信多路复用,所以当一个web网站上使用多个域名下的资源,改善效果会受到限制。 263 | SPDY的确是一种可有效消除HTTP瓶颈的技术,但很多Web网站存在的问题并非仅仅是由HTTP瓶颈所导致。对web本身的速度提升,还应该从其他可细致钻研的地方入手,比如改善Web内容的编写方式。 264 | ###9.3 使用浏览器进行全双工通信的WebSocket 265 | - WebSocket的设计与功能 266 | 267 | 268 | 269 | 270 | 271 | 272 | -------------------------------------------------------------------------------- /极光推送.md: -------------------------------------------------------------------------------- 1 | #iOS最新极光推送实现细节以及对收到通知跳页面的处理 2 | ## 一、前言 3 | 4 | 推送对于APP来说是非常常见的功能,对于移动端开发的朋友来说肯定不会陌生,掌握它对于广大开发者来说是极其有必要的;今天我们介绍一下极光推送,关于apple通知的一些原理以及极光SDK的集成过程就不多做赘述了,按照极光的步骤几乎不会有什么问题。主要聊一聊收到消息之后的处理。 5 | 6 | ## 二、收到消息的来源 7 | 8 | 极光SDK集成之后,收到消息推送的时候分为3种情况,分别是: 9 | 10 | 1:APP在前台运行 11 | 12 | 2:APP进入后台(未结束进程) 13 | 14 | 3:APP未开启 15 | 16 | ## 三、对收到消息时不同情况的处理 17 | 18 | 对于不同的情况程序会进入不同的代理方法: 19 | 20 | - APP在前台运行时: 21 | 22 | ``` 23 | 24 | - (void)jpushNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(NSInteger))completionHandler { 25 | 26 | NSDictionary * userInfo = notification.request.content.userInfo; 27 | 28 | self.notificationDic = [NSMutableDictionary dictionaryWithDictionary:userInfo]; 29 | 30 | [self dealNewNotifacation:[NSMutableDictionary dictionaryWithDictionary:userInfo]];//前台收到通知的处理,这里主要是处理数据源 31 | 32 | if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { 33 | 34 | [JPUSHService handleRemoteNotification:userInfo]; 35 | 36 | } 37 | 38 | completionHandler(UNNotificationPresentationOptionAlert); // 需要执行这个方法,选择是否提醒用户,有Badge、Sound、Alert三种类型可以选择设置 39 | 40 | } 41 | 42 | ``` 43 | 44 | - APP进入后台(未结束进程)时: 45 | 46 | ``` 47 | - (void)jpushNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler{ 48 | 49 | NSDictionary * userInfo = response.notification.request.content.userInfo; 50 | 51 | if([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) { 52 | 53 | [JPUSHService handleRemoteNotification:userInfo]; 54 | 55 | } 56 | 57 | [JPUSHService setBadge:0]; 58 | 59 | self.notificationDic = [NSMutableDictionary dictionaryWithDictionary:userInfo]; 60 | 61 | [[NSNotificationCenter defaultCenter] postNotificationName:@"didReceiveJPushNotification" object:nil userInfo:self.notificationDic]; 62 | 63 | completionHandler(); // 系统要求执行这个方法 64 | 65 | } 66 | ``` 67 | 68 | - APP未开启时(进程被杀死时): 69 | 70 | ``` 71 | 此时可以通过- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions ;方法获取到消息内容,实现如下: 72 | 73 | //获取推送消息 74 | 75 | self.notificationDic = [launchOptions objectForKey:@"UIApplicationLaunchOptionsRemoteNotificationKey"]; 76 | 77 | if (self.notificationDic) {//通知消息不为空 78 | 79 | [JPUSHService setBadge:0];//设置服务器上的badge为0 80 | 81 | [[NSNotificationCenter defaultCenter] postNotificationName:@"didReceiveJPushNotification" object:nil userInfo:self.notificationDic];//发送通知 82 | 83 | } 84 | ``` 85 | 86 | 此时由于APP已经结束进程无法使用断点调试,也无法打印消息内容,建议把字典转成字符串,使用MBP/SVP等方式展示出来,(记得调试结束干掉测试代码);具体如下: 87 | 88 | //[SlwySVProgressHUD showWithStatus:[SlwyCharacterToString dictionaryToJson:self.notificationDic]]; 89 | 90 | 91 | ## 四、收到消息根据内容跳转到指定页面 92 | 93 | 细心的朋友肯定发现,在后台收到通知和应用程序未开启的情况下收到通知消息之后都发送了一个名为didReceiveJPushNotification的自定义通知,而在前台收到消息时仅仅只是调用了一个方法; 94 | 95 | 解释一下,由于我的项目需求是前台收到消息之后,只是保存数据到本地,其他两种情况都要求跳转到指定页面;所以在前台收到消息后仅仅只是做了数据处理和存储,其他两种情况到另外的地方处理(本项目结构是登录界面和tabbar装入了一个容器控制器中,所以需要在容器控制器中做逻辑处理,跳转页面);一般的Tabbar+Navigation的项目在APPDelegate.m就能实现。 96 | 97 | 好了,看看具体的收到通知消息的跳转吧,在二三种情况下啊,发送的通知会触发下面的方法: 98 | 99 | ``` 100 | /** 101 | 102 | 收到极光通知的处理 103 | 104 | @param notification 通知内容 105 | 106 | */ 107 | 108 | - (void)didReceiveJPushNotification:(NSNotification *)notification { 109 | 110 | if ([[self.childViewControllers firstObject] isKindOfClass:[SlwyTabBarViewController class]]) {//当前是tabbar 111 | 112 | SlwyTabBarViewController *tabbar = [self.childViewControllers firstObject]; 113 | 114 | SlwyNvigaitionController *nav = tabbar.selectedViewController;//获取到当前视图的导航视图 115 | 116 | NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:notification.userInfo]; 117 | 118 | //BusinessLine:1.机票 / 2.酒店 / 3.火车票 / 4.专车 / 5.保险 / 6.贵宾厅服务 119 | 120 | switch ([dic[@"BusinessLine"] integerValue]) { 121 | 122 | case 1:{ 123 | 124 | SlwyApproPlaneDetailController *VC = [SlwyApproPlaneDetailController new]; 125 | 126 | VC.orderID = dic[@"OrderID"]; 127 | 128 | [nav.topViewController.navigationController pushViewController:VC animated:YES];//push到目的页面 129 | 130 | } 131 | 132 | break; 133 | 134 | case 2:{ 135 | 136 | SlwyHotelOrderDetailViewController *VC = [SlwyHotelOrderDetailViewController new]; 137 | 138 | VC.orderID = dic[@"OrderID"]; 139 | 140 | [nav.topViewController.navigationController pushViewController:VC animated:YES];//push到目的页面 141 | 142 | } 143 | 144 | break; 145 | 146 | case 3:{ 147 | 148 | SlwyMeTrainDetailController *VC = [SlwyMeTrainDetailController new]; 149 | 150 | VC.orderString = dic[@"OrderID"]; 151 | 152 | [nav.topViewController.navigationController pushViewController:VC animated:YES];//push到目的页面 153 | 154 | } 155 | 156 | break; 157 | 158 | case 4:{ 159 | 160 | SlwyCarViewController *VC = [[SlwyCarViewController alloc]init]; 161 | 162 | VC.isFromOrderList = YES; 163 | 164 | VC.orderID = dic[@"OrderID"]; 165 | 166 | [nav.topViewController.navigationController pushViewController:VC animated:YES];//push到目的页面 167 | 168 | } 169 | 170 | break; 171 | 172 | default: 173 | 174 | break; 175 | 176 | } 177 | 178 | dic[@"isRead"] = @"1";//设置该消息已读 179 | 180 | [SlwyMyMessageModel dealMessageArray:dic];//处理收到通知 181 | 182 | } else {//当前是登录 183 | 184 | } 185 | 186 | } 187 | ``` 188 | 这里面最核心的就是取到navigationController,进行跳转,由于项目结构的不同我这里需要判断当前是否是加载的tabbar,一般的Tabbar+Navigation的项目直接在APPDelegate.m中拿到navigationController直接跳转即可。 189 | 190 | -------------------------------------------------------------------------------- /私有库命令.md: -------------------------------------------------------------------------------- 1 | #常用命令 2 | ##私有库 3 | s1: 4 | pod lib create 库名 5 | 6 | s2: 7 | 把需要做成私有库的文件,拖入pods/Development Pods/库名下的class文件夹 8 | 9 | s3: 10 | 终端输入cd到Podfile所在文件夹,pod install 11 | 12 | s4: 13 | OSChina 创建代码库,description一定要填,选择导入已有项目,按页面上的命令输入终端 14 | 15 | s5:编辑podSpec文件(版本号特别重要) 16 | 17 | s6: 18 | 执行git add .; 19 | 执行git commit -m "初始化”; 20 | git push -u origin master;(如果本地or远程库没有需要执行git remote add origin https://gitee.com/qingfengiOS/QFTools) 21 | 22 | s7: 23 | git tag git tag 0.1.0 git push —tags 24 | 25 | s8:提交podSpec文件 26 | cd 到podSpec文件所在文件夹,执行 pod lib lint —allow-warnings等待podSpec文件验证通过, 27 | 执行pod repo push QFSpec Tools.podspec --allow-warnings;(如果本地索引库提示没有clean,执行pod repo add QFSpec https://gitee.com/qingfengiOS/QFSpec.git 28 | Over........................ 29 | 30 | cocoaPods源:https://github.com/CocoaPods/Specs.git 31 | 32 | Git 全局设置: 33 | git config --global user.name "四川商旅无忧科技服务有限公司" 34 | git config --global user.email "18080490272@163.com" 35 | 36 | 创建 git 仓库: 37 | mkdir SlwyBaseTools 38 | cd SlwyBaseTools 39 | git init 40 | touch README.md 41 | git add README.md 42 | git commit -m "first commit" 43 | git remote add origin https://gitee.com/slwy/SlwyBaseTools.git 44 | git push -u origin master 45 | 46 | 已有项目? 47 | cd existing_git_repo 48 | git remote add origin https://gitee.com/slwy/SlwyBaseTools.git 49 | git push -u origin master 50 | 51 | Care: 52 | 53 | - Swift支持: 54 | echo "3.0" > .swift-version 55 | 56 | - 文件依赖: 57 | s.source_files = 'SlwyCalendarLib/Classes/**/*' 58 | s.resources = 'SlwyCalendarLib/Assets/*.png' 59 | 60 | s.resource_bundles = { 61 | 'SlwyCalendarLib' => ['SlwyCalendarLib/Assets/*.png'] 62 | } 63 | 64 | - 当自己的私有库依赖另外的库(三方库or自己其他的私有库)时,一定要在 65 | s.dependency中写明,否则podSpec文件无法验证通过 66 | s.dependency 'AFNetworking', '~> 3.1.0' 67 | s.dependency 'SDWebImage', '~> 4.1.2' 68 | s.dependency 'QFRouter', '~> 0.1.1' 69 | --------------------------------------------------------------------------------