iOS相关基础看这篇就够了

响应链

知识点: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

点击屏幕之后的整个流程

点击一个按钮的系统处理过程

Source0:用户主动触发的事件(点击button或是点击屏幕)–非基于Port的
Source1:通过内核和其他线程相互发送消息(与内核相关)–基于Port的

touch方法传递到control(uibutton)就不会再往上传递,如果需要传递,需要手动,[self.nextResponder touchesBegan:touches withEvent:event],响应链不受这个影响

实际应用

  1. 按钮点击没有响应
    按钮的父view是UIImageView,imageView的userInteractionEnabled默认是false
    父view忘记设置frame,因为没有设置clipTobounds,子view在这种时候也是能够显示出来的
  2. 扩展一个按钮的点击区域
    巧妙使用覆写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-class

// 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:)

探寻KVO本质

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

KVC的底层实现原理

循环引用

造成循环应用的金典场景

  1. delegate使用了strong
  2. vc引用了block,block引用了self
  3. timer没有释放,这个是timer持有住了self,timer一直在跑所以是没有释放,不是循环引用
  4. 多层引用, 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 里面的代码时,就刚好释放了。

iOS循环引用

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变量包装成一个对象

探寻block的本质(一)
揭露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和线程

  1. 每条线程都有唯一的一个与之对应的RunLoop对象
  2. 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要手动创建
  3. RunLoop在第一次获取时创建,在线程结束时销毁
  4. 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

  1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
  2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
  4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
  5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode

Source类型:
Source0:用户主动触发的事件(点击button或是点击屏幕)–非基于Port的
Source1:通过内核和其他线程相互发送消息(与内核相关)–基于Port的
注意:Source1在处理的时候回分发一些操作给Source0去处理

NSTimer是对RunLoopTimer的封装

iOS底层原理探究-Runloop

iOS RunLoop详解

autoreleasepool

  1. NSthread和AutoReleasePool
    每一个线程都会维护自己的AutoReleasePool,而每一个AutoReleasePool都会对应唯一一个线程,但是线程可以对应多个AutoReleasePool。
    线程是在线程结束的时候,调用exit之后去执行释放操作

  2. 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

  1. frame不管对于位置还是大小,改变的都是自己本身
  2. frame的位置是以父视图的坐标系为参照,从而确定当前视图在父视图中的位置
  3. frame的大小改变时,当前视图的左上角位置不会发生改变,只是大小发生改变
  4. bounds改变位置时,改变的是子视图的位置,自身没有影响;其实就是改变了本身的坐标系原点,默认本身坐标系的原点是左上角
  5. bounds的大小改变时,当前视图的中心点不会发生改变,当前视图的大小发生改变,看起来效果就想缩放一样
  6. 旋转的时候,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

iOS多线程:『GCD』详尽总结

iOS多线程安全-13种线程锁

  • 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");

iOS多线程:『GCD』详尽总结

layer和view

  1. layer没有响应链
  2. view有响应链
  3. view中的drawRect其实是layer的代理方法
  4. view是管理,layer是渲染
  5. view有个layer属性,可以返回它的主layer实例
  6. 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子类没有实现的话,会调用父类的实现

Category-load、initialize调用原理

load、initialize方法的区别是什么?他们在Category中的调用顺序?

  1. 调用方式

    • load是根据函数地址调用
    • initialize是通过objc_msgSend调用
  2. 调用时刻

    • load是runtime加载类、分类的时候调用(只会调用一次)
    • initialize是类第一次接收到消息的时候调用,每一个类只会initialize一次(父类的initialize方法可能会调用多次)

load、initialize调用顺序

  1. load

    • 先调用类的load
      a) 先编译的类,优先调用load
      b) 调用子类的load之前,会优先调用父类的load

    • 再调用分类的load
      a) 先编译的分类,优先调用load

  2. intialize

    • 先初始化父类
    • 再初始化子类(可能最终调用的是父类的initialize方法)

离屏渲染

在使用圆角、阴影和遮罩等视图功能的时候,图像属性的混合体被指定为在未与合成之前不能直接在屏幕中绘制,所以就需要在屏幕外的上下文中渲染,即离屏渲染

卡顿原因:离屏渲染之所以会特别消耗性能,是因为要创建一个屏幕外的缓冲区,然后从当屏缓冲区切换到屏幕外的缓冲区,然后再完成渲染;其中,创建缓冲区和切换上下文最消耗性能,而绘制其实不是性能损耗的主要原因。

触发离屏渲染:
shouldRasrize 光栅化
masks 遮罩
shadows 阴影
edge antialiasing 抗锯齿
group opatity 不透明
复杂形状设置圆角
渐变

离屏渲染指的是在图像绘制到当前屏幕前,需要先进行一次渲染,之后才绘制到当前屏幕。
OpenGL中,GPU屏幕渲染有以下两种形式:

  • On-Screen Rendering即当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
  • Off-Screen Rendering即离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

编译过程

