load/initalize/MethodSwizzling/Block

前言

2016年6月7号开始load/initalize/KVO/KVC/Block,并通过代码实现

load/initalize

  • NSObject是大多数Objective-C类层次结构的根类,通过继承NSObject对象,拥有Objective-C运行时对象能力基本的接口
  • load/initalize 两个方法,由每个类Objective - C运行时自动调用

load 简介

当一个类或分类被添加到Objective-C运行时;会调用load方法,实现这个方法来加载执行特定类的行为。

Discussion:

  • load方法消息会被发送到动态加载和静态链接的类和类别,只有当被加载新的类或类别实现该方法才可以响应。

初始化时执行顺序如下:

  • 对所有链接框架初始化 (#import)
  • 加载所有load方法
  • 初始化所有C++ 静态变量和静态方法和使用attribute(constructor) 关键字声明的函数
  • 初始化所有链接到你的框架 (在Link Binary With Libraries导入的框架)

注意

  • 该类的load方法调用在所有父类的load方法之后
  • 该分类的load方法调用在对应类的load方法之后

使用场景

在自定义实现load方法时,可以很安全地(线程安全)提前获取其他毫无相关类消息,并且在消息还没实现时,手动去实现该消息(也就是说load方法常常应用于Swizzling Method
操作)

initalize 简介

在初始化类之前收到第一个消息,也就是说,对该类进行初始化时被调用

Discussion

  • 在程序中向类或者与其继承的任何子类发送第一条消息前,runtime会以线程安全的方式来向类发起initialize消息。
  • 父类会在子类之前收到这条消息。如果子类没有实现initialize方法,由于Runtime机制
    将按顺序调用实现该类继承而来的父类initialize方法,父类的initialize可能会被调用多次,针对这种情况解决方法代码:
+ (void)initialize {
  if (self == [ClassName self]) {
    // ... do the initialization ...
  }
}

使用该方法注意点:

在一个线程安全的方式下进行初始化时,在不同的类initialize被调用的顺序是不保证的,环,使用initialize方法时应尽量做少量且必要的工作。具体而言,initialize方法内部实现了同步锁如果在方法内实现复杂的逻辑,容易会导致死锁。因此,不应该依赖于initialize复杂的初始化,而应该限制它的简单,可以使用init、new等方法实现初始化。

特别注意

initialize每个类只会被调用一次,如果你想为类或分类实现自定义初始化方法可以使用load方法进行Swizzling Method操作,后面会简单讲下FDTemplateLayoutCell框架如何使用Swizzling Method

Method Swizzling 简介

  • 使用Method Swizzling可以改变现有的Selector的实现一项处理技术,方法的调用在Objective - C中运行时,通过改变Selector映射到该类的底层函数调度表,来修改方法的实现.
  • 在Objective-C实现扩展方法可以使用Category来覆盖系统方法,系统会优先调用Category中的代码,然后在调用原类中的代码,如果我们在已有的Category想实现UITableViewDelegate/UITableViewSource代理方法,往往就会使用Method Swizzling,而且很轻松做到这一点,可以通过新建UITableView Category,在其分类使用+(load)Method Swizzling替换方法代理,例如调用reloadData等方法,FDTemplateLayoutCell框架就是这么做的。

Method Swizzling应用场景

  • 跟踪视图控制器ViewDidLoad、viewWillAppear 、viewDidAppear实现,如果多个ViewController重复使用代码,可以在UIViewController分类在load方法中替换其实现方法

  • 实现UITableViewDelegate、UITableViewDataSource自定义代理方法,例如在编写框架时,用到UITableViewDelegate、UITableViewDataSource代理,替换具体实现名称

Swizzling应该总是在dispatch_once中执行

  • 因为Method Swizzling会改变全局状态,需要提供一切预防措施给Runtime。GCD的dispatch_once原子性就是一个很好的预防措施,无论有多少个线程,确保代码块恰好执行一次。

  • Selectors, Methods, & Implementations
    在Objective - C中,实现 Selectors, Methods, Implementations是指在Runtime的特定方面,实现消息发送时往往会用到他们

Selectors(typedef struct objc_selector *SEL):用于表示在运行时方法的名称。
Method(typedef struct objc_method Method
):表示类的方法
Implementation(typedef id (
IMP)(id, SEL, ...)
): 这个数据类型是指向一个指针函数实现方法

Method Swizzling使用

//
//  UIViewController+MethodSwizzlingCategoryExample.m
//  LoadInitalizeKVOKVCBlockExample
//
//  Created by lmj  on 16/6/8.
//  Copyright © 2016年 linmingjun. All rights reserved.
//

#import "UIViewController+MethodSwizzlingCategoryExample.h"
#import <objc/runtime.h>
@implementation UIViewController (MethodSwizzlingCategoryExample)


+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken,^{
        Class class= [self class];
        
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(swizzledViwDidLoad);
        // 通过class_getInstanceMethod()函数从当前class对象中的method list获取method结构体
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // 如果是类方法:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
        
        // 使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现swizzledSelector交换的方法,会导致失败
        BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) { // 如果self没有实现swizzledViwDidLoad方法,class_replaceMethod向对象所属的类动态添加所需的selector:,如果swizzledSelector没有实现,
            
            
            
            // class_replaceMethod,它有两种不同的行为。当类中没有想替换的原方法时,该方法会调用class_addMethod来为该类增加一个新方法,也因为如此,class_replaceMethod在调用时需要传入types参数,而method_exchangeImplementations和method_setImplementation却不需要。
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {// 通过class_addMethod()的验证,如果self实现了swizzledViwDidLoad这个方法,class_addMethod()函数将会返回NO,进行交换了。
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
        
        
    });
    
}

