缘由
看微博@我就叫Sunny怎么了了招聘一个靠谱的 iOS开发,发现自己有很多知识点需要重新梳理,这里是原文:,感兴趣的可以去看一下。 而需要解决问题,看懂《Effective Objective-C》和《禅与 Objective-C 编程艺术》其实就够了!下面我会针对这两本书不同的模块来一一分析这些问题,也是对自己知识的一次重新梳理~当然有错误的地方希望大家可以给我提出来,感谢~
- 理解"属性"这一概念
"属性"是Object-C的一项特性,用于封装对象中的数据。
在设置完属性后,编译器会自动写出一套存取方法,用于访问相应名称的变量 如果我们不想让编译器帮我们直接生成存取方法的话,需要设置@dynamic
@interface OnlyPerson : NSObject@property NSString *name;@property NSArray *friends;@end@interface OnlyPerson : NSObject@dynamic name,friends// 编译器会自动生成get和set方法- (NSString*)name;- (void)setName:(NSString *)name;- (NSArray *)friends;- (void)setFriends:(NSArray *)friends@end复制代码
属性可以定义关键字来赋予它一些特性,那么属性的关键字有哪些呢?
原子性:nonatomic:不使用同步锁atomic:加同步锁,确保其原子性// 使用同步锁代价更多,基本都使用nonatomic,为保证线程安全使用其他更深层的锁定机制(NSLock...等)读写readwrite:同时存在存取方法readonly:只有获取方法,仅供外界使用的属性采用此方式内存管理assign:纯量类型(scalar type)的简单赋值操作,strong:拥有关系保留新值,释放旧值,再设置新值weak:非拥有关系(nonowning relationship),属性所指的对象遭到摧毁时,属性也会清空,通常delegate,IBOOutlet使用weak属性,weak修饰只能针对OC对象forExample:@property(nonatomic,weak) IBOOutlet UIButton *btn;// 这里使用弱引用是因为xib或者sb已经对这个控件进行了强引用了@property(nonatomic,weak) iddelegate;// 避免循环引用强引用,可以看下面tableview和VC的图的例子。还有一个引用叫做相互引用,比如A控制器import了B控制器,B控制器impot了A控制器,就会相互引用,造成两个控制器都无法释放的问题,最严重的结果就是crash了。解决方案就是用@class的方式引用了,可以去看我上一篇关于风格纠错的那篇bolg。unsafe_unretained :类似assign,适用于对象类型,非拥有关系,属性所指的对象遭到摧毁时,属性不会清空。copy:不保留新值,而是将其拷贝,任何可以用一个可变的对象设置的((比如NSString,NSArray,NSURLRequest))属性的内存管理类型必须是copy 的。而用copy定义的对象在设定属性的时候也需要遵循copy的定义;forExample:- (id)initWithName:(NSString*)name lastName:(NSString*)friends{ if ((self = [super init])) { _name = [name copy];// 这里为什么要用_name呢? _friends = [friends copy]; } return self;}首先需要知道内存分为五个区:栈区(系统管理的地方)、堆区(程序员控制的地方)、常量区、代码区、静态区(全局区)。另外block的修饰也是使用copy的方式,block的创建时是在栈区的,因为栈区是由系统管理的,内存什么时候会释放我们是未知的,为了避免野指针错误的问题,我们需要主动用copy的方式将block拷贝到程序员可以控制的堆区。当然到了ARC时代,在创建的时候系统会帮助我们直接将block拷贝到堆区,所以使用strong或者copy修饰都可以了。复制代码
- 理解“对象等同性”这一概念
== // 操作符比较的是指针值,也就是内存地址。等同性判断:isEqual // 比较的是指针所指向的内容isEqualToString // 比较的是两个字符串的isEqualToArray // 比较的是两个数组isEqualToDictionary // 比较的是两个字典hash 如果比较的对象类型和当前对象类型相同,就需要采用自己编写的判定方法来保证它们真的相同- (BOOL)isEqualToPerson:(EOCPerson*)otherPerson { //先比较对象类型,然后比较每个属性 if (self == object) return YES; if (![_firstName isEqualToString:otherPerson.firstName]) return NO; if (![_lastName isEqualToString:otherPerson.lastName]) return NO; if (_age != otherPerson.age) return NO; return YES;}- (BOOL)isEqual:(id)object { //如果对象所属类型相同,就调用自己编写的判定方法,如果不同,调用父类的isEqual:方法 if ([self class] == [object class]) { return [self isEqualToPerson:(EOCPerson*)object]; } else { return [super isEqual:object]; }}复制代码
- 理解objc_msgSend的作用
说到objc_msgSend就不得不谈一下OC的runtime机制了,它是OC这门动态语言的根基。
当我们进行方法调用的时候
id result = [someObject message:parameter]复制代码
编译器看到这条消息之后这条消息后会将这条消息转换成一条标准的C语言函数objc_msgSend,它的原型是:
void objc_msgSend(id self,SEL,cmd,...)someObject --->self---->叫做接收者(recevier)message ---->SEL----->叫做选择子(selector)// SEL是selector的类型parameter ---> ...----->parameter为需要传递的参数,其中selector和parameter合起来叫做"消息"(message)复制代码
objc_msgSend在编译器会变成:
objc_msgSend(receiver, selector, arg1, arg2, ...)复制代码
objc_msgSend函数会依据recevier和selector的类型来调用适当的类型。而适当的方法是存在recevier的快速映射表(一块儿缓存着类经常调用的方法的list)和方法列表里,如果能找到就会实现这个消息,如果找不到就会沿着继承体系去找,如果找到合适的方法之后再跳转,如果最终找不到相符的方法会触发"消息转发"的操作或者临时向接收者动态添加selector对应实现内容,否则就crash。
而要更好的理解OC这门语言就需要我们对Runtime 的函数有个充分的理解。
Runtime 基础数据结构
objc_msgSend:的原型是
id objc_msgSend ( id self, SEL op, ... );复制代码
SEL是selector在Objc中的表示类型。其中selector是一个方法选择器,可以理解为区分方法的ID,它的数据结构是SEL:
typedef struct objc_selector *SEL;// 复制代码
id的定义是:
typedef struct objc_object *id;复制代码
objc_object的定义是:
struct objc_object {private: isa_t isa;// isa在指向实例对象所指向的类的时候可以根据isa指针找到对象所属的类,但是有时候它不指向实例对象所属的类,所以靠它来确定实例对象所属的类是不准确的。(比如isa—swizzling的时候),那么用什么来确定实例对象的类呢?public: // ISA() assumes this is NOT a tagged pointer object Class ISA(); // getIsa() allows this to be a tagged pointer object Class getIsa(); ... 此处省略其他方法声明}复制代码
Class其实是一个指向 objc_class 结构体的指针:
typedef struct objc_class *Class;复制代码
objc_class的定义是:
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache;//我们前边提到的缓存快速的映射表 class_data_bits_t bits; class_rw_t *data() { return bits.data(); }}复制代码
cache_t
struct cache_t { struct bucket_t *_buckets;// 存储着IMP,_mask 和 _occupied 对应 vtable mask_t _mask; mask_t _occupied; ... 省略其他方法}复制代码
bucket_t
struct bucket_t {private: cache_key_t _key; IMP _imp;public: inline cache_key_t key() const { return _key; } inline IMP imp() const { return (IMP)_imp; } // IMP一个函数指针 inline void setKey(cache_key_t newKey) { _key = newKey; } inline void setImp(IMP newImp) { _imp = newImp; } void set(cache_key_t newKey, IMP newImp);};复制代码
IMP
// 定义typedef void (*IMP)(void /* id, SEL, ... */ );复制代码
它就是一个函数指针,当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现.
Runtime的消息发送机制
上边我其实已经简单的说了objc_msgSend它调用的整个生命周期,下面我们更深入的去叙述它的发送步骤:
1.检测这个 selector 是不是要忽略的。2.检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。所以我们用一个nil对象调用方法的时候不会crash3.如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache(快速映射表) 里面找,完了找得到就跳到对应的函数去执行。4.如果 cache 找不到就找一下方法分发表。5.如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。6.如果还找不到就要开始进入动态方法解析了。复制代码
当然上边讲到的消息的调用只描述了部分消息的调用过程,还有其他的一些""边界情况"则有需要交给Object-C运行环境中的另一些函数来处理:
动态方法解析
下边我用一个类来解释动态方法解析
@implementation DBModel#pragara mark - runtime系统会顺着子类到父类的cache和方法分发表中找不到执行方法的时候,会调用resolveInstanceMethod:和resolveClassMethod:给程序员一次动态添加方法的机会,添加如果我们没有添加方法会走到消息转发+ (BOOL)resolveClassMethod:(SEL)sel { if (sel == @selector(learnClass:)) { // 我们在此处动态添加方法,系统会执行动态添加的方法 class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:"); return NO; } return [class_getSuperclass(self) resolveClassMethod:sel];}+ (BOOL)resolveInstanceMethod:(SEL)aSEL{ if (aSEL == @selector(goToSchool:)) { // 我们在此处动态添加方法 class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:"); return NO; } return [super resolveInstanceMethod:aSEL];}// 动态添加的方法+ (void)myClassMethod:(NSString *)string { NSLog(@"myClassMethod = %@", string);}- (void)myInstanceMethod:(NSString *)string { NSLog(@"myInstanceMethod = %@", string);}@end复制代码
消息转发
下面我们用Effective Objective上边的一个图来说明消息转发机制的各个步骤
没有动态添加方法,进入消息重定向阶段:
消息重定向 :让一个其他对象来接收这个消息
实例方法的重定向方法:- (id)forwardingTargetForSelector:(SEL)aSelector
类方法的重定向方法: + (id)forwardingTargetForSelector:(SEL)aSelector
- (id)forwardingTargetForSelector:(SEL)aSelector{ if (aSelector == @selector(goToSchool:)) { return NSClassFromString(@"Model"); } return [super forwardingTargetForSelector:aSelector];}+ (id)forwardingTargetForSelector:(SEL)aSelector { if(aSelector == @selector(xxx)) { return NSClassFromString(@"Class name"); } return [super forwardingTargetForSelector:aSelector];}复制代码
消息重定向阶段没有一个其他的对象来接收对象,进入消息转发阶段,forwardInvocation:方法会被执行
- (void)forwardInvocation:(NSInvocation *)anInvocation{ if ([someOtherObject respondsToSelector: [anInvocation selector]]) [anInvocation invokeWithTarget:someOtherObject]; else [super forwardInvocation:anInvocation];}复制代码
forwardInvocation:方法只有一个参数anInvocation,它封装了原始的消息和消息的参数。
forwardInvocation:像是一个消息转发中心,它不关心这些消息是干什么的,它仅仅承担转发的职责。
但是anInvocation是从哪里来的呢?是因为在触发forwardInvocation之前,runtime机制会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。所有在消息重定向的时候我们还需要重写methodSignatureForSelector:方法。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSMethodSignature* signature = [super methodSignatureForSelector:aSelector]; if (!signature) { signature = [NSMethodSignature methodSignatureForSelector:aSelector]; } return signature;}复制代码
这样一个简单实现的消息转发就完毕了。看到这里你对于runtime有个入门级的了解了,博主也是smile_cry~,那么它的应用有哪些呢?
给分类添加属性(关联对象)
在开发中会遇到使用三方SDK的情况,而有时候三方的SDK的提供的属性可能不能满足我们所有的业务需求,这时候就需要给方法的SDK添加一个属性,而我们只能直接拿到他的.h,这时候就需要在分类中添加一个属性了。
#import "Model.h"// 假设三方的SDK暴露的头文件是Model.h@interface Model (Extern)@property (strong, nonatomic) NSString *location;@property (assign, nonatomic) int age;@end复制代码
#import "Model+Extern.h"#importstatic const void *locationBy = &locationBy;static const void *ageBY = &ageBY;@implementation Model (Extern)- (void)setLocation:(NSString *)location{ objc_setAssociatedObject(self, locationBy, location, OBJC_ASSOCIATION_COPY_NONATOMIC);}- (NSString *)location{ return objc_getAssociatedObject(self, locationBy);}- (void)setAge:(int)age{ NSString *ageString = [NSString stringWithFormat:@"%zd",age]; objc_setAssociatedObject(self, ageBY, ageString, OBJC_ASSOCIATION_ASSIGN);}- (int)age{ return [objc_getAssociatedObject(self, ageBY) intValue];}复制代码
它主要用到的两个方法是
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)复制代码
objc_getAssociatedObject(id object,void *key)复制代码
参数 | 描述 | 对应Model+Extern.h |
---|---|---|
object | 源对象 | self |
key | 进行关联的关键字 | locationBy |
value | 关联的对象 | location |
policy | 关键策略 | OBJC_ASSOCIATION_COPY_NONATOMIC |
其中policy的定义typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */ OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. * The association is not made atomically. */ OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied. * The association is not made atomically. */ OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object. * The association is made atomically. */ OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied. * The association is made atomically. */};复制代码
Method Swizzling(方法交换:Hook)
#import@implementation UIViewController (Tracking) + (void)load { // 为什么使用在load中方法交换呢?? static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // dispatch_once起到了什么作用呢? Class aClass = [self class]; // When swizzling a class method, use the following: // Class aClass = object_getClass((id)self); SEL originalSelector = @selector(viewWillAppear:); SEL swizzledSelector = @selector(xxx_viewWillAppear:); Method originalMethod = class_getInstanceMethod(aClass, originalSelector); Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector); BOOL didAddMethod = class_addMethod(aClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); // 如果类中没有实现 Original selector 对应的方法,那就先添加 Method,并将其 IMP 映射为 Swizzle 的实现。然后替换 Swizzle selector 的 IMP 为 Original 的实现;否则交换二者 IMP。 if (didAddMethod) { class_replaceMethod(aClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); } #pragma mark - Method Swizzling - (void)xxx_viewWillAppear:(BOOL)animated { [self xxx_viewWillAppear:animated]; // 此处实际调用的被调换的方法viewWillAppear所以不会造成循环引用 @TODO("11231231");// 此处用来提示需要做的事情 NSLog(@"viewWillAppear: %@", self); } @end复制代码
+load方法在整个文件已被加载到runtime的时时候,在main函数之前就会被调用的钩子方法。这就注定了它的调用时机很早很早,所以我们要进行方法交换,它的很早的时机是一个极大的优势。 dispatch_once是为了防止方法交换调用多次,因为有时候hook了子类和父类共有的一个方法。他们调用的顺序可能是父类->子类->子类类别->父类类别。所义hook的顺序是不能保证的,而hook不同的方法也可能带来不同的结果。
函数截流防抖 coverFrom
最近项目中因为频繁调用方法造成项目的性能问题,就想到了函数截流的概念,这种策略在JS中有很多的实现和应用方案,iOS这边看到了玉令天下造的轮子MessageThrottle,感觉很厉害,特此学习一下他的实现逻辑。
iOS这边函数的调用本质其实都是消息发送(msg_send),所以我们可以用消息发送转发的武器来武装我们APP的性能。我这边就从我调用MessageThrottle的整个生命周期与大家分享一下。
// 添加函数防抖(用户重复点击最是按最后的一次提交)- (void)addMessageThrottle{ MTRule *rule = [[MTRule alloc] initWithTarget:self selector:@selector(SubmitFavoriteOrUnFavoritVideo:) durationThreshold:1]; rule.messageQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); rule.mode = MTPerformModeDebounce; [MTEngine.defaultEngine applyRule:rule];}复制代码
先从类的创建开始
- (instancetype)initWithTarget:(id)target selector:(SEL)selector durationThreshold:(NSTimeInterval)durationThreshold{ self = [super init]; if (self) { _target = target;// 需要限制的target _selector = selector;// 具体限制的方法 _durationThreshold = durationThreshold;// 函数截流的阈值限制频率 _mode = MTPerformModeDebounce;// 函数截流的模式 _lastTimeRequest = 0;// 最后请求的时间 _messageQueue = dispatch_get_main_queue();// 默认是在主线程的,当然也可以根据情况设置不同的线程 } return self; typedef NS_ENUM(NSUInteger, MTPerformMode) { MTPerformModeFirstly,// 执行最靠前发送的消息,后面发送的消息会被忽略 MTPerformModeLast,// 执行最靠后发送的消息,前面发送的消息会被忽略,执行时间会有延时 MTPerformModeDebounce,//消息发送后延迟一段时间执行,如果在这段时间内继续发送消息,则重新计时}; }复制代码
// 这一句又是做了什么呢? [MTEngine.defaultEngine applyRule:rule];复制代码
MTEngine.defaultEngine
+ (instancetype)defaultEngine{ static dispatch_once_t onceToken; static MTEngine *instance; dispatch_once(&onceToken, ^{ instance = [MTEngine new];// 初始化一个管理MTRule类的MTEngine对象 }); return instance;}- (instancetype)init{ self = [super init]; if (self) { _rules = [NSMutableDictionary dictionary];// 生成一个存储MTRule的NSMutableDictionary*rules pthread_mutex_init(&mutex, NULL);pthread_mutex_init初始化一个互斥锁 } return self;}复制代码
applyRule
- (BOOL)applyRule:(MTRule *)rule{ // 加锁 pthread_mutex_lock(&mutex); // 需要应用这个rule __block BOOL shouldApply = YES; // 在应用和废除规则的时候需要检查下库中涉及的方法 if (mt_checkRuleValid(rule)) { // 遍历存储所有的rule [self.rules enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, MTRule * _Nonnull obj, BOOL * _Nonnull stop) { if (sel_isEqual(rule.selector, obj.selector)// 判断这两个SEL是否一样 && mt_object_isClass(rule.target) && mt_object_isClass(obj.target)) { Class clsA = rule.target; Class clsB = obj.target; shouldApply = !([clsA isSubclassOfClass:clsB] || [clsB isSubclassOfClass:clsA]); *stop = shouldApply; NSCAssert(NO, @"Error: %@ already apply rule in %@. A message can only have one rule per class hierarchy.", NSStringFromSelector(obj.selector), NSStringFromClass(clsB)); } }]; if (shouldApply) {// 需要应用 self.rules[mt_methodDescription(rule.target, rule.selector)] = rule;// 使用target 和 selector 的组合值作为 key来存储当前rule mt_overrideMethod(rule.target, rule.selector);// 如果当前的rule复合交换的规则的话,使用class_replaceMethod将rule.selector替换为@selector(forwardInvocation:),直接进入消息转发阶段。 mt_configureTargetDealloc(rule);// 如果截流的对象是类对象那么直接返回,如果是实例对象,用对象关联的方式将生成一个MEDelloc对象 } } else { shouldApply = NO; } pthread_mutex_unlock(&mutex); return shouldApply;}复制代码
所以下边的这些问题,你应该知道怎么回答了:
- 什么情况使用 weak 关键字,相比 assign 有什么不同?
什么情况使用 weak 关键字?在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong。在下文也有论述:***《IBOutlet连出来的视图属性为什么可以被设置成weak?》***不同点:weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。 而 assign 的“设置方法”只会执行针对“纯量类型” (scalar type,例如 CGFloat 或 NSlnteger 等)的简单赋值操作。assign 可以用非 OC 对象,而 weak 必须用于 OC 对象复制代码
- 怎么用 copy 关键字?
用途:NSString、NSArray、NSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;block 也经常使用 copy 关键字,具体原因见官方文档:Objects Use Properties to Keep Track of Blocks:block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。复制代码
- 这个写法会出什么问题: @property (copy) NSMutableArray *array;
两个问题:1、添加,删除,修改数组内的元素的时候,程序会因为找不到对应的方法而崩溃.因为 copy 就是复制一个不可变 NSArray 的对象;;2、使用了 atomic 属性会严重影响性能 ;复制代码