ios

XDRefresh

开发的时候经常用到上下拉刷新控件,因为项目只是需要实现简单的菊花样式的刷新动画,所以自己封装了一个简易版上下拉刷新控件,到时未来再拓展出可以自定义的刷新控件。

效果图

使用

1
2
3
4
5
6
7
8
9
10
11
12
#import "KitRefresh.h"
...
KitRefreshHeader *_header;
KitRefreshFooter *_footer;

_header =  [KitRefreshHeader headerOfScrollView:tableView withRefreshingBlock:^{
     //下拉刷新回调
    }];
    
_footer = [KitRefreshFooter footerOfScrollView:tableView withRefreshingBlock:^{
        //上拉刷新回调
    }];

实现原理

通过观察scrollview的contentOffset属性的变化来进行处理。
给出一段KVO处理,具体实现大家可以看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context {
    if ([keyPath isEqualToString:@"contentOffset"]) {
        _contentHeight = _scrollView.contentSize.height;
        //拉伸状态
        if (_scrollView.isDragging) {
            CGFloat currentPosition = _scrollView.contentOffset.y;
            
            if (!_isRefreshing) {
                [UIView animateWithDuration:0.3f animations:^{
                    //下拉过程超过_headerHeight*1.5
                    if (currentPosition < -_headerHeight* 1.5 ) {
                        _statusLabel.text       = _releaseText;
                        _arrowView.transform    = CGAffineTransformMakeRotation(M_PI);
                    }else {
                        //上拉
                        if (currentPosition - _lastPosition > 5) {
                            _lastPosition = currentPosition;
                            _statusLabel.text = _pulldownText;
                            _arrowView.transform = CGAffineTransformMakeRotation(M_PI*2);
                            //下拉不超过_headerHeight*1.5
                        }else if(_lastPosition - currentPosition > 5) {
                            _lastPosition = currentPosition;
                        }
                    }
                }];
            }
            
        }else {
            //松开手时
            if ([_statusLabel.text isEqualToString:_releaseText]) {
                [self beginRefreshing];
            }
        }

    }
}

源码地址

https://github.com/caixindong/XDRefresh/tree/master/XDRefresh 大家觉得喜欢就赏个star,有什么问题可以issue我或者在评论指出,相互学习

ios

前言

最近刚做完一个项目,整理一下项目里面可以复用的东西,今天先拿里面的网络框架来说

XDNetworking

基于AFNetworking3.0封装网络请求功能,API面向业务层更友好,基础功能包括GET、POST、下载、单文件上传、多文件上传、取消网络请求。此外拓展出缓存功能,缓存分为内存缓存和磁盘缓存。

目录结构

将cache相关的东西抽离为了方便日后拓展

缓存实现

内存缓存用NSCache实现,磁盘缓存用文件缓存实现

使用

将XDNetworking包拉进工程

1
#import "XDNetworking.h"

GET请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 *  GET请求
 *
 *  @param url              请求路径
 *  @param cache            是否缓存
 *  @param params           拼接参数
 *  @param progressBlock    进度回调
 *  @param successBlock     成功回调
 *  @param failBlock        失败回调
 *
 *  @return 返回的对象中可取消请求
 */
+ (XDURLSessionTask *)getWithUrl:(NSString *)url
                            cache:(BOOL)cache
                           params:(NSDictionary *)params
                    progressBlock:(XDGetProgress)progressBlock
                     successBlock:(XDResponseSuccessBlock)successBlock
                        failBlock:(XDResponseFailBlock)failBlock;

POST请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 *  POST请求
 *
 *  @param url              请求路径
 *  @param cache            是否缓存
 *  @param params           拼接参数
 *  @param progressBlock    进度回调
 *  @param successBlock     成功回调
 *  @param failBlock        失败回调
 *
 *  @return 返回的对象中可取消请求
 */
+ (XDURLSessionTask *)postWithUrl:(NSString *)url
                             cache:(BOOL)cache
                            params:(NSDictionary *)params
                     progressBlock:(XDPostProgress)progressBlock
                      successBlock:(XDResponseSuccessBlock)successBlock
                         failBlock:(XDResponseFailBlock)failBlock;

下载请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 *  文件下载
 *
 *  @param url           下载文件接口地址
 *  @param progressBlock 下载进度
 *  @param successBlock  成功回调
 *  @param failBlock     下载回调
 *
 *  @return 返回的对象可取消请求
 */
+ (XDURLSessionTask *)downloadWithUrl:(NSString *)url
                        progressBlock:(XDDownloadProgress)progressBlock
                         successBlock:(XDDownloadSuccessBlock)successBlock
                            failBlock:(XDDownloadFailBlock)failBlock;

文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 *  文件上传
 *
 *  @param url              上传文件接口地址
 *  @param data             上传文件数据
 *  @param type             上传文件类型
 *  @param name             上传文件服务器文件夹名
 *  @param mimeType         mimeType
 *  @param progressBlock    上传文件路径
 * @param successBlock     成功回调
 * @param failBlock  失败回调
 *
 *  @return 返回的对象中可取消请求
 */
+ (XDURLSessionTask *)uploadFileWithUrl:(NSString *)url
                                fileData:(NSData *)data
                                    type:(NSString *)type
                                    name:(NSString *)name
                                mimeType:(NSString *)mimeType
                           progressBlock:(XDUploadProgressBlock)progressBlock
                            successBlock:(XDResponseSuccessBlock)successBlock
                               failBlock:(XDResponseFailBlock)failBlock;

多文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 *  多文件上传
 *
 *  @param url           上传文件地址
 *  @param datas         数据集合
 *  @param type          类型
 *  @param name          服务器文件夹名
 *  @param mimeType      mimeTypes
 *  @param progressBlock 上传进度
 *  @param successBlock  成功回调
 *  @param failBlock     失败回调
 *
 *  @return 任务集合
 */
+ (NSArray *)uploadMultFileWithUrl:(NSString *)url
                         fileDatas:(NSArray *)datas
                              type:(NSString *)type
                              name:(NSString *)name
                          mimeType:(NSString *)mimeTypes
                     progressBlock:(XDUploadProgressBlock)progressBlock
                      successBlock:(XDMultUploadSuccessBlock)successBlock
                         failBlock:(XDMultUploadFailBlock)failBlock;

取消所有网络请求

1
+ (void)cancleAllRequest;

取消单个请求