#pragma mark - Method Swizzling

- (void)swizzledViwDidLoad {
    // 在swizzledViwDidLoad方法内部调用[self swizzledViwDidLoad];时,执行的是UIViewController的viewDidLoad方法。
    // 该方法调用 [self swizzledViwDidLoad]; 不会造成死循环;因为swizzledViwDidLoad已经被指定为viewDidLoad方法,所以这里实质调用的是[self viewDidLoad];方法,经过调试如果不使用这行代码程序运行结果相同,但是 Mattt Thompson指出这行代码是为了防止潜在的危险或麻烦,体现一个优秀程序员的良好素养.
    [self swizzledViwDidLoad];
    NSLog(@"swizzledViwDidLoad: %@", self);
}

@end

 SEL originalSelector = @selector(viewDidLoad);
 SEL swizzledSelector = @selector(swizzledViwDidLoad);

 Method originalMethod = class_getInstanceMethod(class, originalSelector)
 Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

1.通过class_getInstanceMethod()函数从当前class对象中的method list获取method结构体

BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));

2.使用class_addMethod()函数对Method Swizzling做了一层验证,如果ViewController没有viewDidLoad(书写过程可能写成viewDidLoadd等错误),swizzledSelector交换的方法,会导致失败,返回YES

method_exchangeImplementations(originalMethod, swizzledMethod);

3.通过class_addMethod()的验证,如果self实现了swizzledViwDidLoad这个方法,class_addMethod()函数将会返回NO,进行交换了。

swizzledViwDidLoad方法内调用[self swizzledViwDidLoad]原因:

  • 在swizzledViwDidLoad方法内部调用[self swizzledViwDidLoad];时,执行的是UIViewController的viewDidLoad方法。
  • 该方法调用 [self swizzledViwDidLoad]时候不会造成死循环;因为swizzledViwDidLoad已经被指定为viewDidLoad方法,所以这里实质调用的是[self viewDidLoad];方法,经过调试如果不使用这行代码程序运行结果相同,但是 Mattt Thompson指出这行代码是为了防止潜在的危险或麻烦,体现一个优秀程序员的良好素养.

FDTemplateLayoutCell使用Method Swizzling

  • FDIndexPathHeightCacheInvalidation(UIViewTableView Category)

