目录:
- 继承中的@synthesize
- 引用#import、#include、@import
- inline
- define的函数用法
- @selector为啥用assign
- protocol和父类定义的属性容易引起的crash
- if define防止头文件重复引入
- object强转bool
- 需要synthesis的情况
- @weakify(self)
1. 继承中的@synthesize
@synthesize我之前一直觉得没有什么用处,毕竟实例变量在声明属性的时候是会自动生成的,但是如果有继承关系的时候不是的哦。
首先先来看下如果父类有个属性,子类是不能用实例变量下划线的方式去存取这个属性的,只能用self.的方式:
// .h文件
@interface Parent : NSObject
@property (nonatomic, strong) NSNumber * num;
@end
// .m文件
#import "Parent.h"
@implementation Parent
- (instancetype)init {
if (self = [super init]) {
_num = @(10);
}
return self;
}
- (void)setNum:(NSNumber *)num {
_num = num;
NSLog(@"parent set num");
}
==============================
// .h文件
#import <Foundation/Foundation.h>
#import "Parent.h"
NS_ASSUME_NONNULL_BEGIN
@interface Child : Parent
@end
NS_ASSUME_NONNULL_END
// .m文件
#import "Child.h"
@implementation Child
//@synthesize num = _num;
- (instancetype)init {
if (self = [super init]) {
self.num = @(20);
}
return self;
}
- (void)setNum:(NSNumber *)num {
NSLog(@"child set num");
}
@end
如果按上面的代码init一个child对象,命令行将会打印child set num
并且打断点看child是这样子的:
child Child * 0x60000343c740 0x000060000343c740
|______ Parent Parent
NSObject NSObject
_num __NSCFNumber * (int)10 0xbb7c46e9adaa4cde
也就是说,由于child复写了setNum,所以没走父类的setNum,故而其实父类的_num仍旧是10,没有被改成20。
如果改为下面的样子:
- (void)setNum:(NSNumber *)num {
[super setNum:num];
NSLog(@"child set num");
}
那么这个时候child的父类Parent的_num就会被改成20了,并且命令行会有parent set num
& child set num
。
用self.赋值的时候永远走的是setter方法哦
然后我们尝试把child覆写的setter注释掉,然后放开@synthesize num = _num;
的注释,再来看一下child是什么样子的:
child Child * 0x6000024e6ae0 0x00006000024e6ae0
|______ Parent Parent
_num __NSCFNumber * (int)10 0xdd07221b04acaed4
_num __NSCFNumber * (int)20 0xdd07221b04acaf34
这个时候发现父类的_num仍旧是10,但是子类自己拥有了一个_num实例变量,已经被改成了20。
也就是说父类的property会自动生成实例变量,然后这个实例变量也是父类的,子类只能通过self.的方式来改变父类的实例变量。如果想让子类也拥有一个一样的实例变量,需要用synthesize给子类生成,此时相当于父类和子类都有一个属于自己的实例变量了,互不冲突。
当子类用self.来改的时候其实改的是子类的,如果父类用self.来改的时候改的是父类的实例变量。
2. #import、#include、@import
先是#import和#include的作用差不多就是相当于把头文件的内容复制过来,然后把本身的#import的头文件那句话给替换掉。但是#import比#include进步的一点就是能减少重复引用了,比如说你在A.h文件中#import C.h,B.h中也#import C.h,然后你在D.h文件中#import A.h #import B.h,是不会有关于C.h文件重复引用的问题的。具体的方法是通过#ifndef也就是条件编译来实现。
Xcode5以后有三个新东西modules、AutoLinking和@import,我们就可以不用再去在如下图所示的的Linked Frameworks and Libraries中去显式的添加framework了。
在苹果没有推出Modules的时候,我们如果写的程序比较复杂的话,我们#import了很多相同的头文件,这样会很麻烦,而且编译速度慢,所以苹果有一个pch文件可以帮助我们解决,但是放在pch的头文件,虽然提高编译速度,在整个工程中都能访问,这样终归不好。pch文件其实就是采用了预编译处理,当在Build Setting中的Precompile Prefix Header为YES,把一些公用的头文件放入预编译头文件中预先进行编译,然后在真正编译工程时再将预先编译好的产物加入到所有还没编译的文件中去,以此来加快编译速度。
※ 一个完整的编译过程如下所示
- 预处理(Pre-process):把宏替换,删除注释,展开头文件,产生 .i 文件
- 编译(Compliling):把之前的 .i 文件转换成汇编语言,产生 .s文件
- 汇编(Asembly):把汇编语言文件转换为机器码文件,产生 .o 文件
- 链接(Link):对.o文件中的对于其他的库的引用的地方进行引用,生成最后的可执行文件(同时也包括多个 .o 文件进行 link)
所以苹果推出了Modules,Modules相当于将框架进行了封装,然后加入在实际编译之时加入了一个用来存放已编译添加过的Modules列表。如果在编译的文件中引用到某个Modules的话,将首先在这个列表内查找,找到的话说明已经被加载过则直接使用已有的,如果没有找到,则把引用的头文件编译后加入到这个表中。这样被引用到的Modules只会被编译一次,而且在开发的时候也不存在整个工程都能去访问了。又可以提高编译速度。
在Xcode5之后,是默认开启了modules的,我们如果使用#import,Xcode会帮我们自动映射成@import,所以我们可以不用手动的去导入框架了。自动做的配置如下图所示。
这其中其实就是@import通过AutoLinking,Xcode的编译器LLVM会在编译阶段将所涉及到的框架自动帮你写到link里,不需要在build phases里手动添加了。
所以其实还是推荐库用@import引入滴,将 #import <MapKit/MapKit.h>
替换为 @import MapKit;
即可,而且你也可以只加载framework里面的submodules@import MapKit.MKAnnotation;
pch文件
可参考:https://www.cnblogs.com/wzdevelop/p/7339262.html
现在xcode已经不会默认创建pch文件啦,如果需要的话就自己建一个,然后在build setting里面设置precompile以及pch文件位置。
编辑器会自动帮你把所有的预编译文头文件导入到项目所有的源文件中,所以从编程规范上讲,应该利用条件编译将Objective-C头文件隔离起来,例如#ifdef OBJC(OBJC前后是两条下划线)和#endif可以将OC的头文件与C语言的源文件进行有效的隔离。
PCH文件可以很方便的将多个文件中都要使用的头文件一次性导入到项目中所有的源文件中,同时,你也可以在PCH文件中定义全局使用的宏,这种操作可以有效帮你节约开发时间。但是,同时你也应该注意到,PCH文件的使用,无形中增加了项目编译的时间,应该有选择性的慎重使用。
3. inline
static inline是经常出现的关键字组合,static表示在当前文件中应用(如 static A,在其它文件中也可以出现static A。)不会导致重名的错误。inline则是内联函数,用于替代宏。
例如:
static inline CGFloat CGFloatFromPixel(CGFloat value) {
return value / YYScreenScale();
}
//YYScreenScale()方法说明:
CGFloat YYScreenScale() {
static CGFloat scale;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
scale = [UIScreen mainScreen].scale;
});
return scale;
}
虽然static inline修饰的是函数,但它在这里就是宏的作用,即你可以将CGFloatFromPixel当作一个宏。
※ inline与宏的区别
inline是定义函数用的哦~ 宏其实不一定是啥反正就是替换。
正常函数被调用时,要有函数调用和返回。要保存当前程序上下文信息,以便函数调用完毕后返回原来的地方,继续执行程序。将函数的参数进行压栈、出栈,执行函数,函数调用完毕后释放内部变量占用的内存。
将函数声明为inline,编译器不把它当做是一个函数,而是类似于把函数代码拷贝到原来的地方,这样就省下了函数调用的开销。在这一点上和宏非常类似,就是复制粘贴。
所以其实相对于函数调用,inline节约了保存当前栈帧以及压栈等操作。相对于宏,它的优点主要是:
避免了宏的缺点:需要预编译。因为inline内联函数也是函数,不需要预编译。
编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。
可以使用所在类的保护成员及私有成员。
※ inline的局限性
是不是觉得inline是一个很好的事情,可以减少函数调用的消耗,还能像一个函数被编译检查,但是不是所有函数都能用inline,也不是所有inline真的可以被内联。
inline的使用时有所限制的,inline只适合函数体内部代码简单短小的函数使用,不能包含复杂的结构控制语句例如while、switch,并且不能内联函数本身不能是直接递归函数。
inline函数仅仅是一个对编译器的建议,所以最后能否真正内联,看编译器的意思:它如果认为函数不复杂,能在调用点展开,就会真正内联,并不是说声明了内联就会内联,声明内联只是一个建议而已。
关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。
因此,inline 是一种用于实现的关键字,而不是一种用于声明的关键字。
一般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内联函数的声明、定义体前面都加了inline 关键字,但inline不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。
虽然说内联函数可以提高执行效率,但是不可以将所有的函数都定义为内联函数。内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
4. define的函数用法
在实现依赖注入的时候,我们需要向container要一个对象,如果经常需要找container要,这段代码就会写好多遍,如果可以直接定义一个函数,然后找container要完直接赋值给属性就好啦~
例如:
#define InjectReal(N, property,proto) - (id<proto>) N##property { \
if (!_##property) {\
_##property = [[PUGLiveDIContainer container] resolve:@protocol(proto)];\
if (!_##property) {\
NSAssert(NO, @"property is not exit or not registry to container");\
}\
}\
return _##property;\
}
#define Inject(property, proto) InjectReal(,property,proto)
当我使用Inject(property, proto)
的时候,相当于在@implementation下面加了一个酱紫的函数:
- (id<proto>) N##property {
if (!_##property) {
_##property = [[YYYLiveDIContainer container] resolve:@protocol(proto)];
if (!_##property) {
NSAssert(NO, @"property is not exit or not registry to container");
}
}
return _##property;
}
所以这样当属性被读取的时候就会走这个宏定义的getter啦,自动从DI container拿一个实例。
注意define在预编译的时候就被替代为我们定义的函数了,但是inline是在函数真的被调用的时候把内容拷贝过来,所以其实这里还是应该用define。
在#define中,标准只定义了#和##两种操作。#用来把参数转换成字符串,##则用来连接前后两个参数,例如_##property=_属性名
。
一个经典的例子:
#include <stdio.h>
#define paster( n ) printf( "token " #n" = %d\n ", token##n )
int main() {
int token9=10;
paster(9);
return 0;
}
输出为
token 9 = 10
区别可以看下下面的例子:
#define f(a,b) a##b
#define d(a) #a --》 以"#"开头的,直接替换,不展开:immediately replaced by the unexpanded actual argument
#define s(a) d(a) --》 非以"#"开头的,先展开,再替换,也就是一般的情况
所以就两种情况:
1,不以"#"开头的,先展开参数a,然后是替换代码:puts(s(f(a,b)));-->puts(s(ab))-->puts(d(ab))-->puts("ab")
2,以"#"开头的,直接替换,不展开:puts(d(f(a,b)))-->puts("f(a,b)")
5. @selector为啥用assign
可以参考https://my.oschina.net/u/2340880/blog/396367 & https://www.cnblogs.com/cocoajin/p/3249824.html
@selector是什么?SEL并不是一种对象类型,它是一个关键字,就像int,long一样,它声明了一种类型:类方法指针。其实就可以理解为一个函数指针。
Objective-C在编译的时候,会根据方法的名字(包括参数序列),生成一个用 来区分这个方法的唯一的一个ID(一个整数),这个ID就是SEL类型的。我们需要注意的是,只要方法的名字(包括参数序列)相同,那么它们的ID都是相同的。就是说,不管是超类还是子类,不管是有没有超类和子类的关系,只要名字相同那么ID就是一样的。
// 方法存在
SEL sel = @selector(testBlock);
NSLog(@"SEL: %d", sel);
输出:
2019-12-20 23:08:40.031479+0800 Example1[1523:37824] SEL: 1388698754
// 方法不存在
SEL sel = @selector(testBlockfhdsfhdsf);
NSLog(@"SEL: %d", sel);
输出:
2019-12-20 23:09:25.391055+0800 Example1[1545:38986] SEL: 115122353
这里我试了一下如果输出用%@替代%d是会crash的,所以其实sel就是基本数据类型类似long这种。而且每次执行,sel的数值都是一样的,他也不会管这个函数存不存在,直接类似把String转为了int,可能是看hash值吧每次都是一样的,也就不分子类父类了,全靠名称。
大概因为其实SEL就是一串数字ID,不是OC对象,所以就assign啦,类似基本数据类型,系统会管理它的内存哒。
这样的机制大大的增加了我们的程序的灵活性,我们可以通过给一个方法传递SEL参数,让这个方法动态的执行某一个方法;我们也可以通过配置文件指定需要执行的方法,程序读取配置文件之后把方法的字符串翻译成为SEL变量然后给相应的对象发送这个消息。
从效率的角度上来说,执行的时候不是通过方法名字而是方法ID也就是一个整数来查找方法,由于整数的查找和匹配比字符串要快得多,所以这样可以在某种程度上提高执行的效率。
我们可以方便的通过方法的名字,获取到方法的ID也就是我们所说的SEL,反之亦然。具体的使用方法如下:
SEL 变量名 = @selector(方法名字);
SEL 变量名 = NSSelectorFromString(方法名字的字符串);
NSString *变量名 = NSStringFromSelector(SEL参数);
至于SEL的应用,我相信最广泛的便是target——action设计模式了。我们来简单模拟一下系统button的工作原理:
.h文件:
#import <UIKit/UIKit.h>
@interface Mybutton : UIButton
-(void)addMyTarget:(id)target action:(SEL)action;
@end
.m文件
#import "Mybutton.h"
@implementation Mybutton
{
SEL _action;
id _target;
}
-(void)addMyTarget:(id)target action:(SEL)action{
_target=target;
_action=action;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[_target performSelector:_action];
}
@end
6. protocol和父类定义的属性容易引起的crash
有的时候我们优化的时候提取子类里的属性到protocol和父类声明,但是如果build没啥问题可能手快就提交了,一run就会crash QAQ
注意如果在提取属性到父类/protocol的时候,千万记得同步
@synthesize
吖,要不self.xxx来赋值属性的时候,如果你在protocol里面声明的,那就找不到属性啦~ 父类里声明就木有 _xxx 实例变量了。
7. if define防止头文件重复引入
我们创建一个.h文件会自动生成下面的代码:
#ifndef HeaderName_h
#define HeaderName_h
// 这里面通常写各种宏定义、其他头文件的包含
#endif
这样子其实为了防止该头文件被重复引用~
被重复引用是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。
比如:存在a.h文件#include "c.h",而b.cpp文件同时#include "a.h" 和#include "c.h",此时就会造成c.h被b.cpp重复引用。
头文件被重复引用引起的后果:有些头文件重复引用只是增加了编译工作的工作量,不会引起太大的问题,仅仅是编译效率低一些。但是对于大工程而言,编译效率低下那将是一件多么痛苦的事情。
而有些头文件重复包含,则会引起错误,比如:在头文件中定义了全局变量(虽然这种方式不被推荐,但确实是C规范允许的),这种头文件重复包含会引起全局变量的重复定义。
在OC中
#import
保证了每个文件只会被import一次,即使我们没有加上面的ifdef也没有问题,但最好要加上啊,毕竟你不知道别人怎么import你的头文件,下面来尝试一下~
可以编译过的版本:
// Header1.h
static const NSString * header1Name = @"header1";
// Header2.h
#import "Header1.h"
static const NSString * header2Name = @"header2";
// vc
#import "Header1.h"
#import "Header2.h"
不可以编译过会报错redefine的版本:
// Header1.h
static const NSString * header1Name = @"header1";
// Header2.h
#include "Header1.h"
static const NSString * header2Name = @"header2";
// vc
#include "Header1.h"
#include "Header2.h"
改一下让它可以编译过:
// Header1.h
#ifndef Header1_h
#define Header1_h
static const NSString * header1Name = @"header1";
#endif /* Header1_h */
// Header2.h
#include "Header1.h"
static const NSString * header2Name = @"header2";
// vc
#include "Header1.h"
#include "Header2.h"
8. object强转bool
经常会遇到return一下这个对象是不是nil的bool值,我老是直接return obj
就好,更好一点的方式还是通过return !!obj
把它转为BOOL值,因为!
操作以后返回的就是bool啦,所以这里两次取反。
9. 需要synthesis的情况
这几天遇到一个readonly覆写了getter以后拿不到默认ivar的问题~ 我就查了一下,所以其实如果readwrite属性同时覆写了getter及setter、或者readonly属性覆写了getter,ivar不会自动生成,需要用synthesis。
我感觉因为默认的getter及setter里面的实现返回的就是ivar,当我们覆写了所有这个属性所需要的getter或setter(对于readonly而言setter没用),ivar就没有存在的意义了,所以它默认是不生成的,毕竟生成是为了让默认的存取方法用它。
you only need to explicitly synthesize (to get the synthesized ivar) if you explicitly implement all of the accessor methods (both getter and setter for
readwrite
properties, just the getter forreadonly
properties). You've written the getter for thisreadwrite
property, but not the setter, so the ivar will still be synthesized for you. Thus, as your code stands, you do not need to explicitly@synthesize
.
10. @weakify(self)
当self强引用了block时,再在block中调用self会引发循环引用问题。所以,为了避免这种情况,都会使用weak-strong来解除循环引用问题。
__weak typeof(self) weak_self = self;
a_block = ^{
__strong typeof(weak_self) strong_self = weak_self;
strong_self.view = ...
};
然鹅看起来不是非常的好看,可以定义一个宏定义:
#ifndef weakify
#if DEBUG
#if __has_feature(objc_arc)
#define weakify(object) autoreleasepool{} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) autoreleasepool{} __block __typeof__(object) block##_##object = object;
#endif
#else
#if __has_feature(objc_arc)
#define weakify(object) try{} @finally{} {} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) try{} @finally{} {} __block __typeof__(object) block##_##object = object;
#endif
#endif
#endif
#ifndef strongify
#if DEBUG
#if __has_feature(objc_arc)
#define strongify(object) autoreleasepool{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) autoreleasepool{} __typeof__(object) object = block##_##object;
#endif
#else
#if __has_feature(objc_arc)
#define strongify(object) try{} @finally{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) try{} @finally{} __typeof__(object) object = block##_##object;
#endif
#endif
#endif
使用变得好看简洁了很多~
@weakify(self)
blcok = ^{
@strongify(self)
self.view = ...
}
先看下weakify做了什么:
#define weakify(object) autoreleasepool{} __weak __typeof__(object) weak##_##object = object;
所以@weakify(self) = @autoreleasepool{} __weak __typeof__(self) weak_self= self;
所以其实宏定义里面的autoreleasepool就是为了让weakify可以在开头加个@,让使用方式更像其他语言的语法糖一样。
@strongify(self) = autoreleasepool{} __typeof__(self) self = weak_self;
所以其实weak和strong做的事情就是先定义一个weak属性的weak_self,再将weak_self赋值给strong类型的自己定义self变量,只不过这个变量和系统的self指针同名。所以在使用时虽然仍然使用的是self,但已经替换为自己定义的self。