1
+ (void)cancelRequestWithURL:(NSString *)url;

获取缓存大小

1
2
3
4
5
6
/**
 *  获取缓存大小
 *
 *  @return 缓存大小
 */
+ (unsigned long long)totalCacheSize;

清理缓存

1
2
3
4
/**
 *  清除所有缓存
 */
+ (void)clearTotalCache;

自动清理缓存

1
2
3
4
    //每次网络请求的时候,检查此时磁盘中的缓存大小,如果超过阈值,则清理所有缓存
    //未来优化点:1、这里到时会做进一步优化,到时会有两种清理策略,一种基于时间维度,一种基于缓存大小,
    //          2、清理也不会清理全部,会采取LRU算法来淘汰在磁盘中价值最低的缓存
    if ([self totalCacheSize] > CACHEMAXSIZE) [self clearTotalCache];

使用Tip

1. 对于cache与否,到时大家针对自己的业务数据的特征来决定是否开启cache,即时性或时效性的数据建议不开启缓存,一般建议开启,开启缓存后会回调两次,第一次获取是缓存数据,第二次获取的是最新的网络数据;      2. 具体使用的时候,建议面向业务层再封装一次Service层或者将网络请求写进MVVM的viewModel中,向controller暴露只面向业务的参数。     

未来拓展

1. 为缓存添加相应的缓存淘汰算法LRU;       2. 添加请求缓存策略;     
3. 再封装面向业务更友好的manager,管理业务层的API,使其方便在不同网络环境下切换(测试环境与正式环境……)

github地址

https://github.com/caixindong/XDNetworking 大家觉得喜欢就赏个star,有什么问题可以issue我或者在评论指出,相互学习

ios

XDAlertController

最近在一个项目中遇到的这样的一个问题iOS8以下UIAlertController无法使用,XDAlertController我的一个解决方案

实现原理

在iOS7环境下调用UIAlertController会崩溃,所以通过判断系统的版本号来调用不同的API;         通过Method Swizzling替换原生的presentViewController和提供近似于原生API接口,让开发者感觉不出与原生API有什么差别。(Method Swizzling通过method_exchangeImplementations交换方法,交换时期在调用load的时候,里面涉及runtime相关知识,如果理解起来吃力,可以移步到这边文章了解 )       

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import "XDAlertController.h"
...
XDAlertController *alert = [XDAlertController alertControllerWithTitle:@"我是actionsheet" message:@"123" preferredStyle:XDAlertControllerStyleActionSheet];
    XDAlertAction *action1 = [XDAlertAction actionWithTitle:@"default" style: XDAlertActionStyleDefault handler:^( XDAlertAction * _Nonnull action) {
        
    }];
    
    XDAlertAction *action2 = [XDAlertAction actionWithTitle:@"取消" style:XDAlertActionStyleCancel handler:^(XDAlertAction * _Nonnull action) {
        
    }];
    
    XDAlertAction *action3 = [XDAlertAction actionWithTitle:@"destructive" style:XDAlertActionStyleDestructive handler:^(XDAlertAction * _Nonnull action) {
        
    }];
    
    [alert addAction:action1];
    [alert addAction:action2];
    [alert addAction:action3];
    
    [self presentViewController:alert animated:YES completion:nil];

github地址

https://github.com/caixindong/XDAlertController 大家觉得喜欢就赏个star,有什么问题可以issue我或者在评论指出,相互学习

ios

iOS多级展开列表的实现

效果图

用法(类似UITableView)

初始化XDMultTableView

1
2
3
4
5
6
7
8
9
10
#import "XDMultTableView.h"
...
@property(nonatomic, readwrite, strong)XDMultTableView *tableView;

 _tableView = [[XDMultTableView alloc] initWithFrame:CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height-64)];
    _tableView.openSectionArray = [NSArray arrayWithObjects:@1,@2, nil];
    _tableView.delegate = self;
    _tableView.datasource = self;
    _tableView.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:_tableView];

实现数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (NSInteger)mTableView:(XDMultTableView *)mTableView numberOfRowsInSection:(NSInteger)section{
    return 5;
}

- (XDMultTableViewCell *)mTableView:(XDMultTableView *)mTableView
              cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    static NSString *cellIdentifier = @"cell";
    UITableViewCell *cell = [mTableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    }
    UIView *view = [[UIView alloc] initWithFrame:cell.bounds] ;
    view.layer.backgroundColor  = [UIColor whiteColor].CGColor;
    view.layer.masksToBounds    = YES;
    view.layer.borderWidth      = 0.3;
    view.layer.borderColor      = [UIColor lightGrayColor].CGColor;

    cell.backgroundView = view;
    cell.selectionStyle = UITableViewCellSelectionStyleNone;

    return cell;
}

- (NSInteger)numberOfSectionsInTableView:(XDMultTableView *)mTableView{
    return 6;
}

-(NSString *)mTableView:(XDMultTableView *)mTableView titleForHeaderInSection:(NSInteger)section{
    return @"我是头部";
}

实现代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (CGFloat)mTableView:(XDMultTableView *)mTableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return 50;
}

- (CGFloat)mTableView:(XDMultTableView *)mTableView heightForHeaderInSection:(NSInteger)section{
    return 40;
}


- (void)mTableView:(XDMultTableView *)mTableView willOpenHeaderAtSection:(NSInteger)section{
    NSLog(@"即将展开");
}


- (void)mTableView:(XDMultTableView *)mTableView willCloseHeaderAtSection:(NSInteger)section{
    NSLog(@"即将关闭");
}

- (void)mTableView:(XDMultTableView *)mTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    NSLog(@"点击cell");
}

列表展开关闭的实现原理

在section header注册一个手势

1
2
3
4
5
//section header view 设置tag值为section
view.tag = section;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapHeader:)];
    [view addGestureRecognizer:tap];

