简介
Objective-C 所有发送消息最终都被转换为runtime C语言的API
常用功能
- runtime可以访问类的私有成员
- runtime交换两个方法的实现,一般应用于修改系统方法的实现
- runtime动态添加方法,在加载类的时候,系统会把该类的所有方法都会加载到方法区,但是如果有个别方法只是偶尔使用,甚至几乎不会被用到,这样的方法被加载到内存的方法区中造成内存的浪费,最好使用懒加载模式,需要用到就动态添加。在resolveInstanceMethod方法中动态添加方法class_addMethod, 调用的时候通过performSelector:方法调用
- runtime动态添加属性,实现方式:在类别增加一个属性,并重写getter&setter方法,使用objc_setAssociateObject方法动态添加属性,使用objc_getAssociateObject方法获取属性值。一般应用于给系统类添加属性
id objc = [NSObject allc]; =>
id objc = objc_msgSend(objc_getClass("NSObject"), sel_registerName("alloc"));
objc = [objc init]; =>
objc = obj_msgSend(objc, sel_regisiterName("init"));
objc_msgSend(id, SEL); // id: 要发送的类, SEL:要发送发方法
objc_msgSend方法调用参数提示:Build Settings —> 搜索objc_msgSend—> NO
简单示例
#import <Foundation/Foundation.h>
@interface Person : NSObject
@end
#import "Person.h"
@implementation Person
- (void)eat {
NSLog(@"eating...");
}
@end
#import <Foundation/Foundation.h>
#import "Person.h"
#import <objc/message.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
person = objc_msgSend(person, sel_registerName("init"));
objc_msgSend(person, sel_registerName("eat"));
}
return 0;
}
方法掉用
isa : 指向当前类的类对象Class isa;
对象方法:存储在类对象的方法列表 methodLists
类方法:存储在元类的方法列表
内存5大区:
1. 栈 :不需要手动管理内存,自动管理
2. 堆 : 需要手动管理内存,主动去释放
3. 静态区
4. 常量区
5. 方法区
方法调用过程:首先找到调用的对象的isa所指向的类对象,然后在类对象中方法列表methodLists中找,如果找不到再看一下当前类是否实现了resolveInstanceMethod方法,如果没有找到再看当前类有没有实现消息转发和重定向,如果都没有实现就报错
在类别中添加属性时,只会生产getter&setter方法的声明,不会生成实现,更不会生成带下划线的实例变量
super关键字详解
是一个编译指示器,给编译器使用的,不是一个是指针,super的作用是告诉编译器让当前对象去调用父类的方法,本质还是当前对象去调用
验证: self 是指针是可以通过NSLog进行打印的,而super不是指针是不能打印的
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)test;
@end
//-----------------------------------------------------------------------
#import "Person.h"
@implementation Person
@end
//-----------------------------------------------------------------------
#import <Foundation/Foundation.h>
#import "Chinese.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Chinese *people = [[Chinese alloc] init];
[people test];
}
return 0;
}
#import "Person.h"
@interface Chinese : Person
@end
//------------------------------------------------------------------------
#import "Chinese.h"
@implementation Chinese
- (void)test {
NSLog(@"%@", self);
// NSLog(@"%@", super);
// class: 获取调用对象对应的类
// superClass: 获取调用对象对应的父类
// 因super是编译指示器,作用是让当前对象去调用父类中的方法,所以调用者仍是当前对象,所以[super class] 是指让当前对象去调用父类中的class方法,方法是调用的父类的方法但是调用者仍然是当前对象,即:Chinese
// [super superClass] 同理,调用者是Chinese, 调用者的父类就是Person
NSLog(@"%@ %@ %@ %@", [self class], [self superclass],
[super class], [super superclass]);
}
@end
/**
2016-09-04 10:35:17.246 RuntimeTest[7665:1132901] <Chinese: 0x1005048b0>
2016-09-04 10:35:17.247 RuntimeTest[7665:1132901] Chinese Person Chinese Person
Program ended with exit code: 0
*/
使用clang -rewrite-objc main.m 重新编译并查看源码看看调用者是谁
对runtime方法进行简化,去掉强转的代码
objc_msgSendSuper(
{(id)self, (id)class_getSuperclass(objc_getClass("Person"))}, sel_registerName("description")
);
// ojbc_msgSendSuper方法接收两个参数,第一个参数是一个结构体,第二参数是要调用的方法
// objc_msgSendSuper(struct objc_super *super, SEL op, ...)
// objc_super struct 声明如下:
struct objc_super {
__unsafe_unretained id receiver; // 消息发送者
__unsafe_unretained Class super_class; // 父类
};
// 从以上可知objc_msgSendSuper中的第一个参数即结构中第一个值(id)self 即使消息的发送者,而我们的代码是[super description]; 如果执行NSLog(@"%@", [super class]); 而我们知道 class方法是返回方法调用着所对应的Class对象,打印对象就是打印该对象的description方法,即该对象所对应的Class对象对应的description方法,而该方法的默认实现是打印对应的类名,逆向退出调用这是self当前类,即Person类,打印的是Person 而不是NSObject
const与宏的区别
苹果推荐使用const
区别:
1.编译时刻不同, 宏:预编译时期; const:编译时期
2.编译检查, 宏:不会进行编译检查,直接替换; const:进行编译检查
3.宏的好处,宏可以定义宏方法,而const却不能
4.宏的坏处, 如果大量使用宏会导致预编译时间过长
const是修饰的是右边,static既可以修饰局部变量也可以修饰全局变量,修饰全局变量作用域只能是当前文件,其他外部文件是不能访问的,extern 用于定于变量的作用域,外部的,即使用extern修饰的变量,其他文件也可以作用,注意extern只能用于变量的声明,不能用于变量的赋值。
static NSString * const CELL= @”Cell”;
extern NSString * const URL;
NSstring *const URL = @”xxxx”;
addSubview: 添加子视图,会先检查要添加的视图对应的父视图是否已经被添加过,如果添加过了,会先将之前的视图移除掉,然后再重新添加,所以如果重复添加同一个视图添加多次,只会有一个。
父子控制器
什么时候会用到父子控制器:当另一个控制器的视图被添加到当前视图控制器的视图上时。[self.view addSubview:xxxViewController.view];
【父子控制器】每个视图控制都有一个addChildViewController方法,将视图控制器添加到器childViewControllers数组中, 如果把B视图控制器中的view添加到A视图控制器中的view中,需要将B视图控制器作为A视图控制器的子控制器(当控制器的view互为父子关系,那么控制器最好也互为父子关系)视图控制器成为父子控制器有以下作用
1. 首先能够保证子控制器的命,不至于过早销毁
2. 当父控制器发生变化的时候,可以通知子控制器,例如父控制器屏幕旋转了,子控制器也能监听到
3. iOS的个别API会判断是否父子控制器做一些相应的处理,如 self.navigationController 方法先获取当前控制器对应的导航控制器,如果为空再获取当前控制器对应的父控制器,如果还为空就返回空。所以可以将下一个控制器添加到当前控制器的子控制器中addChildViewController,当前控制器是导航控制器,那么子控制器也能拿到导航控制器
assign和weak的区别
共同点:都不会使引用计数+1
不同点:assign用于基本类型,当指向的指针被销毁的时候,变量仍然指向该内存地址,不会被清空,weak用于修饰对象,当指向的指针被销毁时,该变量的值为被清空赋值为nil,所以销毁以后再调用对象的方法不会报EXE_BAD_ACCESS 内存不可访问的问题,这种更加安全
frame和bounds
frame 和 bounds 都是用来描述一片范围区域,不同的是frame是用于描述控件的范围区域,而bounds是用来描述控件中的内容的范围区域,默认情况下bounds的x和y都是0,宽度高度和frame的宽高保持一致,在实际开发中我们一般只获取bounds的值,主观的认为bounds的横纵坐标都是0(错位观点),但是在一些特殊情况下会有可能去修改bounds中的横纵坐标的值,只要修改,那么横纵坐标就会有不为0的可能性。
frame:可视区域,相对于父视图的左上角为原点(0, 0),frame用于描述可视范围的区域
bounds: 内容区域,相对于自己的左上角为原点(0, 0),bounds用于描述可视范围的内容区域
所有控件都有一个看不到的内容区域用于存放内容,而且这个内容区域可以是无限大。例如UITableViewCell 有一个contentView容器用于盛放单元格的所有子控件的内容,iOS提供出了这个contentView属性,例如UIButton中将UIImageView和UILabel都会添加到内容区域中,只不过这个区域系统没有提供contentView来操作,只能通过bounds来改变它的区域
frame: 固定的可视范围,相对于父视图的位置保持不变,可视范围相对于内容位置是可变的!是相对论
通过修改bounds也可以统一改变内容中的所有子控件的位置
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) UIView *redView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
_redView = redView;
UISwitch *swtich = [[UISwitch alloc] init];
[redView addSubview:swtich];
UISwitch *swtich2 = [[UISwitch alloc] initWithFrame:CGRectMake(100, 100, 0, 0)];
[redView addSubview:swtich];
[redView addSubview:swtich2];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CGRect bounds = self.redView.bounds;
bounds.origin.y -= 20;
_redView.bounds = bounds;
}
@end
UIScrollView添加到view中,frame是固定的,但是内容是可以滚动的,其实就是修改bounds对应的x,y值,UIScrollView中的内容偏移量contentOffset其实就是bounds中的x,y值
UIScrollView的实现:对UIView添加拖动手势UIPanGestureRecognizer, 根据手势位移获取对应的偏移量,然后去修改bounds中的x,y 值
block
block是一个Objective-C对象,在MRC中block使用copy,在ARC下使用strong,在不同的情况下分配到不同的内存区区域NSMallocBlock(堆)、NSStackBlock(栈)、NSGlobalBlock(全局区)
如果block没有引用外部局部变量,block被分配在【全局区】NSGlobalBlock
如果block引用了外部局部变量,block被分配在【栈】NSStackBlock
循环引用:
将self强指针引用包装成弱指针引用 __weak typeof(self) weakSelf = self;
将self弱引用包装成强指针引用 __strong typeof(weakSelf) strongSelf = weakSelf;
代码块访问外部变量:
除了访问局部变量是值传递,其他情况下(静态变量,全局变量,__block修饰的变量)都是引用传递(指针传递)
Objective-C的特殊语法
- Type varName = (Value, Value2, Value3, …);
该表达式返回最后一个值,小括号中的值的个数不限
e.g.
int a = (1, 2, 3); // 3
NSString *value = (@”value”, @”value1”, @”value2”); // @”value2” - {} 表达式
我们一般在方法体和代码块中使用大括号,但是以下也是正确的语法
{
int i = 10;
NSLog(@”i = %d, a= %d”, i, a);
}
用大括号将多行代码进行括起来,好处时可以将逻辑紧凑的一段代码使用{}封装,用来表示一个完整的逻辑,在大括号中定义的局部变量只能在大括号中使用 - ({}}); 表达式
大括号中仍然是存放多行代码
示例:
int result = ({
int a = 10;
int b = 20;
a + b;
});
该语法的作用是,允许大括号中的代码并且将大括号中的代码的最后一句计算出的值作为整个表达式的值,该表达式会直接进行运算,而无需像block那样要显式调用。上面代码首先计算前两句,然后将最后一句代码计算的结果30作为整个表达式的返回值赋值给result;