xcode编译过程

  1. 生成辅助文件,目录结构写入,生成空的.app
  2. dependency 其他target
  3. 执行脚本run script
  4. 编译文件
  5. 链接文件:将项目中的多个可执行文件合并成一个文件;
  6. 拷贝资源copy bundled
  7. 编译 Asset 文件
  8. pod相关的
  9. 完成.app包
  10. 签名

编译文件的过程:

  1. 预处理,import头文件的导入,宏拆开
  2. 代码符号化
  3. 抽象语法树
  4. 静态检查,语法检查,拼写检查,方法的申明检查(是否实现的检查是在运行时处理)
  5. 生成llvm ir
  6. 优化
  7. 生成汇编
  8. 生成二进制文件可执行文件

启动过程

  1. 加载动态链接库dyld到app进程
  2. 加载动态库(包括所依赖的所有动态库)- 先加载mach-o的header和command部分
  3. Rebase 修复内部的指针地址,使用PIC技术解决ASLD和code sign。address space layout randomization
    • 因为 Mach-O 镜像文件加载到内存中的地址和初始地址不同
      Bind 修正外部指针指向,根据字符串匹配的方式查找符号表
    • 动态库不编译进程序最终的二进制文件中,而是在运行的时候动态的查找调用函数的地址,调用外部符号进行绑定的过程就称作 Binding,比如项目中用到 UIView ,因为 UIView 属于 UIKit 框架,所以需要进行绑定操作
  4. objc runtime
  5. +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,详细见方法转发那节

struct 占内存大小

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)

深入分析MVC、MVP、MVVM、VIPER

Runtime相关

runtime涉及的知识点挺多的,消息转发,is_a考点,runtime.h的使用(swizzle),网上涉及的研究文章一堆,我这里贴一些

  1. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?
    不能向编译后得到的类中增加实例变量。因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表和 instance_size 实例变量的内存大小已经确定,同时 runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量。
    能向运行时创建的类中添加实例变量。运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。

  2. runtime如何实现weak属性
    hash全局表,weak指向的对象内存地址作为key,用所有指向该对象的weak指针表作为value。当此对象的引用计数为0的时候会dealloc,假如该对象的内存地址是a,会在weak表中搜索,找到所有以a为key的weak对象,从而设置为nil。

  3. NSString*obj=[[NSDataalloc]init];
    编译时是 NSString 的类型;运行时是 NSData 类型的对象

  4. Method Swizzle 使用陷阱
    Objective-C Method Swizzling
    如何正确使用 Runtime

常见的内存问题

循环引用问题

  1. delegate 使用weak
  2. block 使用@weakify(self)
  3. nstimer
    • 使用block和weakify的形式
    • 使用中间类,让NSTimer定时中的方法由中间类转发给target执行.中间类弱持有target
    • 使用消息转发的NSProxy forwardInvocation和methodSignatureForSelector
    • 使用dispatch_source_t DispatchTimerSource

MLeaksFinder

内存优化

栈区(stack):线性结构,内存连续,系统自己管理内存,程序运行记录,每个线程,也就是每个执行序列各有一个(看crash log最容易理解),都是编译的时候能确定好的,还有一个特点就是这里面的数据可以不用指针,也不会丢。

堆区(heap):链式结构,内存不连续,最灵活的内存区,用途多多,动态分配和释放,编译时不能提前确定,我们的Objective-C对象都是这么来的,都存在这里,通常堆中的对象都是以指针来访问的,指针从线程栈中来,但不独属于某个线程,堆也是对复杂的运行时处理的基础支持,还有就是ARC还是MRC、“谁分配谁释放”说的都是堆上对象的管理。

静态区(全局区)(bss):初始化数据,简单理解就是有初始值的变量、常量。

常量区(data):未初始化数据,只声明未给值的变量,运行前统统为0,之所以单独分出来,是出于性能的考虑,因为这些东西都是0,没必要放在程序包里,也不用copy。

代码区(text):最静态的,就是只读的东西,存储代码。

NSTimer

  1. nstimer与runloop配合使用

  2. 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的结合。

  3. [NSTimer timerWithTimeInterval:1.0 target:weakSelf
    使用weakSelf的这种形式,其实在内部还是会转为strong

  4. 并非循环引用造成的内存泄露,而是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

  1. 例子,对于不可变对象,如NSString,copy是生成一份新的内存空间,strong是指针复制。strong指向的对象如果被修改,strong的也会被修改。

  2. 对于可变对象,如NSMutableArray,使用 copy之后,如果调用add方法就会被奔溃,因为赋值之后生成的新对象是不可变数组

  3. 如果希望NSString、NSArray、NSDictionary在赋值之后不会被改变(因为有可能赋值到的是他们的可变类型子类对象),就应该使用copy。

  4. copy还被用来修饰block,在ARC环境下编译器默认会用copy修饰,一般情况下在block需要捕获外界数据时该block就会被诶分配在堆区,但在MRC环境下由于手动管理引用计数,block一般被分配在栈区,可能会被释放,需要copy到堆区来防止野指针错误。使用strong也行,因为系统隐形的帮忙做了copy操作。

  5. assign修饰对象会产生悬空指针的问题:修饰的对象释放后,指针不会自动置成nil,此时再向对象发消息程序会崩溃

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值