手势的响应事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)tapHeader:(UITapGestureRecognizer *) tap {
    NSInteger section = tap.view.tag;
    NSNumber *sectionObj = [NSNumber numberWithInteger:section];
    UIImageView *imageView = (UIImageView *)[tap.view viewWithTag:100];
    //_multopenSectionArray 用于记录每个 section的展开和关闭状态
    if ([_multopenSectionArray containsObject:sectionObj]) {
        NSArray *deleteArray = [self buildDeleteRowWithSection:section];
        [_multopenSectionArray removeObject:sectionObj];
        //想关闭的section的所有indexPath
        [_tableView deleteRowsAtIndexPaths:deleteArray withRowAnimation:UITableViewRowAnimationFade];
        [UIView animateWithDuration:0.3 animations:^{
            imageView.transform = CGAffineTransformMakeRotation(-M_PI/2);
        }];
    }else{
        [_multopenSectionArray addObject:sectionObj];
        //想展开的section的所有indexPath
        NSArray *insertArray = [self buildInsertRowWithSection:section];
        [_tableView insertRowsAtIndexPaths:insertArray withRowAnimation:UITableViewRowAnimationFade];
        [UIView animateWithDuration:0.3 animations:^{
            imageView.transform = CGAffineTransformMakeRotation(0);
        }];
    }
}

源码地址:https://github.com/caixindong/XDMultTableView

ios

何为RunLoop

RunLoop就是一直运行的循环。程序的持续运行和即时反应依赖于RunLoop。我们知道,一个线程一次只能执行一个任务,执行完后线程就会退出,而RunLoop就是一个机制使我们的线程能够随时处理事件而不退出。其实它实现逻辑很简单,就是在内部开启一个死循环。

1
2
3
4
5
-(void)loop{
  do{
      bool isRunning = SomeToDo();
  }while(isRunning);
}

这种模型有个术语叫作Event Loop,实现这种模型的关键在于:
1、 如何管理事件;
2、 如何让线程在没有消息处理的时候休眠,在有事件响应的时候及时被唤醒;

RunLoop是一个对象

RunLoop管理着需要处理的事件和消息,并提供一个入口函数来执行上面Event Loop的逻辑。线程执行这个函数后就会处于“接受消息->等待->处理”的循环中,直到循环结束,函数返回。

Cocoa 中的 RunLoop

苹果为我们提供这样的两个对象:NSRunLoop 和 CFRunLoopRef 来实现RunLoop。
NSRunLoop是基于CFRunLoopRef封装的,提供面向对象API,但是它不是线程安全的。
CFRunLoopRef是CoreFoundation框架内的,它提供纯C的API,而且它是线程安全的。此外它是开源的。
我们这篇博客讲解也是主要基于CFRunLoopRef。

RunLoop与线程

pthread_t和NSThread都可以用于创建新线程,他们都是基于底层的mach thread封装的。且pthread_t与NSThread是一一对应的。CFRunLoop是基于pthread来管理的。
苹果不允许直接创建RunLoop,它提供两个自动获取函数:CFRunLoopGetMain()和CFRunLoopGetCurrent()。实现逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;

// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);

    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }

    // 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));

    if (!loop) {
        // 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        // 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }

    OSSpinLockUnLock(&loopsLock);
    return loop;
}

CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}

CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

由上面的代码,可看出,线程与RunLoop是一一对应,而且他们以键值对的关系保存在一个全局的字典里面。线程刚创建时是没有RunLoop,如果你不主动获取,那它一直都不会有。RunLoop的创建发生在第一次获取时,RunLoop的销毁发生在线程结束时。

RunLoop对外提供的API

CoreFoundation的RunLoop提供以下5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRUNLoopObserverRef
他们的关系如下图:

一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer。每次调用RunLoop的主函数,只能指定其中一个Mode,这个Mode被称为Current Mode。如果要切换Mode,只能退出Loop,再重新指定一个Mode进入,这样做是为了分隔不同组的Source/Timer/Observer,让其互不影响。

CFRunLoopSourceRef

是事件产生的地方,它有两个版本:Source0 和 Source 1。
Source0 只包含一个回调(函数指针),它并不能主动触发事件,使用时,你需要先调用CFRunLoopSourceSignal(source),将这个source标记为待处理,然后手动调用CFRunLoopWakeUp(source)来唤醒RunLoop,让其处理这个事件。
Source1 包含一个mach_port和一个回调,被用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop线程。

CFRunLoopTimerRef

是基于时间的触发器,它与NSTimer是toll-free-bridged的,可以混用。它包含一个时间长度和一个回调。当其加入RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒去执行那个回调。

CFRunLoopObserverRef

是观察者,每个Observer包含一个回调,当RunLoop的状态发生变化时,观察者能通过回调接收到这个变化。以下是观察者观察的时间点:

1
2
3
4
5
6
7
8
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
};

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

CFRunLoopMode

有了以上的基础,来谈Mode比较容易理解。
我们看下CFRunLoop和CFRunLoopMode的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};

struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

这里解释下commonModes和comminModeItems:
一个Mode可以将自己标识为Common属性(通过将其ModeName添加到RunLoop的commonModes中)。每当RunLoop的内容发生变化时,RunLoop都会自动将commonModeItems里的Source/Observer/Timer同步到具有“Common”标识的所有Mode里。
举例说明下:主线程的RunLoop里有两个预设的Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个Mode都已经被标志为“Common”属性。DefaultMode是App平时所处的状态,TrackingRunLoopMode是追踪ScrollView滑动时的状态。当你创建一个Timer并添加到DefaultMode时,Timer会得到重复回调,但当你滑动scrollView时,RunLoop会将Mode切换为TrackingRunLoopMode来保证滑动顺畅,这时Timer会停止回调。
如果你想Timer在滑动的时候也有回调的话,有两种做法:
一:将这个Timer分别加入这两个Mode;
二:这个做法用得比较多,就是将Timer加入到顶层的RunLoop的“commonModeItems”中。“commonModeItems”被RunLoop自动更新到所有具有“Common”属性的Mode里去。

我们只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。 苹果公开提供Mode有两个:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode,我们可以通过这两个Mode Name来操作相应的Mode。
同时苹果还提供了一个操作Common标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),我们可以用这个字符串来操作Common Items 或标志一个Mode为“Common”。

RunLoop的内部逻辑

RunLoop内部实现逻辑大致如下图:

代码实现逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/// 用DefaultMode启动
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

    /// 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode里没有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;

    /// 1. 通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

    /// 内部函数,进入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {

            /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            /// 4. RunLoop 触发 Source0 (非port) 回调。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);

            /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }

            /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }

            /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
            /// • 一个基于 port 的Source 的事件。
            /// • 一个 Timer 到时间了
            /// • RunLoop 自身的超时时间到了
            /// • 被其他什么调用者手动唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }

            /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

            /// 收到消息,处理消息。
            handle_msg:

            /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            }

            /// 9.2 如果有dispatch到main_queue的block,执行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            }

            /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }

            /// 执行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);


            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 进入loop时参数说处理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出传入参数标记的超时时间了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部调用者强制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一个都没有了
                retVal = kCFRunLoopRunFinished;
            }

            /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
        } while (retVal == 0);
    }

    /// 10. 通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

