响应链
知识点:Responder相关的子类、Runloop
常见提问:在屏幕上点击一个按钮发生了什么?
响应链上的相关继承关系,你为什么可以响应?
继承至UIResponder的才可以响应
- NSObject->UIResponder->UIView->UIControl->UIButton
- NSObject->UIResponder->UIApplication
- NSObject->UIResponder->UIViewController
详细继承图见:UI继承结构图
需要记住的方法特点:
- UIResponder:touch press motion responder
- UIView:hitTest: pointInside:
- UIControl: addTarget
点击屏幕之后的整个流程
- 硬件捕捉到用户点击
- IOKit.framework封装IOHIDEvent对象,通过mach port转发到SpringBoard.app,然后接着通过mach port转发给当前app的主线程(IPC进程间通信)
- RunLoop 的source1触发,source1触发source0回调,封装为UIEvent
并放到当前活动的application的事件队列中,整个event自下而上传递 - application->window->hittest->view->hittest->button 找到第一响应者
- 之后响应链开始从上往下响应:第一响应者 -> 父视图 -> 视图控制器 直到UIApplication都没响应则丢弃
- 如果button上面叠加添加了手势,手势会覆盖住不执行target方法,除非设置手势的cancelsTouchesInView = NO
如果button上有subview,subview需要设置userInteractionEnabled=NO
Source0:用户主动触发的事件(点击button或是点击屏幕)–非基于Port的
Source1:通过内核和其他线程相互发送消息(与内核相关)–基于Port的
touch方法传递到control(uibutton)就不会再往上传递,如果需要传递,需要手动,[self.nextResponder touchesBegan:touches withEvent:event],响应链不受这个影响
实际应用
- 按钮点击没有响应
按钮的父view是UIImageView,imageView的userInteractionEnabled默认是false
父view忘记设置frame,因为没有设置clipTobounds,子view在这种时候也是能够显示出来的 - 扩展一个按钮的点击区域
巧妙使用覆写pointInside:withEvent:方法
其他
按钮、手势同时覆盖,哪个响应先后的逻辑挺恶心的,可以自行写demo试试
贴一段hittest代码
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//判断自己是否能接受事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
return nil:
}
// 判断当前点 在不在自己身上.
if (![self pointInside:point withEvent:event]) {
return nil;
}
// 查看自己是不是最合适的view,从后往前遍历自己的子控件.
int count = (int)self.subviews.count;
for (int i = count -1 ; i >= 0; i--) {
UIView *childView = self.subviews[i];
CGPoint childP = [self convertPoint:point toView:childView];
UIView *view = [childView hitTest:childP withEvent:event];
if (view) {
return view;
}
}
return self;
}
消息转发
所有方法调用在编译之后都是objc_msgSend相关方法
知识点:
struct objc_object
struct objc_class : objc_object
typedef struct objc_class *Class;
typedef struct objc_object *id;
OC是分两个版本
- Objective-C 1.0,已经废弃了不用了
- Objective-C 2.0,现在在使用的
// objc-private.h
struct objc_object {
// isa结构体
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// 下面一堆其他方法
}
// objc-runtime-new.h
// objc_class继承于objc_object,因此
// objc_class中也有isa结构体
struct objc_class : objc_object {
// ISA占8位
// Class ISA;
// superclass占8位
Class superclass;
// 缓存的是指针和vtable,目的是加速方法的调用 cache占16位
cache_t cache; // formerly cache pointer and vtable
// class_data_bits_t 相当于是class_rw_t 指针加上rr/alloc标志
class_data_bits_t bits;
// 类的方法、属性、协议等信息都保存在class_rw_t结构体中
class_rw_t *data() {
// 这里的bits就是class_data_bits_t bits;
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
// 下面一堆其他方法
}
// 类的方法、属性、协议等信息都保存在class_rw_t结构体中
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
// class_ro_t结构体存储了类在编译期就已经确定的属性、方法以及遵循的协议
const class_ro_t *ro;
// 方法信息
method_array_t methods;
// 属性信息
property_array_t properties;
// 协议信息
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
void setFlags(uint32_t set)
{
OSAtomicOr32Barrier(set, &flags);
}
void clearFlags(uint32_t clear)
{
OSAtomicXor32Barrier(clear, &flags);
}
// set and clear must not overlap
void changeFlags(uint32_t set, uint32_t clear)
{
assert((set & clear) == 0);
uint32_t oldf, newf;
do {
oldf = flags;
newf = (oldf | set) & ~clear;
} while (!OSAtomicCompareAndSwap32Barrier(oldf, newf, (volatile int32_t *)&flags));
}
};
// class_ro_t结构体存储了类在编译期就已经确定的属性、方法以及遵循的协议
// 因为在编译期就已经确定了,所以是ro(readonly)的,不可修改
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
// 方法列表
method_list_t * baseMethodList;
// 协议列表
protocol_list_t * baseProtocols;
// 变量列表
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
// 属性列表
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
objc_msgSend 走进汇编,最后调用到
// objc-runtime-new.mm
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
汇编调用部分总结objc_msgSend流程分析
lookUpImpOrForward讲解见对象方法消息传递流程
总结来说,也是先找缓存,再找方法列表,再找父类的缓存和方法列表,最后没找到就是调用msgForward
objc_objcet.isa ⇒ objc_class.bits.data ⇒ class_rw_t.ro ⇒ class_ro_t.baseMethodList
被问过,对象占多大内存这种问题,蜜汁疑惑,可能是问objc_class的占内存大小吧(40)。
下面贴下简写版本的 objc_class和objc_object,
//
// main.cpp
// RAMCPPDemo
//
// Created by rambo on 2020/11/24.
//
#include <iostream>
typedef void (*IMP)(void /* id, SEL, ... */ );
struct objc_class;
struct objc_object;
typedef struct objc_class *Class;
int main(int argc, const char * argv[]) {
struct Test {
int a;
long b;
void test();
void test2();
};
union isa_t {
Class cls;
unsigned long bits;
struct {
unsigned long nonpointer : 1;
unsigned long has_assoc : 1;
unsigned long has_cxx_dtor : 1;
unsigned long shiftcls : 33;
unsigned long magic : 6;
unsigned long weakly_referenced : 1;
unsigned long deallocating : 1;
unsigned long has_sidetable_rc : 1;
unsigned long extra_rc : 19;
};
};
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
unsigned long _key;
IMP _imp;
#endif
};
struct cache_t {
struct bucket_t *_buckets;
unsigned int _mask;
unsigned int _occupied;
};
struct class_data_bits_t {
unsigned long bits;
};
// 类的方法、属性、协议等信息都保存在class_rw_t结构体中
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
// class_ro_t结构体存储了类在编译期就已经确定的属性、方法以及遵循的协议
const class_ro_t *ro;
// 方法信息
method_array_t methods;
// 属性信息
property_array_t properties;
// 协议信息
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
// class_ro_t结构体存储了类在编译期就已经确定的属性、方法以及遵循的协议
// 因为在编译期就已经确定了,所以是ro(readonly)的,不可修改
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
// 实例变量大小,决定对象创建时要分配的内存
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
// 类名
const char * name;
// (编译时确定的)方法列表
method_list_t * baseMethodList;
// (编译时确定的)所属协议列表
protocol_list_t * baseProtocols;
//(编译时确定的)实例变量列表
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
// (编译时确定的)属性列表
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
struct objc_object {
// isa结构体
isa_t isa;
};
struct objc_class : objc_object {
// ISA占8位
// Class ISA;
// superclass占8位
Class superclass;
// 缓存的是指针和vtable,目的是加速方法的调用 cache占16位
cache_t cache; // formerly cache pointer and vtable
// class_data_bits_t 相当于是class_rw_t 指针加上rr/alloc标志 占8位
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
// 这里的bits就是class_data_bits_t bits;
return bits.data();
}
};
printf("%lu\n", sizeof(objc_class));//共占 40
return 0;
}
1.Objective-C 对象是什么?Class 是什么?id 又是什么?
2.isa 是什么?为什么要有 isa?
3.为什么在 Objective-C 中,所以的对象都用一个指针来追踪?
4.Objective-C 对象是如何被创建(alloc)和初始化(init)的?
5.Objective-C 对象的实例变量是什么?为什么不能给 Objective-C 对象动态添加实例变量?
6.Objective-C 对象的属性是什么?属性跟实例变量的区别?
7.Objective-C 对象的方法是什么?Objective-C 对象的方法在内存中的存储结构是什么样的?
8.什么是 IMP?什么是选择器 selector ?
9.消息发送和消息转发
10.Method Swizzling
11.Category
12.Associated Objects 的原理是什么?到底能不能在 Category 中给 Objective-C 类添加属性和实例变量?
13.Objective-C 中的 Protocol 是什么?
14.self 和 super 的本质
15.load 方法和 initialize 方法
isa和objc-object解析
读 objc4 源码,深入理解 Objective-C Runtime
从源代码看 ObjC 中消息的发送
KVO KVC
KVO
利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
当修改instance对象的属性时,会调用Foundation的_NSsetIntValueAndNotify函数
willChangeValueForKey:
父类原来的setter
didChangeValueForKey:
内部会触发监听器(Oberser)的监听方法(observeValueForKeyPath:ofObject:change:context:)
KVC
通过KVC修改属性会触发KVO
找setKey方法->找_setKey方法-> accessInstanceVariablesDirectly(是否可以直接访问成员变量)->_key->_isKey->key->isKey成员变量
getKey->key->isKey->_key方法-> accessInstanceVariablesDirectly->_key、_isKey、key、iskey
valueForUndefinedKey:
setValue:forUndefinedKey:
高阶用法:@avg、@count、@max、@min、@sum
@distinctUnionOfObjects
@unionOfObjects
循环引用
造成循环应用的金典场景
- delegate使用了strong
- vc引用了block,block引用了self
- timer没有释放,这个是timer持有住了self,timer一直在跑所以是没有释放,不是循环引用
- 多层引用, a->b->c->d->b a释放了,但是bcdb循环
@weaikfy(self) 和@strongify(self)宏拆解
- #define weakify(self) autoreleasepool{} _weak typeof (self) self_weak = self;
- #define strongify(self) autoreleasepool{} _strong typeof(self) self = self_weak;
1. block 里面为什么要用strong?
因为不用strong的话,self被释放掉之后执行block里面的self会失败。并且strongself是个局部变量,存在于栈中,而栈中内存系统会自动回收,也就是在block执行结束之后就会回收。.同时strongSelf使Controller的引用计数加1,致其在pop后不会立马执行dealloc销毁str属性,因为此时strongSelf持有了Controller,block执行并打印str,局部变量strongSelf被系统回收,其持有的ControllerB也会执行dealloc方法.
masonry不需要weakify,是因为masonry没有持有住block,block执行完就会被释放
2. __strong __typeof(wself) sself = wself; 为什么能起到强引用效果?
引用计数描述的是对象而不是指针。sself 强引用 wself 指向的那个对象,因此对象的引用计数会增加一个。
3 block 内部定义了sself,会不会因此强引用了 sself?
block 只有截获外部变量时,才会引用它。如果是内部新建一个,则没有任何问题。会随block执行结束而释放,因为是局部变量,存在于栈中
4 如果在 block 内部没有强引用,而是通过 if 判断是否为nil在执行,是不是也可以?
不可以!考虑到多线程执行,也许在判断的时候,self 还没释放,但是执行 self 里面的代码时,就刚好释放了。
block
block方法被转成了__main_block_impl_0,block的方法体被转成了__main_block_func_0
impl里面就会有一个局部变量,申明的时候传入外部的,这就是捕获
为什么当我们在定义block之后修改局部变量age的值,在block调用的时候无法生效。
因为block在定义的之后已经将age的值传入存储在__main_block_imp_0结构体中并在调用的时候将age从block中取出来使用,因此在block定义之后对局部变量进行改变是无法被block捕获的block内的变量已经是另一个变量了。(block内的变量已经是另一个变量了)
block会进行变量捕获
auto变量-局部变量 会在block内部专门增加一个参数来存储 值传递,因为局部变量可能会被释放
static变量 增加一个参数 指针传递
全局变量不会捕获,因为局部变量因为跨函数访问所以需要捕获,全局变量在哪里都可以访问 ,所以不用捕获
Block, 你为啥要 copy?
block为什么要用copy修饰,不用strong?
block 在内存的管理中是被当成栈对象来管理的,用copy就是讲block弄到堆上。ARC中用strong系统会默认帮我去做copy这个动作,但是建议用copy去指出这种内存行为。
深入理解iOS的block
__block
int age = 0; 变成 __Block_byref_age_0 age;
实际代码如下,然后其实就是转化为了一个指针,传入的是这个结构体的地址
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {
(void)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10}
__block不能修饰全局变量、静态变量(static)
编译器会将__block变量包装成一个对象
Runloop
1. 基于NSTimer的轮播器什么情况下会被页面滚动暂停,怎样可以不被暂停,为什么?
NSTimer不管用是因为Mode的切换,因为如果我们在主线程使用定时器,此时RunLoop的Mode为 kCFRunLoopDefaultMode,即定时器属于kCFRunLoopDefaultMode,那么此时我们滑动ScrollView时,RunLoop的Mode会切换到UITrackingRunLoopMode,因此在主线程的定时器就不在管用了,调用的方法也就不再执行了,当我们停止滑动时,RunLoop的Mode切换回kCFRunLoopDefaultMode,所有NSTimer就又管用了。若想定时器继续执行,需要将NSTimer 注册为 kCFRunLoopCommonModes 。
2. 延迟执行performSelecter相关方法是怎样被执行的?在子线程中也是一样的吗?
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
一般来说,一个线程执行完任务就会退出,如果我们需要一种机制,让线程能随时处理事件但不退出,那么RunLoop就是这样的一个机制。RunLoop是事件接收和分发机制的一个实现。
RunLoop实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能。
程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行。其他平台也有类似的RunLoop,Android,Windows。
RunLoop是在程序的入口main函数中开启的
是do while实现的
通知即将进入runloop
do {
通知将要处理timer和source
处理非延迟的主线程调用
处理source0事件
如果有source1处于ready状态,直接处理这个source1然后跳转去处理消息
通知observers:runloop的线程即将进入休眠
即将进入休眠
等待内核mach_msg事件
等待...
从等待中醒来
处理因timer的唤醒
处理异步方法唤醒,如dispatch_async
处理source1
再次确保是否有同步的方法需要调用
} while();
通知即将推出runloop
RunLoop和线程
- 每条线程都有唯一的一个与之对应的RunLoop对象
- 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要手动创建
- RunLoop在第一次获取时创建,在线程结束时销毁
- Thread包含一个CFRunloop,一个CFRunloop包含一种CFRunloopModel,model包含CFRunLoopSource、CFRunloopTimer、CGRunLoopObserver
线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
[NSRunLoop currentRunLoop];方法调用时,会先看一下字典里有没有存子线程相对用的RunLoop,如果有则直接返回RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。
一个RunLoop包含若干个mode,每个mode又包含若干source、timer、observer。每次调用RunLoop的主函数时,只能指定其中一个mode,这个mode被称作currentmode。如果需要切换mode,只能退出loop,在重新指定一个mode进入。这样做主要是为了分隔开不同组的source、timer、observer,让其互不影响。
各种mode
- kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
- UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
- kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode
Source类型:
Source0:用户主动触发的事件(点击button或是点击屏幕)–非基于Port的
Source1:通过内核和其他线程相互发送消息(与内核相关)–基于Port的
注意:Source1在处理的时候回分发一些操作给Source0去处理
NSTimer是对RunLoopTimer的封装
autoreleasepool
-
NSthread和AutoReleasePool
每一个线程都会维护自己的AutoReleasePool,而每一个AutoReleasePool都会对应唯一一个线程,但是线程可以对应多个AutoReleasePool。
线程是在线程结束的时候,调用exit之后去执行释放操作 -
NSRunLoop和AutoReleasePool
AutoReleasePool与RunLoop 与线程是一一对应的关系,AutoReleasePool在RunLoop在开始迭代时做push操作,在RunLoop休眠或者迭代结束时做pop操作。
APP启动后,系统会在主线程Runloop里注册两个observer,回调都是autoReleasePoolHandler()
第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用autoreleasePoolPush()创建自动释放池。
第二个Observer监视两个事件:BeforeWaiting(准备进入休眠)时调用autoreleasePoolPop()和autorealeasePoolPush()释放旧的池并创建新的池;Exit(即将推出Loop)时调用autoreleasePoolPop()来释放自动释放池。
autoreleasePool里的autorelease对象的加入是在runloop事件中,autoreleasePool里的autorelease对象的释放是在autoreleasePool休眠或者迭代结束时
AutoReleasePool与RunLoop 与线程是一一对应的关系,AutoReleasePool在RunLoop在开始迭代时做push操作,在RunLoop休眠或者迭代结束时做pop操
自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的
当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中
调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息
iOS 各个线程 Autorelease 对象的内存管理
Autoreleasepool 与 Runloop 的关系?
主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理
ARC 下什么样的对象由 Autoreleasepool 管理?
alloc/new/copy/mutableCopy开始的方法进行初始化时
子线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?不手动处理会内存泄漏吗?
会走进autoreleaseNoPage中,然后新建一个poolpage
Autorelease对象什么时候释放?
当前线程结束,执行pop
Autorelease & AutoreleasePool
自动释放池的前世今生 ---- 深入解析 autoreleasepool
frame和bounds
- frame不管对于位置还是大小,改变的都是自己本身
- frame的位置是以父视图的坐标系为参照,从而确定当前视图在父视图中的位置
- frame的大小改变时,当前视图的左上角位置不会发生改变,只是大小发生改变
- bounds改变位置时,改变的是子视图的位置,自身没有影响;其实就是改变了本身的坐标系原点,默认本身坐标系的原点是左上角
- bounds的大小改变时,当前视图的中心点不会发生改变,当前视图的大小发生改变,看起来效果就想缩放一样
- 旋转的时候,frame的size和bounds的size是不等的
iOS frame和Bounds 以及frame和bounds区别
深入浅出frame&bounds
GCD 多线程
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create(“net.bujige.testQueue”, DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create(“net.bujige.testQueue”, DISPATCH_QUEUE_CONCURRENT);
dispatch_get_main_queue() 主队列 串行
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) 全局并发队列
dispatch_source_t 定时器相关 set_timer set_event_hander cancel才能停止循环 dispatch_resume启动
dispatch_semaphore_create 信号量 wait等signal 信号,wait可以设置超时时间
dispatch_barrier_async
dispatch_group notiffy等待所有进入group的方法执行完才调用;wait等group执行完才会往下执行;enter leave
dispatch_once
dispatch_after
锁
- OSSpinLock 自旋锁,和线程优先级冲突会导致问题,已被弃用
- os_unfair_lock 取代不安全的OSSpinLock
- pthread_mutex 互斥锁
- NSLock 是对mutex普通锁的封装。pthread_mutex_init(mutex, NULL);
- NSRecursiveLock 是对mutex递归锁的封装,API跟NSLock基本一致
- dispatch_semaphore 信号量
- dispatch_queue(DISPATCH_QUEUE_SERIAL) 串行队列串行执行,实现先后执行,类似锁
- @synchronized 是对mutex递归锁的封装,是个关键字,@synchronized的实现原理 objc_sync_enter、objc_sync_exit
- dispatch_barrier_async
- atomic
在主线程中将同步任务压进主队列中
串行队列中,又嵌套了当前串行队列的同步执行任务就会死锁
串行队列因为宽度为1,会走进方法dispatch_barrier_sync_f中,然后互相等待对方的signal导致死锁dispatch_sync 的分析、dispatch_sync死锁问题研究
这是因为 主队列中追加的同步任务 和 主线程本身的任务 两者之间相互等待,阻塞了 『主队列』,最终造成了主队列所在的线程(主线程)死锁问题。
同步执行(sync):加在添加队列的最后面,只能在当前线程中执行任务,不具备开启新线程的能力
异步执行(async):立即执行,可以在新线程中执行任务,具备开启新线程的能力
串行队列:每次只有一个任务被执行。让任务一个接着一个地执行。
并发队列:可以让多个任务并发执行。并发队列的并发功能只有在异步(dispatch_async)的方法下才能有效。
主队列是个串行队列(dispatch_get_main_queue())
全局队列是并发队列(dispatch_get_global_queue())
『主线程』中,『不同队列』+『不同任务』简单组合的区别:
区别 | 并发队列 | 串行队列 | 主队列 |
---|---|---|---|
同步(sync) | 没有开启新线程,串行执行任务 | 没有开启新线程,串行执行任务 | ①死锁卡住不执行 |
异步(async) | 有开启新线程,并发执行任务 | 有开启新线程(1条),串行执行任务 | 没有开启新线程,串行执行任务 |
// 死锁
NSLog(@"1");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
『不同队列』+『不同任务』 组合,以及 『队列中嵌套队列』 使用的区别:
区别 | 『异步执行+并发队列』嵌套『同一个并发队列』 | 『同步执行+并发队列』嵌套『同一个并发队列』 | ②『异步执行+串行队列』嵌套『同一个串行队列』 | ③『同步执行+串行队列』嵌套『同一个串行队列』 |
---|---|---|---|---|
同步(sync) | 没有开启新的线程,串行执行任务 | 没有开启新线程,串行执行任务 | 死锁卡住不执行 | 死锁卡住不执行 |
异步(async) | 有开启新线程,并发执行任务 | 有开启新线程,并发执行任务 | 有开启新线程(1 条),串行执行任务 | 有开启新线程(1 条),串行执行任务 |
// 死锁
dispatch_queue_t syncQueue = dispatch_queue_create("test.queue5", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
// dispatch_sync(syncQueue, ^{ // ③ 同步执行 + 串行队列
dispatch_async(syncQueue, ^{ // ② 异步执行 + 串行队列
NSLog(@"2");
dispatch_sync(syncQueue, ^{ // 同步执行 + 当前串行队列
// 追加任务 1
NSLog(@"3");
});
});
NSLog(@"4");
layer和view
- layer没有响应链
- view有响应链
- view中的drawRect其实是layer的代理方法
- view是管理,layer是渲染
- view有个layer属性,可以返回它的主layer实例
- view有个layerClass方法,返回主layer所使用的的类,view的子类可以通过重载这个方法,来让view使用不同的layer来显示
引用计数ARC
ARC 在编译期间,用更底层的C接口实现retain/release/autorelease。在编译器和运行期两部分共同帮助开发者管理内存
每次runloop的时候,都会检查对象的retainCount,如果为0,说明可以释放掉
有些对象如果支持使用 TaggedPointer,苹果会直接将其指针值作为引用计数返回;如果当前设备是 64 位环境并且使用 Objective-C 2.0,那么“一些”对象会使用其 isa 指针的一部分空间来存储它的引用计数;否则 Runtime 会使用一张散列表来管理引用计数。
问题1:引用计数如何存储,TaggedPointer(this指针标识)、NONPOINTER_ISA(19bit保存引用计数)、sidetable(管理引用计算表和weak表)
引用计数原理
iOS内存管理及ARC相关实现学习(源码解析)
问题5:SideTables、tagged pointer、NoPointerISA
原理,
32位、64位指的是CPU位数,在32位CPU下占4个字节,在64位CPU下是占8个字节的。而指针类型的大小通常也是与CPU位数相关,一个指针所占用的内存在32位CPU下为4个字节,在64位CPU下也是8个字节。32 位和 64 位中的“位”,也叫字长,是 CPU 通用寄存器的数据宽度,是数据传递和处理的基本单位。字长是 CPU 的主要技术指标之一,指的是 CPU 一次能并行处理的二进制位数,字长总是8的整数倍。
获取引用计数源码
// NSObject.mm
- (NSUInteger)retainCount {
return ((id)self)->rootRetainCount();
}
// objc-object.h
// 很好的解释了上面的三个知识点 taggedpointer nonpointer sidetable
inline uintptr_t
objc_object::rootRetainCount()
{
/// http://yulingtianxia.com/blog/2015/12/06/The-Principle-of-Refenrence-Counting/
// 如果是tag pointer 当前指针就是引用计数
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
// isa 指针(NONPOINTER_ISA)
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
// sidetable存储
return sidetable_retainCount();
}
load和initialize
load每个类,父类子类分类,在main函数之前都会调用一次,仅此一次
initialize使用的时候才会调用,category覆盖->自身覆盖->父类,分类之间的覆盖看编译的先后顺序
initialize子类没有实现的话,会调用父类的实现
load、initialize方法的区别是什么?他们在Category中的调用顺序?
-
调用方式
- load是根据函数地址调用
- initialize是通过objc_msgSend调用
-
调用时刻
- load是runtime加载类、分类的时候调用(只会调用一次)
- initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会调用多次)
load、initialize调用顺序
-
load
-
先调用类的load
a) 先编译的类,优先调用load
b) 调用子类的load之前,会优先调用父类的load -
再调用分类的load
a) 先编译的分类,优先调用load
-
-
intialize
- 先初始化父类
- 再初始化子类(可能最终调用的是父类的initialize方法)
离屏渲染
在使用圆角、阴影和遮罩等视图功能的时候,图像属性的混合体被指定为在未与合成之前不能直接在屏幕中绘制,所以就需要在屏幕外的上下文中渲染,即离屏渲染
卡顿原因:离屏渲染之所以会特别消耗性能,是因为要创建一个屏幕外的缓冲区,然后从当屏缓冲区切换到屏幕外的缓冲区,然后再完成渲染;其中,创建缓冲区和切换上下文最消耗性能,而绘制其实不是性能损耗的主要原因。
触发离屏渲染:
shouldRasrize 光栅化
masks 遮罩
shadows 阴影
edge antialiasing 抗锯齿
group opatity 不透明
复杂形状设置圆角
渐变
离屏渲染指的是在图像绘制到当前屏幕前,需要先进行一次渲染,之后才绘制到当前屏幕。
OpenGL中,GPU屏幕渲染有以下两种形式:
- On-Screen Rendering即当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
- Off-Screen Rendering即离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
编译过程
- 生成辅助文件,目录结构写入,生成空的.app
- dependency 其他target
- 执行脚本run script
- 编译文件
- 链接文件:将项目中的多个可执行文件合并成一个文件;
- 拷贝资源copy bundled
- 编译 Asset 文件
- pod相关的
- 完成.app包
- 签名
编译文件的过程:
- 预处理,import头文件的导入,宏拆开
- 代码符号化
- 抽象语法树
- 静态检查,语法检查,拼写检查,方法的申明检查(是否实现的检查是在运行时处理)
- 生成llvm ir
- 优化
- 生成汇编
- 生成二进制文件可执行文件
启动过程
- 加载动态链接库dyld到app进程
- 加载动态库(包括所依赖的所有动态库)- 先加载mach-o的header和command部分
- Rebase 修复内部的指针地址,使用PIC技术解决ASLD和code sign。address space layout randomization
- 因为 Mach-O 镜像文件加载到内存中的地址和初始地址不同
Bind 修正外部指针指向,根据字符串匹配的方式查找符号表 - 动态库不编译进程序最终的二进制文件中,而是在运行的时候动态的查找调用函数的地址,调用外部符号进行绑定的过程就称作 Binding,比如项目中用到 UIView ,因为 UIView 属于 UIKit 框架,所以需要进行绑定操作
- 因为 Mach-O 镜像文件加载到内存中的地址和初始地址不同
- objc runtime
- +load attribute(construct)
美人相机启动优化
Xcode & Instruments: Measuring Launch time, CPU Usage, Memory Leaks, Energy Impact and Frame Rate
iOS App Launch time analysis and optimizations
加载dyld的过程
dyld 全名 The dynamic link editor,动态链接器
由于 iOS 系统中 UIKit / Foundation 等库每个应用都会通过 dyld 加载到内存中 , 因此 , 为了节约空间 , 苹果将这些系统库放在了一个地方 : 动态库共享缓存区 (dyld shared cache) . ( Mac OS 一样有 ) .
因此 , 类似 NSLog 的函数实现地址 , 并不会也不可能会在我们自己的工程的 Mach-O 中 , 那么我们的工程想要调用 NSLog 方法 , 如何能找到其真实的实现地址呢 ?
其流程如下 :
编译时 : 工程中所有引用了共享缓存区中的系统库方法 , 其指向的地址设置成符号地址 , ( 例如工程中有一个 NSLog , 那么编译时就会在 Mach-O 中创建一个 NSLog 的符号 , 工程中的 NSLog 就指向这个符号,这个符号就是mach-o预留出来的一段空间,其实就是符号表,存放在_DATA数据段中)
运行时 : 当 dyld将应用进程加载到内存中时 , 根据 load commands 中列出的需要加载哪些库文件 , 去做绑定的操作 ( 以 NSLog 为例 , dyld 就会去找到 Foundation 中 NSLog 的真实地址写到 _DATA 段的符号表中 NSLog 的符号上面 )
整个过程其实 PIC 技术 . ( Position Independent Code : 位置代码独立 )
类占内存大小
Class其实就是objc_class,详细见方法转发那节
MVC MVP MVVM
MVP:
MVP(MVCP)从视图层中分离了行为(事件响应)和状态(属性,用于数据展示),它创建了一个视图的抽象,也就是presenter层,而视图就是P层的『渲染』结果。P层中包含所有的视图渲染需要的动态信息,包括视图的内容(text、color)、组件是否启用(enable),除此之外还会将一些方法暴露给视图用于某些事件的响应。
VC层
view的布局和组装
view的生命周期控制
通知各个P层去获取数据然后渲染到view上面展示
controller层
生成view,实现view的代理和数据源
绑定view和presenter
调用presenter执行业务逻辑
model层
和MVC的model层类似
view层
监听P层的数据更新通知, 刷新页面展示.(MVC里由C层负责)
在点击事件触发时, 调用P层的对应方法, 并对方法执行结果进行展示.(MVC里由C层负责)
界面元素布局和动画
反馈用户操作
Presenter层职责
实现view的事件处理逻辑,暴露相应的接口给view的事件调用
调用model的接口获取数据,然后加工数据,封装成view可以直接用来显示的数据和状态
处理界面之间的跳转(这个根据实际情况来确定放在P还是C)
MVVM:
从MVP发展过来,多了数据绑定(M VM CV - MCVVM) VM其实就会P
Binder层最好的应用就是RAC(ReactiveCocoa)
Runtime相关
runtime涉及的知识点挺多的,消息转发,is_a考点,runtime.h的使用(swizzle),网上涉及的研究文章一堆,我这里贴一些
-
能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
不能向编译后得到的类中增加实例变量。因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,同时 runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量。
能向运行时创建的类中添加实例变量。运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。 -
runtime如何实现weak属性
hash全局表,weak指向的对象内存地址作为key,用所有指向该对象的weak指针表作为value。当此对象的引用计数为0的时候会dealloc,假如该对象的内存地址是a,会在weak表中搜索,找到所有以a为key的weak对象,从而设置为nil。 -
NSString*obj=[[NSDataalloc]init];
编译时是 NSString 的类型;运行时是 NSData 类型的对象 -
Method Swizzle 使用陷阱
Objective-C Method Swizzling
如何正确使用 Runtime
常见的内存问题
循环引用问题
- delegate 使用weak
- block 使用@weakify(self)
- nstimer
- 使用block和weakify的形式
- 使用中间类,让NSTimer定时中的方法由中间类转发给target执行.中间类弱持有target
- 使用消息转发的NSProxy forwardInvocation和methodSignatureForSelector
- 使用dispatch_source_t DispatchTimerSource
栈区(stack):线性结构,内存连续,系统自己管理内存,程序运行记录,每个线程,也就是每个执行序列各有一个(看crash log最容易理解),都是编译的时候能确定好的,还有一个特点就是这里面的数据可以不用指针,也不会丢。
堆区(heap):链式结构,内存不连续,最灵活的内存区,用途多多,动态分配和释放,编译时不能提前确定,我们的Objective-C对象都是这么来的,都存在这里,通常堆中的对象都是以指针来访问的,指针从线程栈中来,但不独属于某个线程,堆也是对复杂的运行时处理的基础支持,还有就是ARC还是MRC、“谁分配谁释放”说的都是堆上对象的管理。
静态区(全局区)(bss):初始化数据,简单理解就是有初始值的变量、常量。
常量区(data):未初始化数据,只声明未给值的变量,运行前统统为0,之所以单独分出来,是出于性能的考虑,因为这些东西都是0,没必要放在程序包里,也不用copy。
代码区(text):最静态的,就是只读的东西,存储代码。
NSTimer
-
nstimer与runloop配合使用
-
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES];
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];a. 两个方法等价
b. 第一个是自动添加到当前线程的Runloop中
c. Default模式在触发滑动屏幕,系统会自动切换到Tracking模式导致添加在这个runloop中的方法会不被执行,这是需要改为使用Common模式,common模式等效于default和tracking的结合。 -
[NSTimer timerWithTimeInterval:1.0 target:weakSelf
使用weakSelf的这种形式,其实在内部还是会转为strong -
并非循环引用造成的内存泄露,而是timer还在运行无法释放让持有住的target也无法被释放
NSTimer是对RunLoopTimer的封装
category和extension
extension在编译期决定,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类。
category在运行期决定。
这样就推导出,extension可以添加实例变量,但是category不行,因为运行期类的instance_size和ivar_list都已经确定了
category可以为系统的方法添加方法和使用关联对象的方式添加属性,extension不行; objc_setAssociatedObject, objc_getAssociatedObject
category添加和原类相同的方法的时候,不是替换,而是两个都存在,只是在运行时查找方法的时候是顺着方法列表的顺序查找的,分类的方法在原来的方法前面
在类的+load方法调用的时候,我们可以调用category中声明的方法,因为附加category到类的工作会先于+load方法的执行
+load的执行顺序是先类,后category,而category的+load执行顺序是根据编译顺序决定的。
但是关联对象又是存在什么地方呢? 如何存储? 对象销毁时候如何处理关联对象呢?所有关联对象都由AssociationsManager管理,它是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的key是这个对象的指针地址,而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对。
runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作
@property以及@synthesize和@dynamic
@property 自动合成getter和setter
@property 有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize 和 @dynamic 都没写,那么默认的就是 @syntheszie var = _var;。
@synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。
@dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。假如一个属性被声明为 @dynamic var,然后你没有提供 @setter 方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
property中strong和copy的区别、assign和weak
-
例子,对于不可变对象,如NSString,copy是生成一份新的内存空间,strong是指针复制。strong指向的对象如果被修改,strong的也会被修改。
-
对于可变对象,如NSMutableArray,使用 copy之后,如果调用add方法就会被奔溃,因为赋值之后生成的新对象是不可变数组
-
如果希望NSString、NSArray、NSDictionary在赋值之后不会被改变(因为有可能赋值到的是他们的可变类型子类对象),就应该使用copy。
-
copy还被用来修饰block,在ARC环境下编译器默认会用copy修饰,一般情况下在block需要捕获外界数据时该block就会被诶分配在堆区,但在MRC环境下由于手动管理引用计数,block一般被分配在栈区,可能会被释放,需要copy到堆区来防止野指针错误。使用strong也行,因为系统隐形的帮忙做了copy操作。
-
assign修饰对象会产生悬空指针的问题:修饰的对象释放后,指针不会自动置成nil,此时再向对象发消息程序会崩溃