+ (void)load {
    // All methods that trigger height cache's invalidation(触发高度缓存失效的所有方法)
    SEL selectors[] = {
        @selector(reloadData),
        @selector(insertSections:withRowAnimation:),
        @selector(deleteSections:withRowAnimation:),
        @selector(reloadSections:withRowAnimation:),
        @selector(moveSection:toSection:),
        @selector(insertRowsAtIndexPaths:withRowAnimation:),
        @selector(deleteRowsAtIndexPaths:withRowAnimation:),
        @selector(reloadRowsAtIndexPaths:withRowAnimation:),
        @selector(moveRowAtIndexPath:toIndexPath:)
    };
    // sizeof(selectors) / sizeof(SEL) =  (9(数组大小))* SEL  / SEL; 一种用C语言计算个数的常用的技巧
    for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
        SEL originalSelector = selectors[index];
        
        SEL swizzledSelector = NSSelectorFromString([@"fd_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
        Method originalMethod = class_getInstanceMethod(self, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
     
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
  • 为什么在项目调用reloadData调用自动计算高度以及缓存,原因就在这里,在Objective-C实现扩展方法可以使用Category来覆盖系统方法,系统会优先调用Category中的代码,然后在调用原类中的代码,如果我们在已有的Category想实现UITableViewDelegate/UITableViewSource代理方法,往往就会使用Method Swizzling,可以通过新建UITableView Category,在其分类使用+(load)Method Swizzling替换方法代理(内部计算cell高度,缓存高度),再执行项目当中UITableViewDelegate/UITableViewDataSource代理方法
  • 写到这里,先写其他内容,有空再对FDTemplateLayoutCell框架所有类的源码进行分析,并且实现扩展

block

本章介绍了块和变量之间的相互作用,包括内存管理

block变量的类型

在block作为对象代码中,变量可以用五种不同的方式处理,其中有是三种标准类型的变量,如同一个函数:

  • 全局变量,包括静态局部变量
  • 全局函数(在技术上不是变量)
  • 在函数内的局部变量

block还支持其他两种类型的变量

  • 以__block修饰的变量,block代码块是可变的,任何引用快内的代码内存会从栈区复制到堆区

  • 通过const来修饰的变量

以下规则适用于block中使用变量

  • 全局变量是可访问的,包括在同一Scope(范围)里的静态变量

  • 传递给block参数都可以访问(就像函数 的参数)

  • 局部栈区(非静态)在同一Scope(范围)里以const修饰的局部变量(在于block外的变量可以无缝地直接在block内部使用,而且在block内变量发生变化的不会体现到block外)

  • 局部变量声明在该block的范围内,它的行为完全像一个函数的局部变量,每次调用block时,都会对变量进行一次copy操作(从栈区Copy到堆区),这些变量可以作为常量或在block内作为引用变量

  • 下面的示例演示了局部非静态变量的使用:

- (void)blockExample1 {

    int x = 123;
     
    void (^printXAndY)(int) = ^(int y) {
     
        printf("%d %d\n", x, y);
    };
     
    printXAndY(456); 

}
// prints: 123 456

局部非静态变量试图将一个的值赋给块内作为一个新值会导致一个错误:

- (void)blockExample2 {

    int x = 123;
     
    void (^printXAndY)(int) = ^(int y) {
     
        x = x + y; // error
        printf("%d %d\n", x, y);
    };

}
// Error
  • 你可以指定一个可变的具有读写功能的输入变量,通常�应用__block存储类型来声明变量。也可以通过与__block数据类型相似的、但他们又是排斥的数据类型来声明,例如寄存器、自动、静态局部变量,
    详见地址:寄存器、自动、静态关键字详细说明

  • 要允许一个变量在block内改变,使用__block存储类型来修饰变量,如果在栈区声明的block,进行任何的拷贝操作造成超出frame的末尾存储时,将会造成栈区的破坏
    下面的例子演示了在一个给定的词法范围内的多个块可以同时使用一个共享变量

  • 作为一次优化,block开始存储在栈区,可以使用Block_copy对block进行copy操作(或在Objective-C会进行一次copy操作(ARC机制)),变量将从栈区拷贝到堆区,因此,一个__block声明的变量地址是由时间而改变的。在这里有两__block变量进一步的限制:他们不能改变数组长度,并且不能包含C99可变长度的数组结构。

下面的例子说明了如何使用一个__block变量:

__block int x = 123; //  x lives in block storage
 
void (^printXAndY)(int) = ^(int y) {
 
    x = x + y;
    printf("%d %d\n", x, y);
};
printXAndY(456); // prints: 579 456
// x is now 579

下面的例子显示了几个类型的变量的相互作用:

对象和block变量

作为block变量提供Objective-C和C++对象和其他block的支持

Objective-C 对象

  • 当一个block被拷贝时,它会对block内使用的对象变量创建一个强引用。
  • 如果在一个方法中实现并使用一个block:

1.如果你通过引用访问的实例变量,那么这个block会对self产生强引用。
2.如果你通过值来访问实例变量,那么这个block会对这个变量本身产生强引用。

下面的例子说明了这两种不同的情况

dispatch_async(queue, ^{
    // instanceVariable is used by reference, a strong reference is made to self
    doSomethingWithObject(instanceVariable);
});
 
 
id localVariable = instanceVariable;
dispatch_async(queue, ^{
    /*
      localVariable is used by value, a strong reference is made to localVariable
      (and not to self).
    */
    doSomethingWithObject(localVariable);
});


dispatch_async(queue, ^{
    // instanceVariable is used by reference, a strong reference is made to self
    doSomethingWithObject(instanceVariable);
});
 
 
id localVariable = instanceVariable;
dispatch_async(queue, ^{
    /*
      localVariable is used by value, a strong reference is made to localVariable
      (and not to self).
    */
    doSomethingWithObject(localVariable);
});

dispatch_async(queue, ^{

    // instanceVariable is used by reference, a strong reference is made to self
    doSomethingWithObject(instanceVariable);
});
 
 
id localVariable = instanceVariable;
dispatch_async(queue, ^{
    /*
      localVariable is used by value, a strong reference is made to localVariable
      (and not to self).
    */
    doSomethingWithObject(localVariable);
});

C ++对象

  • 一般在block内可以使用c++对象。

  • 在一个成员函数,成员变量和函数的引用是通过隐含的this指针,从而出现可变进口。
    指针,从而出现可变。 如果一个块被复制,则有两点考虑:

  • 如果你有一个__block
    存储类的东西是一个基于堆栈的c++对象,那么平常copy
    使用构造函数。

  • 如果您使用任何其他的c++基于堆栈的对象在一块,它必须有一个const copy
    构造函数。 然后复制使用c++对象的构造函数。

  • 如果你有一个__block存储器修饰的类而且是基于堆栈的C++对象,则通常使用Copy构造函数。如果你在block内使用其他基于堆栈C++对象,它必须有一个const拷贝构造函数。然后,对C ++对象的构造函数使用一次Copy操作。

调用一个block

如果你声明一个block作为一个变量,可以使用它作为一个函数,如下两个例子的使用

int (^oneFrom)(int) = ^(int anInt) {
    return anInt - 1;
};
 
printf("1 from 10 is %d", oneFrom(10));
// Prints "1 from 10 is 9"
 
float (^distanceTraveled)(float, float, float) =
                         ^(float startingSpeed, float acceleration, float time) {
 
    float distance = (startingSpeed * time) + (0.5 * acceleration * time * time);
    return distance;
};
 
float howFar = distanceTraveled(0.0, 9.8, 1.0);
printf:  howFar:4.9

通常,你将一个block作为一个函数或一个方法的参数传递给一个block。在这些情况下,通常创建一个“inline(内联)”块。

使用block作为函数参数

你可以通过一个block作为一个函数参数,如同其他参数一样。然而,在很多情况下,你不需要声明block;相反,你只需简单地实现它们,然后使用inline(内联)它们作为一个参数。下面的例子使用了qsort_b功能。qsort_b类似于标准的qsort_r功能,以最后参数作为一个block。

char *myCharacters[3] = { "TomJohn", "George", "Charles Condomine" };
 
qsort_b(myCharacters, 3, sizeof(char *), ^(const void *l, const void *r) {
    char *left = *(char **)l;
    char *right = *(char **)r;
    return strncmp(left, right, 1);
});
// Block implementation ends at "}"
 
// myCharacters is now { "Charles Condomine", "George", "TomJohn" }

下面的例子演示了如何使用block与dispatch_apply功能。 dispatch_apply声明如下:

void dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));

功能块用于提交多次调用调度队列。它需要三个参数,第一个指定的迭代次数来执行;二是指定一个被提交的队列,第三个是block本身,而这个block本身又需要一个单一的参数作为当前的迭代次数。
可以使用dispatch_apply打印出迭代索引

#include <dispatch/dispatch.h>
size_t count = 10;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 
dispatch_apply(count, queue, ^(size_t i) {
    printf("%u\n", i);
});
printf: 0
        2
        1
        3
        4
        5
        6
        7
        8
        9

使用block作为方法参数

Cocoa提供了一种方法使用block的数量。 你通过block作为一个方法的参数就像任何其他参数。

NSArray *array = @[@"A", @"B", @"C", @"A", @"B", @"Z", @"G", @"are", @"Q"];
NSSet *filterSet = [NSSet setWithObjects: @"A", @"Z", @"Q", nil];
 
BOOL (^test)(id obj, NSUInteger idx, BOOL *stop);
 
test = ^(id obj, NSUInteger idx, BOOL *stop) {
 
    if (idx < 5) {
        if ([filterSet containsObject: obj]) {
            return YES;
        }
    }
    return NO;
};
 
NSIndexSet *indexes = [array indexesOfObjectsPassingTest:test];
 
NSLog(@"indexes: %@", indexes);
 
/*
Output:
indexes: <NSIndexSet: 0x10236f0>[number of indexes: 2 (in 2 ranges), indexes: (0 3)]
*/

拷贝block方法

你可以copy和release模块:
也可以使用C函数,如下两个函数

Block_copy();

Block_release();

为了避免内存泄漏,必须始终平衡Block_copy()
与Block_release()两种操作

避免两种block书写

避免block在for/while循环和判断语句内 (that is, ^{ ... }),,表示该block的是局部数据结构的地址,栈的局部数据结构的范围是封闭的复合语句,所以你应该避免在下面的例子中显示的模式:

面试提问:

load 与 initialize 的区别
答:load :
(1)load方法在这个文件被程序装载时调用 (2)如果一个类实现了load
方法,在调用这个方法前会首先调用父类的load (3)如果一个类没有实现load
方法,那么就不会调用它父类的load方法(4)添加一个子类的分类时,在分类添加load方法时,调用顺序parent->child->category(5)在Compile Sources中,文件的排放顺序就是其装载顺序,自然也就是load方法调用的顺序(除了父子类关系的描述)
(6)load方法是线程安全的,它内部使用了锁,所以我们应该避免线程阻塞在load
方法中。
initialize:(1)实例化一个对象时调用,只会调用一次(2)在initialize
方法内部也会调用父类的方法,即使子类没有实现initialize方法,也会调用父类的方法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容