实际上RunLoop内部就是一个do-while循环,当你调用CFRunLoopRun()时,线程就会一直停留在这个循环里,处于接受消息->等待->处理的循环中,直到超时或者手动停止,函数返回。

RunLoop底层实现

RunLoop的核心是基于mach port,在RunLoop进入休眠时,它调用mach_msg()等待mach port传消息过来,如果没有消息过来,内核会将线程置于等待状态。为了更好的了解mach port,我们先了解OSX/iOS的系统架构。

苹果官方将整个系统大致划分为上述4个层次:
应用层:包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard 等。
应用框架层:即开发人员接触到的 Cocoa 等框架。
核心框架层:包括各种核心框架、OpenGL 等内容。
Darwin: 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码都可以在 opensource.apple.com 里找到。

我们再深入看下Darwin:

其中,在硬件层上面由三部分组成:Mach、BSD、IOKit(还包括一些没标注的内容),共同组成XNU内核。
XNU内核的内环被称为Mach,它作为一个微内核,仅提供诸如处理器调度、IPC(进程间通信)等非常少量的基础服务。
BSD层可以看作围绕Mach层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。 IOKit层为设备驱动提供一个面向对象的框架(C++)。

Mach 本身提供的 API 非常有限,而且苹果也不鼓励使用 Mach 的 API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为"对象"。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

Mach 的消息定义是在 <mach/message.h> 头文件的,很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct {
  mach_msg_header_t header;
  mach_msg_body_t body;
} mach_msg_base_t;

typedef struct {
  mach_msg_bits_t msgh_bits;
  mach_msg_size_t msgh_size;
  mach_port_t msgh_remote_port;
  mach_port_t msgh_local_port;
  mach_port_name_t msgh_voucher_port;
  mach_msg_id_t msgh_id;
} mach_msg_header_t;

一条Mach消息实际上就是一个二进制数据包(BLOB),其头部定义了当前端口local_port和目标端口remote_port。

1
2
3
4
5
6
7
8
mach_msg_return_t mach_msg(
          mach_msg_header_t *msg,
          mach_msg_option_t option,
          mach_msg_size_t send_size,
          mach_msg_size_t rcv_size,
          mach_port_name_t rcv_name,
          mach_msg_timeout_t timeout,
          mach_port_name_t notify);

发送消息和接收消息是通过同个API进行的,其option标记了消息传递的方向。

为了实现消息的发送和接收,mach_msg()实际上是调用了一个Mach陷阱,即mach_msg_trap(),陷阱这个概念在Mach中等同于系统调用,当你在用户态调用mach_msg_trap()时会触发陷阱机制,切换到内核态,内核态中的内核实现的mach_msg()会完成实际的工作。例如你在在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。

ios

前言

block的内存管理相对比较复杂,分ARC和MRC两种情况考虑,虽然现在开发基本是用ARC,编译器已经帮我们做了很多内存管理的优化,但我们不仅要知其然,也要知其所以然。

Block的类型

根据Block的存储方式,分为以下三种: . NSConcretStackBlock
.
NSConcretGlobalBlock
. _NSConcretMallocBlock
这三种类型表明了Block的三种存储方式:栈、全局、堆。
注意: 在ARC模式下,Block只存在全局和堆之中,对于栈上的Block,编译器帮我们拷贝到堆上,这也是为什么我们声明Block的时候即使不声明为copy,Block也是拷贝到堆上。显示地声明不会与编译器相冲突,是为了我们更好地理解是编译器做了copy这一步使得Block拷贝到堆上。

全局Block

位于全局存储区的Block有两种情况:
. 定义在函数外面的Block;

1
2
3
void (^myBlock)(void) = ^(){
    NSLog(@"hello");
};

. 不引用局部变量的Block;

1
2
3
4
5
6
7
8
9
10
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^myBlock)(void) = ^(){
            NSLog(@"hello");
        };
        myBlock();

    }
    return 0;
}

栈Block

1
2
3
4
5
6
typedef void (^stackBlock)() ;

stackBlock returnBlock(){
    int a =10;
    return ^{printf("a=%d\n",a);};
}

上面的例子在MRC下无法编译,而在ARC下可以编译
这是因为返回block的时候是返回局部变量的指针,而这一点恰是编译器已经断定了。在ARC下可以编译过,是因为ARC使用autorelease了

堆Block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
id getBlockArray(){
    int val =10;
    return [NSArray arrayWithObjects:
            ^{NSLog(@"blk0:%d",val);},
            ^{NSLog(@"blk1:%d",val);},nil];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        id obj = getBlockArray();
        typedef void (^blk_t)(void);
        blk_t blk = (blk_t)[obj objectAtIndex:0];
        blk();

    }
    return 0;
}

在MRC模式下,会发生异常,因为数组中的block是在栈上的,到blk指向的block已经释放内存了,访问blk就会造成崩溃,解决方法就是在block后面加上copy,使它变成堆上的block。

copy的使用

不管block配置在何处,用copy方法复制都不会引起任何问题。
在ARC环境下,如果不确定是否要copy block尽管copy即可。ARC会自动处理的。
【注意】:
● 在栈上调用copy那么复制到堆上
● 在全局block调用copy什么也不做
● 在堆上调用block 引用计数增加

ios

前言

这一篇主要着重对__block关键字的理解

例子

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        void (^helloBlock)(void) = ^(){
            printf("%d\n",a);
        };
        a = 11;
        helloBlock();

    }
    return 0;
}

上面那个例子运行后的结果是10。我们可以编译看一下block里面匿名函数的实现:

1
2
3
4
5
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            printf("%d\n",a);
}

bound by copy这里的注释表示,block对它引用的局部变量做了只读拷贝,也就是说block引用的是局部变量的副本。所以在执行block语法后,即使改写block中使用的局部变量的值也不会影响block执行时局部变量的值。
那怎么样才能修改a的值呢?答案就是__block

__block

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int a = 10;
        void (^helloBlock)(void) = ^(){
            printf("%d\n",a);
        };
        a = 11;
        helloBlock();

    }
    return 0;
}

引入__block关键字后,运行结果是11,我们再编译看看block里面匿名函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

            printf("%d\n",(a->__forwarding->a));
}

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

我们看到局部变量a变成一个构造体对象,而不再是一个整形变量,结构体里包含它原来的整形变量。bound by ref这个注释也表明此时已变成引用传递

小小总结

block可以引用局部变量,但是不能修改它,不然就是编译错误,如果想修改它,可以加上__block关键字。但是可以改变全局变量、静态变量、全局静态变量。

ios

前言

最近在总结关于block的东西,可能会写几篇关于block的博客,语法那些就不说,这里提供一个语法的入口,我们主要探究block的底层实现、block引用局部变量、block的内存管理,今天先讲下block的底层实现。

block实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Foundation/Foundation.h>

void hello_(){
    int i = 2;
    int j = 3;
    long (^myBlock)(void) = ^long(){
        return i*j*10;
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {


    }
    return 0;
}

定义hello方法是方便我们等下查看编译代码的时候准确定位myBlock的实现。用clang将objected-C 代码编译为C++代码。

1
控制台命令:clang -rewrite-objc main.m

我们打开main.cpp文件,定位到以下代码:

1
2
3
4
5
6
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

这是block基类的定义,所有block都继承于它,拥有它以下那些属性:
isa是指向该block的类型的类,也就是我们理解的对象指针;
Flags包含了引用计数;
Reserved:保留变量,我的理解是表示block内部的变量数;
FuncPtr:函数指针,指向具体block实现的函数的调用地址;

1
2
3
4
5
6
7
8
9
10
11
12
struct __hello__block_impl_0 {
  struct __block_impl impl;
  struct __hello__block_desc_0* Desc;
  int i;
  int j;
  __hello__block_impl_0(void *fp, struct __hello__block_desc_0 *desc, int _i, int _j, int flags=0) : i(_i), j(_j) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

这是myBlock的定义,它的结构体以hello打头,代表myBlock位于hello函数里,这是block的命名方式。helloblock_desc_0指的是myBlock的描述。

1
2
3
4
5
6
static long __hello__block_func_0(struct __hello__block_impl_0 *__cself) {
  int i = __cself->i; // bound by copy
  int j = __cself->j; // bound by copy

        return i*j*10;
    }

myBlock内函数的具体实现,注意bound by copy,block对外部引用变量做只读拷贝,还有另外的一种情况,在以后讲到关于block引用局部变量的时候会说。

1
2
3
4
static struct __hello__block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __hello__block_desc_0_DATA = { 0, sizeof(struct __hello__block_impl_0)};

这是myBlock的描述

1
2
3
4
5
void hello_(){
    int i = 2;
    int j = 3;
    long (*myBlock)(void) = ((long (*)())&__hello__block_impl_0((void *)__hello__block_func_0, &__hello__block_desc_0_DATA, i, j));
}

myBlock实例变量的定义

做下小小总结

block是对象;
编译器会根据block捕获的局部变量,生成具体的结构体定义。block内部的代码将会提取出来,成为一个单独的C函数(eg:helloblock_func_0)。创建block时,实际就是在方法中声明一个struct,并且初始化该struct的成员。而执行block时,就是调用那个单独的C函数,并把该struct指针传递过去;
block中包含了被引用的局部变量(由struct持有),也包含了控制成分的代码块(由函数指针持有),符合闭包(closure)的概念。

ios

前言

上篇主要讲Runtime的一些术语描述和定义,Runtime的主要应用是用于消息的传递,今天会结合一些实战例子来讲下OC的消息传递机制。

普通消息传递

在OC里,对象调用方法叫作发送消息,对象调用方法在Runtime里被转化为objc_msgSend函数来实现

1
2
3
[receiver oneMethod];
//transfer to :
objc_msgSend(receiver, @selector(oneMethod));

Runtime会根据类型自动转换为下列某个函数:

objc_msgSend:普通的消息都会通过该函数发送;
objc_msgSend_stret:消息中有数据结构作为返回值(不是简单值)时,通过此函数发送和接收返回值;
objc_msgSendSuper:和objc_msgSend类似,这里把消息发送给父类的实例;
objc_msgSendSuper_stret:和objc_msgSend_stret类似,这里把消息发送给父类的实例并接收返回值;

objc_msgSend的调用过程

1. 先检查这个selector是否存在,不存在则忽略;
2. 接着检查selector的target是否为nil,向nil对象发送任何消息都会被忽略掉;
3. 前面两步没问题,则先在isa指针指向的class的cache里面找有没有方法调用记录,如果有,则运行对应的函数,如果没有则在class的methodLists查找方法,没有则通过super_class指针找到父类的类对象结构体,然后从methodLists查找方法,如果仍找不到,则继续通过 super_class向上一级父类结构体中查找,直到根类(NSObject);
4. 如果还是找不到,则进入消息动态解析

消息动态解析

动态解析流程图:

具体解析:
1. 通过resolveInstanceMethod,该方法决定是否动态添加方法。如果返回YES,则通过class_addMethod动态添加方法,消息得到处理,结束;如果返回NO,则进入下一步;
2. 这一步会步入forwardingTargetForSelector方法,用于指定备选对象响应这个selector,不能指定为self,如果放回某个对象则会调用对象的方法,结束。如果放回nil,则进入第三步;
3. 这一步,我们通过methodSignatureForSelector进行方法签名,如果返回nil,则消息无法处理。如果返回methodSignature则进入下一步;
4. 这步调用forwardInvocation方法,我们通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象,如果方法方法调用成功,则结束。如果失败,则进入doesNotRecognizeSelector,如果没有实现这个方法会crash

实战

通过runtime动态创建类和对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <objc/message.h>
void sayFunction(id self, SEL _cmd, id some){

    NSLog(@"%@岁的%@说:%@",object_getIvar(self, class_getInstanceVariable([self class], "_age")),object_getIvar(self, class_getInstanceVariable([self class], "_name")),some);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //动态创建类
        Class MyPeople = objc_allocateClassPair([NSObject class], "Person", 0);

        //为类添加成员变量
        class_addIvar(MyPeople, "_name", sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));
        class_addIvar(MyPeople, "_age", sizeof(int),sizeof(int),@encode(int));

        //注册方法("v@:@"代表返回值+参数列表)
        SEL say = sel_registerName("say:");
        class_addMethod(MyPeople, say, (IMP)sayFunction, "v@:@");

        //注册类
        objc_registerClassPair(MyPeople);

        //创建实例对象
        id peopleInstance = [[MyPeople alloc]init];

        [peopleInstance setValue:@"李明" forKey:@"name"];

        //获取成员变量
        Ivar age = class_getInstanceVariable(MyPeople, "_age");
        object_setIvar(peopleInstance, age, @18);

        //发送消息
        objc_msgSend(peopleInstance,say,@"你好呀");

        //销毁实例对象
        peopleInstance = nil;

        //当类或子类的实例存在,则不能销毁类
        objc_disposeClassPair(MyPeople);
    }
    return 0;
}

tip: 默认会出现以下错误:
objc_msgSend()报错Too many arguments to function call ,expected 0,have3 直接通过objc_msgSend(self, setter, value)是报错,说参数过多。
请这样解决: Build Setting–> Apple LLVM 7.0 – Preprocessing–> Enable Strict Checking of objc_msgSend Calls 改为 NO

通过runtime获取类的相关信息(属性、实例变量、方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#import <Foundation/Foundation.h>

@interface People : NSObject{

    NSString* _nationality;

}

@property(nonatomic,copy)NSString* name;
@property(nonatomic,strong)NSNumber* age;

/**
 * 获取所有属性
 **/
-(NSDictionary*)getAllProperties;

/**
 * 获取所有实例变量
 **/
-(NSDictionary*)getAllIvars;


/**
 * 获取所有方法
 **/
-(NSDictionary*)getAllMethods;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#import "People.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation People


-(NSDictionary *)getAllProperties{
    unsigned int count = 0;
    NSMutableDictionary* result = [@{} mutableCopy];
    objc_property_t* properties = class_copyPropertyList([self class], &count);
    for (NSUInteger i = 0; i<count; i++) {
        const char* propertyName = property_getName(properties[i]);
        NSString* name = [NSString stringWithUTF8String:propertyName];
        id propertyValue = [self valueForKey:name];
        if (propertyValue) {
            result[name] = propertyValue;
        }else{
            result[name] = @"value 不能为 nil";
        }


    }
    free(properties);

    return result;
}

-(NSDictionary *)getAllIvars{
    unsigned int count = 0;
    NSMutableDictionary* result = [@{} mutableCopy];
    Ivar* ivars = class_copyIvarList([self class], &count);
    for (NSUInteger i = 0; i<count; i++) {
        const char* ivarName = ivar_getName(ivars[i]);
        NSString* name = [NSString stringWithUTF8String:ivarName];
        id value = [self valueForKey:name];
        if (value) {
            result[name] = value;
        }else{
            result[name] = @"value 不能为 nil";
        }
    }
    free(ivars);
    return  result;
}

-(NSDictionary*)getAllMethods{
    unsigned int count = 0;
    NSMutableDictionary* result = [@{} mutableCopy];
    Method* methods = class_copyMethodList([self class], &count);
    for (NSUInteger i = 0; i<count; i++) {
        const char* methodName = sel_getName(method_getName(methods[i]));
        NSString* name = [NSString stringWithUTF8String:methodName];
        //获取参数列表
        int args = method_getNumberOfArguments(methods[i]);
        result[name] = [NSString stringWithFormat:@"args count is %d",args-2 ];
    }
    free(methods);
    return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Foundation/Foundation.h>
#import "People.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        People* p = [[People alloc]init];
        p.name = @"罗大锤";
        p.age = @(30);
        [p setValue:@"中国" forKey:@"nationality"];
        NSDictionary* properties = [p getAllProperties];
        NSDictionary* ivars = [p getAllIvars];
        NSDictionary* methods = [p getAllMethods];
        NSLog(@"属性为:%@",properties);
        NSLog(@"实例变量:%@",ivars);
        NSLog(@"方法:%@",methods);
    }
    return 0;
}

打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2016-05-13 11:22:36.298 runtime  获取类的相关信息(属性、实例变量、方法)[2783:307932] 属性为:{
    age = 30;
    name = "\U7f57\U5927\U9524";
}
2016-05-13 11:22:36.299 runtime  获取类的相关信息(属性、实例变量、方法)[2783:307932] 实例变量:{
    "_age" = 30;
    "_name" = "\U7f57\U5927\U9524";
    "_nationality" = "\U4e2d\U56fd";
}
2016-05-13 11:22:36.299 runtime  获取类的相关信息(属性、实例变量、方法)[2783:307932] 方法:{
    ".cxx_destruct" = "args count is 0";
    age = "args count is 0";
    getAllIvars = "args count is 0";
    getAllMethods = "args count is 0";
    getAllProperties = "args count is 0";
    name = "args count is 0";
    "setAge:" = "args count is 1";
    "setName:" = "args count is 1";
}
Program ended with exit code: 0

通过Runtime给category添加属性

1
2
3
4
5
6
7
8
9
#import "People.h"

typedef void(^CallBackSomething)();

@interface People (testCategory)

@property(nonatomic,copy)NSString* address;
@property(nonatomic,copy)CallBackSomething block;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import "People+testCategory.h"
#import <objc/runtime.h>
#import <objc/message.h>


@implementation People (testCategory)

-(void)setAddress:(NSString *)address{
    objc_setAssociatedObject(self, @selector(address), address, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

-(NSString *)address{
    return objc_getAssociatedObject(self, @selector(address));
}

-(void)setBlock:(CallBackSomething)block{
    objc_setAssociatedObject(self, @selector(block), block, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

-(CallBackSomething)block{
    return objc_getAssociatedObject(self, @selector(block));
}

@end

利用Runtime给对象归档和解档

1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>

@interface People : NSObject<NSCoding>{

    NSString* _nationality;

}

@property(nonatomic,copy)NSString* name;
@property(nonatomic,strong)NSNumber* age;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#import "People.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation People

//归档
-(void)encodeWithCoder:(NSCoder *)aCoder{
    unsigned int count = 0;
    Ivar* ivars = class_copyIvarList([self class],&count);
    for (NSUInteger i = 0; i<count; i++) {
        Ivar ivar = ivars[i];
        const char* name = ivar_getName(ivar);
        NSString* key = [NSString stringWithUTF8String:name];
        id value = [self valueForKey:key];
        [aCoder encodeObject:value forKey:key];
    }
    free(ivars);
}

//解档
-(instancetype)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super init]) {
        unsigned int count = 0;
        Ivar* ivars = class_copyIvarList([self class],&count);
        for (NSUInteger i = 0; i<count; i++) {
            Ivar ivar = ivars[i];
            const char* name = ivar_getName(ivar);
            NSString* key = [NSString stringWithUTF8String:name];
            id value = [aDecoder decodeObjectForKey:key];
            [self setValue:value forKey:key];
        }
        free(ivars);
    }
    return self;
}

@end

利用Runtime实现Model与字典互转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import <Foundation/Foundation.h>

@interface People : NSObject

@property(nonatomic,copy)NSString* name;
@property(nonatomic,strong)NSNumber* age;


/**
 * 字典 转 Model
 **/
-(instancetype)initWithDictionary:(NSDictionary*) dict;

/**
 * Model转换成字典
 **/
-(NSDictionary*)covertToDictionary;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#import "People.h"
#import <objc/runtime.h>
#import <objc/message.h>

@implementation People




-(instancetype)initWithDictionary:(NSDictionary *)dict{
    if (self = [super init]) {
        for (NSString* key in dict) {
            id val = dict[key];
            SEL setter = [self propertySetterByKey:key];
            if (setter) {
                objc_msgSend(self, setter,val);
            }
        }
    }
    return self;
}

-(NSDictionary *)covertToDictionary{
    unsigned int count = 0;
    objc_property_t* properties = class_copyPropertyList([self class], &count);

    if (count!=0) {
        NSMutableDictionary* result = [@{} mutableCopy];
        for (NSUInteger i = 0; i<count; i++) {
            const char * propertyName = property_getName(properties[i]);
            NSString* name = [NSString stringWithUTF8String:propertyName];

            SEL getter = [self propertyGetterByKey:name];
            if (getter) {
                id value = objc_msgSend(self, getter);
                if (value) {
                    result[name] = value;
                }else{
                    result[name] = @"value 为 nil";
                }
            }
        }
        free(properties);
        return result;
    }
    free(properties);
    return nil;
}

#pragma mark - 生成setter
-(SEL)propertySetterByKey:(NSString*)key{
    //key的首字母大写
    NSString* propertySetterName = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
    SEL setter = NSSelectorFromString(propertySetterName);
    if ([self respondsToSelector:setter]) {
        return setter;
    }
    return nil;
}

-(SEL)propertyGetterByKey:(NSString*)key{
    SEL getter = NSSelectorFromString(key);
    if ([self respondsToSelector:getter]) {
        return getter;
    }
    return nil;
}

@end

消息动态解析(一)

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>

@interface People : NSObject

@property(nonatomic,copy)NSString* name;

/** m文件不实现方法,通过runtime动态添加方法
 *  通过resolveInstanceMethod:方法决定是否动态添加方法。
    如果返回Yes则通过class_addMethod动态添加方法,消息得到处理,结束;如果返回No
 **/
-(void)sing;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import "People.h"
#import <objc/runtime.h>
#import <objc/message.h>


@implementation People

+(BOOL)resolveInstanceMethod:(SEL)sel{
    if ([NSStringFromSelector(sel) isEqualToString:@"sing"]) {
        class_addMethod([self class], sel, (IMP)otherIMP, "V@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void otherIMP(id self ,SEL cmd){
    NSLog(@"%@正在唱歌",((People*)self).name);
}

@end
1
2
3
4
5
6
7
8
9
10
#import <Foundation/Foundation.h>
#import "People.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        People* p = [[People alloc]init];
        p.name = @"张全蛋";
        [p sing];
    }
    return 0;
}

打印结果:

1
2
2016-05-13 11:24:16.905 runtime  消息动态解析()[2819:311517] 张全蛋正在唱歌
Program ended with exit code: 0

消息动态解析(二)

修改Bird唱歌方法的调用对象

1
2
3
4
5
6
#import <Foundation/Foundation.h>

@interface Bird : NSObject
@property(nonatomic,copy)NSString* name;
-(void)sing;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#import "Bird.h"
#import "People.h"
@implementation Bird


//第一步,不动态添加方法
+(BOOL)resolveInstanceMethod:(SEL)sel{
    return NO;
}

//第二步,不指定备选对象响应sselector
-(id)forwardingTargetForSelector:(SEL)aSelector{

    return nil;
}

//第三步,返回方法签名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//第四部,修改调用对象
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    People* p = [[People alloc]init];
    p.name = @"张铁柱";
    [anInvocation invokeWithTarget:p];
}
1
2
3
4
5
6
7
8
9
10
11
12
#import <Foundation/Foundation.h>
#import "Bird.h"
#import <objc/runtime.h>
#import <objc/message.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Bird* b = [[Bird alloc]init];
        b.name = @"小鸟";
        [b sing];
    }
    return 0;
}

打印结果:

1
2
2016-05-13 11:29:58.335 runtime  消息动态解析()[2963:322829] 张铁柱正在唱歌
Program ended with exit code: 0

消息动态解析(三)

修改Person唱歌方法的实现

1
2
3
4
5
#import <Foundation/Foundation.h>

@interface People : NSObject
-(void)sing;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#import "People.h"

@implementation People

//不动态添加方法
+(BOOL)resolveInstanceMethod:(SEL)sel{
    return NO;
}

//不指定备选响应对象
-(id)forwardingTargetForSelector:(SEL)aSelector{
    return nil;
}

//返回方法签名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"sing"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//修改调用方法
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    [anInvocation setSelector:@selector(dance)];
    [anInvocation invokeWithTarget:self];
}

//若不实现forwardInvocation,则会调用此方法(可以注释掉forwardInvocation方法来做实验)
-(void)doesNotRecognizeSelector:(SEL)aSelector{
    NSLog(@"消息无法处理:%@",NSStringFromSelector(aSelector));
}

-(void)dance{
    NSLog(@"跳舞中");
}

@end
1
2
3
4
5
6
7
8
9
10
11
12
#import <Foundation/Foundation.h>
#import "People.h"
#import <objc/runtime.h>
#import <objc/message.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        People* p = [[People alloc]init];
        [p sing];
    }
    return 0;
}

打印结果:

1
2
2016-05-13 11:33:01.871 runtime  消息动态解析()[3073:329356] 跳舞中
Program ended with exit code: 0

以上demo地址

参考文章:
Objective-C Runtime 1小时入门教程
详解Runtime运行时机制

ios

前言

之前看过关于runtime的几篇文章,水平各有千秋,现在先汇集各个大牛的总结,以后找个时间将Runtime的源码啃一遍,再完善自己的笔记

什么是Runtime

Objective-C Runtime是一个将C语言转化为面向对象语言的扩展。
C++和Objective进行对比:
同 : C++和Objective-C都是在C的基础上加入面向对象的特性扩充而成的程序设计语言;
异 : 实现的机制差异不同。C++是基于静态类型,而Objective-C是基于动态运行时类型。也就是说用C++编写的程序编译时就直接编译成了可令机器读懂的机器语言;用Objective-C编写的程序不能直接编译成可令机器读懂的机器语言,而是在程序运行的时候,通过Runtime把程序转为可令机器读懂的机器语言。Runtime是Objective不可缺少的重要一部分;

一些 Runtime 的术语

id 和 Class

1
2
3
4
5
6
7
8
9
10
11
12
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif

Class是一个指向==objc_class==结构体的指针;
id是一个指向==objc_object==结构体的指针,其中的==isa==是一个指==objc_class==结构体的指针。
换句话说,id就是我们所说的对象,Class就是我们所说的类。

objc_class的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct objc_class *Class;
struct objc_class {
 Class isa                                 OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
 Class super_class                         OBJC2_UNAVAILABLE;
 const char *name                          OBJC2_UNAVAILABLE;
 long version                              OBJC2_UNAVAILABLE;
 long info                                 OBJC2_UNAVAILABLE;
 long instance_size                        OBJC2_UNAVAILABLE;
 struct objc_ivar_list *ivars              OBJC2_UNAVAILABLE;
 struct objc_method_list **methodLists     OBJC2_UNAVAILABLE;
 struct objc_cache *cache                  OBJC2_UNAVAILABLE;
 struct objc_protocol_list *protocols      OBJC2_UNAVAILABLE;
#endif
}
/* Use `Class` instead of `struct objc_class *` */

isa指针:指向的类结构称为metaclass(元类),其中存放着static类型的成员变量与类方法(“+”开头的方法);
super_class:指向该类的父类的指针,如果该类是根类(如NSObject或NSProxy),那么super_class就为nil;
name:类名;
version:类的版本信息,默认为0,可以通过runtime函数class_setVersion或者class_getVersion进行修改、读取;
info:类信息,供运行时期使用的一些位标识,如CLS_CLASS (0x1L) 表示该类为普通 class,其中包含实例方法和变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
instance_size:该类的实例变量大小(包括从父类继承下来的实例变量);
ivars:该类的成员变量地址列表;
methodLists:方法地址列表,与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储实例方法,如CLS_META (0x2L),则存储类方法;
cache:缓存最近使用的方法地址,用于提升效率;
protocols:存储该类声明遵守的协议的列表;

类与对象的继承层次关系如图:

所有的metaclass中isa指针都是指向根metaclass,而根metaclass则指向自身。根metaclass是通过继承根类产生的,与根class结构体成员一致,不同的是根metaclass的isa指针指向自身。

SEL

SEL是方法选择器,作用就和名字一样,日常生活中,我们通过人名辨别谁是谁,注意 Objc 在相同的类中不会有命名相同的两个方法。selector 对方法名进行包装,以便找到对应的方法实现

1
2
3
4
5
6
typedef struct objc_selector *SEL;

struct objc_selector {
    char *name;                       OBJC2_UNAVAILABLE;// 方法名
    char *types;                      OBJC2_UNAVAILABLE;// 方法类型
};

IMP

IMP是由编译器生成的一个函数指针,指向方法的实现。当你发起一个消息后,这个函数指针决定了最终执行哪段代码。

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

Method

Method代表类中的某个方法的类型。

1
2
3
4
5
6
7
typedef struct objc_method *Method;

struct objc_method {
    SEL method_name                   OBJC2_UNAVAILABLE; // 方法名
    char *method_types                OBJC2_UNAVAILABLE; // 方法类型,存储着方法的参数类型和返回值类型
    IMP method_imp                    OBJC2_UNAVAILABLE; // 方法实现
}

Ivar

Ivar代表类中实例变量的类型

1
2
3
4
5
6
7
8
9
10
typedef struct objc_ivar *Ivar;

struct objc_ivar {
    char *ivar_name                   OBJC2_UNAVAILABLE; // 变量名
    char *ivar_type                   OBJC2_UNAVAILABLE; // 变量类型
    int ivar_offset                   OBJC2_UNAVAILABLE; // 基地址偏移字节
#ifdef __LP64__
    int space                         OBJC2_UNAVAILABLE; // 占用空间
#endif
}

objc_property_t

objc_property_t是属性,是内置的类型,与之关联的还有一个objc_property_attribute_t,它是属性的attribute,也就是其实是对属性的详细描述,包括属性名称、属性编码类型、原子类型/非原子类型等。它的定义如下

1
2
3
4
5
6
typedef struct objc_property *objc_property_t;

typedef struct {
    const char *name; // 名称
    const char *value;  // 值(通常是空的)
} objc_property_attribute_t;

Cache

方法地址缓存

1
2
3
4
5
6
7
typedef struct objc_cache *Cache

struct objc_cache {
    unsigned int mask                   OBJC2_UNAVAILABLE;
    unsigned int occupied               OBJC2_UNAVAILABLE;
    Method buckets[1]                   OBJC2_UNAVAILABLE;
};

mask: 指定分配cache buckets的总数。在方法查找中,Runtime使用这个字段确定数组的索引位置。
occupied: 实际占用cache buckets的总数。
buckets: 指定Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。
objc_msgSend每调用一次方法后,就会把该方法缓存到cache列表中,下次调用的时候,就直接优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。

Catagory

这个就是我们平时所说的类别了,它可以动态的为已存在的类添加新的方法。

1
2
3
4
5
6
7
8
9
typedef struct objc_category *Category;

struct objc_category {
    char *category_name                           OBJC2_UNAVAILABLE; // 类别名称
    char *class_name                              OBJC2_UNAVAILABLE; // 类名
    struct objc_method_list *instance_methods     OBJC2_UNAVAILABLE; // 实例方法列表
    struct objc_method_list *class_methods        OBJC2_UNAVAILABLE; // 类方法列表
    struct objc_protocol_list *protocols          OBJC2_UNAVAILABLE; // 协议列表
}

参考文章:
Objective-C Runtime 1小时入门教程
详解Runtime运行时机制