语言-消息转发

CJBaseHelper/HookCJHelper

pod search CJBaseHelper

image-20240822165215629

库的使用方法,见我简书中的以下文章:

CJHook

pod search CJHook

image-20240822165517533

在调用原始方法之前插入额外的执行逻辑

1、使用场景

  1. 日志记录与监控: 在方法执行前后添加日志记录,可以帮助开发者监控应用的行为,特别是在调试和性能分析时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    - (void)swizzleMethod:(Class)class withSelector:(SEL)selector {
    Method originalMethod = class_getInstanceMethod(class, selector);
    IMP originalIMP = method_getImplementation(originalMethod);
    IMP newIMP = imp_implementationWithBlock(^(id self) {
    NSLog(@"Before method execution");
    ((void(*)(id, SEL))originalIMP)(self, selector);
    NSLog(@"After method execution");
    });
    method_setImplementation(originalMethod, newIMP);
    }

2、调用

以在进入个人主页前,需要先判断是否登录为例

常规:使用Util式、使用宏

1
2
3
4
5
6
7
8
- (void)goMineHomePage {
// 在调用原始方法之前执行的代码(进入个人主页前,需要先判断是否登录)
bool canPush = [LoginCheckerUtil checkLogin];
if (!canPush) return;

MineHomeViewController *viewController = [[MineHomeViewController alloc] init];
[self.navigationController pushViewController:viewController animated:YES];
}

改进(改进后发现,其实可读性不高,所以还不如不这么改):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// LoginCheckUtil.m
#define NeedLoginAnnotation(Class originalSelOwnerClass, SEL originalSelector) \
HookCJHelper_addPreExecutionToClassMethod(originalSelOwnerClass, originalSelector, [LoginCheckUtil class], @selector(loginCheckMethod));
class LoginCheckUtil: NSObject {
+ (bool)loginCheckMethod {
if (UserManager.isLogin) {
return YES;
}
LoginViewController *viewController = [[MineHomeViewController alloc] init];
[topVC pushViewController:viewController animated:YES];
return NO;
}
}

// HomeViewController.m
+ (void) initialize {
@NeedLoginAnnotation([self class], @selector(goMineHomePage)) // 在 + (void) initialize 中调用
}

- (void)goMineHomePage {

}

3、内部核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 在调用原始方法之前插入额外的执行逻辑
bool HookCJHelper_addPreExecutionToClassMethod(Class originalSelOwnerClass, SEL originalSelector, Class preExecSelOwnerClass, SEL preExecutionSelector) {
// 原始方法检查
if (!originalSelOwnerClass || !class_getClassMethod(originalSelOwnerClass, originalSelector)) {
return false;
}
Method originalMethod = class_getClassMethod(originalSelOwnerClass, originalSelector);
IMP originalIMP = method_getImplementation(originalMethod);
if (!originalIMP) {
return false;
}

// 要插入的方法
if (!preExecSelOwnerClass || !class_getClassMethod(preExecSelOwnerClass, preExecutionSelector)) {
return false;
}

// 创建新的方法实现
IMP newIMP = imp_implementationWithBlock(^(Class self) {
// 调用额外的逻辑(预执行类方法)
((void(*)(Class, SEL))objc_msgSend)(preExecSelOwnerClass, preExecutionSelector);
// 调用原始的类方法
((void(*)(Class, SEL))originalIMP)(self, originalSelector);
});

// 替换原始方法的实现为新的实现
method_setImplementation(originalMethod, newIMP);

return true;
}

  1. 什么时候会报unrecognized selector的异常
  • 当调用该对象上某个方法,而该对象上没有实现这个方法的时候。可以通过消息转发进行解决,流程见下图

    img

    image

  • OC在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX
    但是在这之前,OC的运行时会给出三次拯救程序崩溃的机会

  • Method resolution(消息动态解析
    OC运行时会调用+resolveInstanceMethod:或者+resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则,运行时就会移到下一步,消息转发(Message Forwarding

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 重写 resolveInstanceMethod: 添加对象方法实现
    + (BOOL)resolveInstanceMethod:(SEL)sel {
    // 如果是执行 run 函数,就动态解析,指定新的 IMP
    if (sel == NSSelectorFromString(@"run:")) {
    // class: 给哪个类添加方法
    // SEL: 添加哪个方法
    // IMP: 方法实现 => 函数 => 函数入口 => 函数名
    // type: 方法类型:void用v来表示,id参数用@来表示,SEL用:来表示
    class_addMethod(self, sel, (IMP)runMethod, "v@:@");
    return YES;
    }
    return [super resolveInstanceMethod:sel];
    }

    //新的 run 函数
    void runMethod(id self, SEL _cmd, NSNumber *meter) {
    NSLog(@"跑了%@", meter);
    }
  • Fast forwarding(消息接受者重定向
    如果目标对象实现了-forwardingTargetForSelector:Runtime这时就会调用这个方法,给你把这个消息转发给其他对象的机会。只要这个方法返回的不是nilself,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点

    1
    2
    3
    4
    5
    6
    7
    8
    // 消息接受者重定向
    - (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(run:)) {
    return [[Person alloc] init];
    // 返回 Person 对象,让 Person 对象接收这个消息
    }
    return [super forwardingTargetForSelector:aSelector];
    }
    • Normal forwarding(消息重定向
      这一步是Runtime最后一次给你挽救的机会。首先它会发送-methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nilRuntime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // 获取函数的参数和返回值类型,返回签名
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"run:"]) {
    return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
    }

    // 消息重定向
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 从 anInvocation 中获取消息
    SEL sel = anInvocation.selector;
    if (sel == NSSelectorFromString(@"run:")) {
    // 1\. 指定当前类的一个方法作为IMP
    // anInvocation.selector = @selector(readBook:);
    // [anInvocation invoke];

    // 2\. 指定其他类来执行这个IMP
    Person *p = [[Person alloc] init];
    // 判断 Person 对象方法是否可以响应 sel
    if([p respondsToSelector:sel]) {
    // 若可以响应,则将消息转发给其他对象处理
    [anInvocation invokeWithTarget:p];
    } else {
    // 若仍然无法响应,则报错:找不到响应方法
    [self doesNotRecognizeSelector:sel];
    }
    }else{
    [super forwardInvocation:anInvocation];
    }
    }

    - (void)doesNotRecognizeSelector:(SEL)aSelector {
    [super doesNotRecognizeSelector:aSelector];
    }

    既然-forwardingTargetForSelector:-forwardInvocation:都可以将消息转发给其他对象处理,那么两者的区别在哪?
    区别就在于-forwardingTargetForSelector:只能将消息转发给一个对象。而-forwardInvocation:可以把消息存储,在你觉得合适的时机转发出去,或者不处理这个消息。修改消息的target,selector,参数等。将消息转发给多个对象

线程-③任务或线程与锁

四、线程安全问题

1、线程安全

当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。就好比几个人在同一时修改同一个表格,造成数据的错乱。

参考:iOS中保证线程安全的几种方式与性能对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
性能对比
对以上各个锁进行1000000此的加锁解锁的空操作时间如下:
OSSpinLock: 46.15 ms
dispatch_semaphore: 56.50 ms
pthread_mutex: 178.28 ms
NSCondition: 193.38 ms
NSLock: 175.02 ms
pthread_mutex(recursive): 172.56 ms
NSRecursiveLock: 157.44 ms
NSConditionLock: 490.04 ms
@synchronized: 371.17 ms

总的来说:

OSSpinLock 自旋锁和 dispatch_semaphore 信号量的效率远远高于其他。
@synchronized和NSConditionLock效率较差。

OSSpinLock 自旋锁原理很简单,就是一直 do while 忙等。它的缺点是当等待时会消耗大量 CPU 资源,所以它不适用于较长时间的任务。 不过最近YY大神在自己的博客不再安全的 OSSpinLock中说明了OSSpinLock已经不再安全,请大家谨慎使用。
dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。


鉴于OSSpinLock的不安全,所以我们在开发中如果考虑性能的话,建议使用dispatch_semaphore。
如果不考虑性能,只是图个方便的话,那就使用@synchronized。

NSDateFormatter在iOS7之后(包括iOS7)才是线程安全的

2、死锁

死锁是指两个或两个以上的进程(线程)在运行过程中因争夺资源而造成的一种僵局(Deadly-Embrace) ) ,若无外力作用,这些进程(线程)都将无法向前推进。

死锁的4个必要条件

一、@synchronized

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSObject *obj = [[NSObject alloc] init];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized(obj) {
NSLog(@"需要线程同步的操作1 开始");
sleep(3);
NSLog(@"需要线程同步的操作1 结束");
}
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
@synchronized(obj) {
NSLog(@"需要线程同步的操作2");
}
});

@synchronized(obj)指令使用的obj为该锁的唯一标识,只有当标识相同时,才为满足互斥,如果线程2中的@synchronized(obj)改为@synchronized(self),刚线程2就不会被阻塞,

二、NSLock

1
2
3
4
5
6
7
8
9
NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[lock lock]; // 加锁
NSLog(@"需要线程同步的操作1 开始");
sleep(2);
NSLog(@"需要线程同步的操作1 结束");
[lock unlock]; // 解锁

});

三、NSRecursiveLock递归锁的使用

我们先写一个典型的死锁情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
NSLock *lock = [[NSLock alloc] init];	// 此锁在本代码中会造成死锁
//NSRecursiveLock *lock = [[NSRecursiveLock alloc] init]; //要想下面的递归调用不会造成死锁,只要这里将锁改成递归锁NSRecursiveLock就可以了。NSRecursiveLock递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。这主要是用在循环或递归操作中。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

static void (^RecursiveMethod)(int);

RecursiveMethod = ^(int value) {
[lock lock];
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveMethod(value - 1); // 此时lock还没解锁,就有执行这个block,而block里又给加了次锁,从而造成了死锁
}
[lock unlock];
};

RecursiveMethod(5);
});

在是跟你面的代码中,在我们的线程中,RecursiveMethod是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSLock *lock = [[NSLock alloc] init];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

static void (^RecursiveMethod)(int);

RecursiveMethod = ^(int value) {
if (value > 0) {
NSLog(@"value = %d", value);
sleep(2);
RecursiveMethod(value - 1);
}
};

[lock lock];
RecursiveMethod(5);
[lock unlock];
});

四、NSCondition & NSConditionLock 条件锁

wait方法是傻等;

waitUntilDate:方法是等一会;

signal是唤起一个在等待的线程。

通过wait()waitUntilDate(limit: NSDate) -> Bool这两个方法都可以实现线程阻塞即线程睡眠。

不同之处在于

wait()会使线程一直处于休眠状态,直到收到signal()为止;

waitUntilDate(limit: NSDate) -> Bool在使线程睡眠的同时会设置睡眠的终止时间。

如果在终止时间前收到了signal()就会唤醒线程;
当到达终止时间的时候,即使没有收到signal(),也会直接唤醒线程,而不会像wait()方法那样一直睡眠下去。

1、NSCondition

基本的条件锁,手动的控制。wait

2、NSConditionLock 条件锁

条件锁,这里的条件并不是bool表达式中的条件,而是一个特定的int值。

条件锁,不是简单的加锁/解锁, 而是需要根据一定条件满足后进行 加锁/解锁.

以一个生产中与消费者的例子,介绍条件锁的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static NSInteger CONDITION_NO_DATA        //条件一: 没有数据
static NSInteger CONDITION_HAS_DATA //条件二: 有数据

// 初始化锁时,指定一个默认的条件
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:CONDITION_NO_DATA];

//生产者,加锁与解锁的过程
while (YES) {

//1. 当满足 【没有数据的条件时】进行加锁
[lock lockWhenCondition:CONDITION_NO_DATA];

//2. 生产者生成数据
//.....

//3. 解锁,并设置新的条件,已经有数据了
[locker unlockWithCondition:CONDITION_HAS_DATA];
}


//消费者,加锁与解锁的过程
while (YES) {

//1. 当满足 【有数据的条件时】进行加锁
[lock lockWhenCondition:CONDITION_HAS_DATA];

//2. 消费者消费数据
//.....

//3. 解锁,并设置新的条件,没有数据了
[locker unlockWithCondition:CONDITION_NO_DATA];
}

五、OSSpinLock自旋锁 & os_unfair_lock

由于OSSpinLock不再安全,所以这里我们就直接说一下os_unfair_lock,这个是苹果用于代替OSSpinLock的。效率很高用法很简单如下

1
2
3
4
5
os_unfair_lock_t lock = OS_UNFAIR_LOCK_INIT;

os_unfair_lock_lock(&lock);
NSLog(@"os_unfair_lock_t");
os_unfair_lock_unlock(&lock);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#import "ViewController.h"
#import <libkern/OSAtomic.h>
@interface ViewController ()
@property (nonatomic,assign) int ticket;
//@property (nonatomic,assign) OSSpinLock lock;
@end

@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// self.lock = OS_SPINLOCK_INIT;
self.ticket=50;
[self ticketsTest];
// Do any additional setup after loading the view.
}

-(void)ticketsTest{
// 这里我们开两个线程处理同一件事
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i =0; i<5; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (int i =0; i<5; i++) {
[self saleTicket];
}
});
}

-(void)saleTicket{
//静态创建、则不需要新建属性
static OSSpinLock lock = OS_SPINLOCK_INIT;
//若后面是个函数、则需
/*
static OSSpinLock lock = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lock = OS_SPINLOCK_INIT;
});
*/
//其他线程执行到这里时,发现锁被加锁了 就会再这排队等待、直到这个锁被打开
//加锁
OSSpinLockLock(&lock);
int ticket = self.ticket;
sleep(.2);
ticket--;
self.ticket=ticket;
NSLog(@"%d",self.ticket);
//解锁
OSSpinLockUnlock(&lock);
}

iOS - 互斥锁&&自旋锁 多线程安全隐患

iOS - 互斥锁&&自旋锁 多线程安全隐患

一、多线程安全隐患

资源共享
一块资源可能会被多个线程共享,也就是多个线程可能会访问到一块资源
比如多个线程访问同一个对象,同一个变量,同一个文件。
当多线程访问同一块资源的时候,很容易引发数据错乱和数据安全问题
二、原子和非原子属性
1>OC 在定义属性的时候有nonatomic和atomic两种选择
* atomic:原子属性,为 setter 方法加锁
* nonatomic:非原子属性,不会为 setter 方法加锁
普通情况下都是在主线程做操作,所以一般都不会加锁。
对比:
*** atomic:线程安全,需要消耗大量的资源
*** nonatomic
:非线程安全,适合内存小的移动设备
2>synchronized 与 atomic
* synchronized:互斥锁
* atomic:自旋锁
共同点:都能保证同一时刻只能有一个线程操作锁住的代码
区别
互斥锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕,当上一个线程的任务执行完毕,下一个线程会. 自动唤醒然后执行任务。
自旋锁:当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会一直等待(不会睡眠),当上一个线程的任务执行完毕,下一个线程会立即执行。
自旋锁应用场景:比较适合做一些不耗时的操作
三、互斥锁
· 注意点:
- 如果多线程访问同一个资源,那么必须使用同一把锁才能锁住
- 在开发中,尽量不要加锁,能在服务端做尽量在服务端做,如果必须要加锁,一定要记住,锁的范围不能太大,哪里有安全隐患就加在哪里。
技巧:因为必须使用同一把锁,开发中如果需要加锁,直接使用 self 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@synchronized(self) {
//线程1进来之后,锁住,2和3都再外面等待
//1、查询剩余的票数 NSUInteger count = self.totalCount;
//2、判断是否还有余票
//2.1卖票
//3 、提示客户,没有票了
if (count>0) {
[NSThread sleepForTimeInterval:0.1];
self.totalCount = count-1;
NSLog(@"%@卖了一张票,还剩%zd票",[NSThread currentThread].name,self.totalCount);
}
else
{
NSLog(@"没票了");
break;
}
}
//解锁

四、自旋锁
注意点:
只会给 setter 方法加锁,并不会给getter方法加锁。

ObjC 多线程简析(一)-多线程简述和线程锁的基本应用

在iOS10之后apple废弃了OSSpinLock自旋锁,使用os_unfair_lock互斥锁来替代。

在iOS10之后apple已经不再建议使用OSSpinLock自旋锁了,它的替代方案是一个互斥锁,所以一般情况下我们使用互斥锁来解决线程同步的问题才是比较合理的。

1
2
3
4
5
6
7
8
9
10
11
// 初始化OSSpinLock
_osspinlock = OS_SPINLOCK_INIT;

// 加锁
OSSpinLockLock(&_osspinlock);

// 操作数据
// ...

// 解锁
OSSpinLockUnlock(&_osspinlock);
1
2
3
4
5
6
7
8
9
10
11
// 初始化os_unfair_lock
_osunfairLock = OS_UNFAIR_LOCK_INIT;

// 加锁
os_unfair_lock_lock(&(_osunfairLock));

// 操作数据
// ...

// 解锁
os_unfair_lock_unlock(&(_osunfairLock));

iOS中的线程同步方案

自旋锁、互斥锁比较:

a, 什么情况使用自旋锁比较划算?

预计线程等待锁的时间很短

加锁的代码(临界区)经常被调用,但竞争情况很少发生

CPU资源不紧张

多核处理器

b,什么情况使用互斥锁比较划算?

预计线程等待锁的时间较长

单核处理器

临界区有IO操作

临界区代码复杂或者循环量大

临界区竞争非常激烈

(4)、自己延伸的问题:什么队列能同时存在同步任务和异步任务?(并发队列)

eg:主线程的主队列是串行队列,不可能在该队列上添加同步任务,否则会造成死锁。所以,同时存在同步任务和异步任务的队列应该只能是并发队列。

最终的执行顺序,还是看这些各式各样的任务所在的队列是什么队列,串行的还是并发的。

1、死锁探究:GCD死锁及报错提示(EXC_BAD_INSTRUCTION)

(1)、死锁举例:通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
- (IBAction)testDeadlock1:(id)sender {
//测试死锁
dispatch_queue_t q1 = dispatch_queue_create("serial_queue_1", DISPATCH_QUEUE_SERIAL);
dispatch_async(q1, ^{
NSLog(@"1(属任务①).验证通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁%@", [NSThread currentThread]);
dispatch_sync(q1, ^{ // 添加的同步任务和外面的任务是同一个串行队列
sleep(2);
NSLog(@"2(属任务②).验证通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁%@", [NSThread currentThread]);
});
NSLog(@"3(属任务①).验证通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁%@", [NSThread currentThread]);
});
}
(2)、死锁结论:往串行队列里添加的同步任务不能卡住该串行队列,否则会造成死锁(这句话非常重要)
(3)、死锁原因分析:

要了解通过串行队列里的任务,往这个串行队列里添加同步任务,会造成死锁,或者说往串行队列里添加的同步任务卡住该串行队列的时候会发生死锁的原因,我们首先需要知道几个概念:

知识点①:创建同步任务的操作,在创建完之后,需要立马执行所创建的同步任务。即创建完同步任务的操作后,只有连其所创建的同步任务也结束了才能继续执行下去;

知识点②:而创建异步任务的操作,在创建完之后,即可继续执行下去,不需要等待所创建的异步任务结束。

附:在线程组的使用中,我们会遇到需要监听多个子线程中的所有任务都结束后,才去执行最后的更新UI的操作。假设最后更新UI的操作,除了需要子线程的任务结束,还需要子线程中的异步线程,如网络请求也结束后,才能更新UI,那么为了不让子线程提前退出,而是等到网络请求也结束后才退出,我们通常会通过使用gcd的enter leave或者信号量来等待。

知识点③:串行队列里的任务是一个执行完,才接着执行下一个;

所以,由以上①②③知识点,我们就能分析出其造成死锁的原因如下:

首先,对于标记123,先只看是同步任务还是异步任务,而不管是串行队列,还是并行队列,或者说是哪个队列。

从代码上我们很容易看出其是同步任务,所以由以上同步的知识点②,可以知道,最终标记输出的正确顺序应该依次是标记1、标记2、标记3

即我们的关注点是标记2能否在标记1之后输出。

答,在上述代码中,标记2处的同步任务是被添加到串行队列的,而且还是当前的串行队列。

我们知道串行队列里的任务是一个执行完,才接着执行下一个的,也就是说,往串行队列里添加的任务要执行的条件一定是在所添加的新任务之前的所有任务都已经全部执行完了后,才会执行到这一个的。

这里我们依次往串行队列里添加了第一个任务块和第二个任务块。

要完成第一个任务块需要同时完成任务1+第二个任务块2+任务3

第二个任务块要执行,根据串行队列的性质,我们知道第二个任务块要等待第一个任务块结束才会执行。(如果两个任务即使都是添加到串行队列,但是他们是不同串行队列的时候就不会需要等待)

由此死锁。

显然这里任务②需要等到任务①真正完成,而任务①的真正完成需要等任务②完成,这样的一个互相等待也就构成了一个死锁,导致我们** EXC_BAD_INSTRUCTION**的崩溃了。

那么以上死锁的问题,怎么解决呢?

答:其实只要解决标记2即任务②,可以在标记1执行之后执行就可以了。
解决方法有:将任务②改成异步任务,或者将任务②这个同步任务添加到非本串行队列下,可以是其他串行队列,也可以是其他并行队列都可以。即以下这种修改方案是能够解决死锁的。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (IBAction)testDeadlock2:(id)sender {
//测试死锁
dispatch_queue_t q1 = dispatch_queue_create("serial_queue_1", DISPATCH_QUEUE_SERIAL);
dispatch_async(q1, ^{
​ NSLog(@"1(属任务①).验证通过串行队列里的任务,往其他串行队列里添加同步任务,不会造成死锁%@", [NSThread currentThread]);
​ dispatch_queue_t q2 = dispatch_queue_create("serial_queue_2", DISPATCH_QUEUE_SERIAL);
​ dispatch_sync(q2, ^{
​ sleep(2);
​ NSLog(@"2(属任务②).验证通过串行队列里的任务,往其他串行队列里添加同步任务,不会造成死锁%@", [NSThread currentThread]);
​ });
​ NSLog(@"3(属任务①).验证通过串行队列里的任务,往其他串行队列里添加同步任务,不会造成死锁%@", [NSThread currentThread]);
});
}

2、主队列中的死锁:在主队列开启同步任务,一定为什么会阻塞线程(看同步任务是不是加在主队列里去了)?

回答本问题前,我们需要先了解的知识点是:

知识点①:主线程和主队列的关系:

主队列是主线程中的一个串行队列。每一个应用程序只有唯一的一个主队列用来update UI。所有的和UI的操作(刷新或者点击按钮)都必须在主线程中的主队列中去执行,否则无法更新UI。

因为主线程是一个串行队列,所以往主队列里添加同步任务(如果不是往主队列添加同步任务就不会)是很有可能发生死锁卡死的。如以下代码就会发生死锁。代码如下:

1
2
3
4
5
6
7
8
9
10
- (void)viewDidLoad {
[super viewDidLoad];

NSLog(@"打印1");

dispatch_sync(dispatch_get_main_queue(), ^{
​ NSLog(@"打印2");
});
NSLog(@"打印3");
}

由引述,我们已经知道往串行队列里添加的同步任务,如果卡住的是该串行队列,则会发生死锁,所以显然即执行往这个串行队列里添加同步任务的该任务也是在这个串行队列里的话,那么由于相互等待会造成死锁。

最简单的解决方法:将sync同步方法,替换成异步方法

1
2
3
4
5
6
7
8
9
10
11
12
- (void)viewDidLoad {
[super viewDidLoad];

NSLog(@"标记1");

dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"标记3");
});

NSLog(@"标记5");
}
//输出结果为 标记1、标记5、标记3

其他修改方法:将同步任务卡住的队列改成并发队列。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)testDeadLock {
NSLog(@"标记1");
dispatch_queue_t other_queue = dispatch_queue_create("other_queue", DISPATCH_QUEUE_SERIAL);//不管是串行队列还是并发队列,都能解决这个死锁问题,因为它同步方法没有卡住这个other_queue。
dispatch_async(other_queue, ^{
​ NSLog(@"标记2");
​ dispatch_sync(dispatch_get_main_queue(), ^{
​ NSLog(@"标记3");
​ });
​ NSLog(@"标记4");
});
sleep(2);
NSLog(@"标记5");
}
//输出结果为 标记1、标记2、标记5、标记3、标记4。

有了这些基础,你再看以下文章中的例子时,就能轻松判断是否会造成死锁了 。

iOS中自旋锁与互斥锁的区别

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。(附:获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active活跃状态的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快)

其他参考文章:iOS中自旋锁与互斥锁的区别

pthread_mutex 表示互斥锁。互斥锁可以传入不同参数,实现递归锁pthread_mutex(recursive)。

NSLock,NSCondition,NSRecursiveLock,NSConditionLock都是内部封装的pthread_mutex,即都属于互斥锁。@synchronized是NSLock的一种封装,牺牲了效率,简洁了语法。

OSSpinLock 表示自旋锁,从上图可以看到自旋锁的效率最高,但是现在的iOS因为优先级反转的问题,已经不安全,所以推荐使用pthread_mutex或者dispatch_semaphore。

总结
  自旋锁会忙等: 所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是不停循环在那里,直到被锁资源释放锁。
  互斥锁会休眠: 所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时cpu可以调度其他线程工作。直到被锁资源释放锁。此时会唤醒休眠线程。

优缺点
  自旋锁的优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度,cpu时间片轮转等耗时操作。所有如果能在很短的时间内获得锁,自旋锁的效率远高于互斥锁。
  缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用。

语言-OC Runtime

Runtime

前言:

Objective C的runtime技术功能非常强大,能够在运行时获取并修改类的各种信息,包括获取方法列表、属性列表、变量列表,修改方法、属性,增加方法,属性等等,本文对相关的几个要点做了一个小结。

目录:

(1)使用class_replaceMethod/class_addMethod函数在运行时对函数进行动态替换或增加新函数

(2)重载forwardingTargetForSelector,将无法处理的selector转发给其他对象

(3)重载resolveInstanceMethod,从而在无法处理某个selector时,动态添加一个selector

(4)使用class_copyPropertyList及property_getName获取类的属性列表及每个属性的名称

(5) 使用class_copyMethodList获取类的所有方法列表

应用:

viewWillAppear 埋点

KVO的本质

问: Category的实现原理,以及Category为什么只能加方法不能直接加属性(需要自己在分类中实现setter、getter方法)?

答:分类的实现原理是将category中的方法,属性,协议数据放在category_t结构体中,然后将结构体内的方法列表拷贝到类对象的方法列表中。 Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量。

一些参考

Objective C运行时(runtime)技术总结

需求1、当项目中,需要继承某一个父类,但是父类中并没有提供我们需要的调用方法,一般情况下,这种时候,我们可能会再派生一个类,或者会直接为这个类写一个分类(category),在这个分类中,我们可以新增或者替换某个方法(注意:不推荐在分类中重写方法,而且也无法通过 super 来获取所谓父类的方法)。大致在这两种情况下,我们可以通过 class_addMethod 来实现我们想要的效果。

先从一个场景问题带出吧,比如一个应用都是继承于UIViewController,刚开始时候不要求要转屏,到后面才决定加上旋转屏适配,这个时候如果我们去创建一个父类,并让去修改每个类的继承这个方法显然是效率太低了。而且有时候,我们也不想多创个父类,于是我们就只能决定一次过替换掉这些controller里的viewWillAppear:和 willAnimateRotationToInterfaceOrientation:duration:,换成自己的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class clazz = object_getClass((id)self);

//将系统的resolveInstanceMethod替换成自己的myResolveInstanceMethod,而系统的resolveInstanceMethod方法就是当运行时对象调用了一个找不到的方法的时候系统会去寻找的机制。
[clazz swizzleMethod:@selector(resolveInstanceMethod:) withMethod:@selector(myResolveInstanceMethod:)];
});
}


/**
* 用什么方法替换掉什么方法
*
* @param origSel 原方法选择子
* @param aftSel 新方法选择子
*/
+ (BOOL)swizzleMethod:(SEL)origSel withMethod:(SEL)aftSel {
//通过class_getInstanceMethod()拿到对应的Method
Method originMethod = class_getInstanceMethod(self, origSel);
Method newMethod = class_getInstanceMethod(self, aftSel);

if(originMethod && newMethod){ //必须两个Method都要拿到
//class_addMethod将本来不存在于被操作的Class里的newMethod的实现添加在被操作的Class里,并使用origSel作为其选择子
if(class_addMethod(self, origSel, method_getImplementation(newMethod),method_getTypeEncoding(newMethod))) {
//当addMethod成功完成后,利用class_replaceMethod换掉method_getImplaementation(oiginMethod)的选择子,将原方法的实现的SEL换成新方法的SEL:aftSel,ok目的达成了。想一想,现在通过旧方法SEL来调用,就会实现新方法的IMP,通过新方法的SEL来调用,就会实现旧方法的IMP。
class_replaceMethod(self, aftSel, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}
return YES;
}
return NO;
}

语言-消息转发NSProxy

一、NSProxy

NSProxy,你可以通过继承它,并重写它的以下两个方法以实现消息转发到另一个实例。说白了,NSProxy专为代理而生(负责将消息转发到真正的target的代理类)。从类名来看是代理类,专门负责代理对象转发消息的。相比NSObject类来说NSProxy更轻量级,通过NSProxy可以帮助Objective-C间接的实现多重继承的功能

1
2
3
- (void)forwardInvocation:(NSInvocation *)anInvocation;

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;

1、通过NSProxy可以帮助Objective-C间接的实现多重继承的功能的例子

现在比较流行的说法是用它来模拟多重继承,大致过程就是让它Hold住/包含你要实现多继承的类的对象,然后被hold住的对象的行为定义在接口中,并让Proxy去实现这些接口。然后再转发的时候把消息转发到实现了该接口的对象去执行,这样就好像实现了多重继承一样。注意:这个真不是多重继承,只是包含,然后把消息路由到指定的对象而已,其实完全可以用NSObject类来实现。

天使=人+鸟

1
2
3
4
5
6
Person *person = [Person new];
Bird *bird = [Bird new];

id angelProxy = [AngelProxy proxyForObject1:person object2:bird];
[proxy speak]; // 人可以说话
[proxy fly]; // 鸟可以飞

另一个例子:NSProxy的学习使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// AngelProxy.h
#import <Foundation/Foundation.h>

@interface AngelProxy : NSProxy

+ (id)proxyForObject1:(id)obj1 object2:(id)obj2;

@end




// AngelProxy.m
#import "AngelProxy.h"

@interface AngelProxy()

@property (nonatomic, weak) id target1;
@property (nonatomic, weak) id target2;
@end

@implementation AngelProxy

- (instancetype)initWithTarget1:(id)obj1 target2:(id)obj2 {
_target1 = obj1;
_target2 = obj2;

return self;
}

+ (id)proxyForObject1:(id)obj1 object2:(id)obj2 {
return [[AngelProxy alloc] initWithTarget1:obj1 target2:obj2];
}

// 重写NSProxy如下两个方法,在处理消息转发时,将消息转发给真正的Target处理
- (void)forwardInvocation:(NSInvocation *)invocation {
id target;
if ([_target1 methodSignatureForSelector:invocation.selector]) {
target = _target1;
}else{
target = _target2;
}
[invocation invokeWithTarget:target];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
NSMethodSignature *sig;

sig = [_target1 methodSignatureForSelector:selector];
if (sig) {
return sig;
}

sig = [_target2 methodSignatureForSelector:selector];
return sig;
}


@end

2、NSProxy 和 NSObject 的比较

相比NSObject,NSProxy更轻量级,做消息转发效率更高。

NSObject寻找方法顺序:本类->(父类-)>(动态方法解析)-> 消息转发;
NSproxy顺序:本类->消息转发

3、NSProxy的用途

  1. AOP面向切片编程

    iOS中面向切片编程一般有两种方式 ,一个是直接基于runtime 的method-Swizzling.还有一种就是基于NSProxy

  2. 解决NSTimer, CADisplayLink等强引用target引起的无法释放问题。如NSTimer:利用消息转发来断开NSTimer对象与视图之间的强引用关系。初始化NSTimer时把触发事件的target替换成一个单独的对象,然后这个对象中NSTimer的SEL方法触发时让这个方法在当前的视图self中实现。

    1
    2
    3
    4
    self.timer = [NSTimer timerWithTimeInterval:1 target:self.proxy 
    selector:@selector(timerEvent) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
    // 这里我们timer的target使用AYProxy对象,它会实现你这个timer想要实现的timerEvent消息转发给self。从而避免了循环引用

    协议类代码为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #import

    @interface AYProxy : NSProxy

    @property (nonatomic, weak) id obj;

    @end

    #import "AYProxy.h"

    @implementation AYProxy

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSMethodSignature *sig = nil;
    sig = [self.obj methodSignatureForSelector:aSelector];
    return sig;
    }

    - (void)forwardInvocation:(NSInvocation *)anInvocation{
    [anInvocation invokeWithTarget:self.obj];

    }

    @end
  3. 多重继承

    实现类似CAAnimation类族.

语言-请求拦截NSURLProtocol

一、NSURLProtocol

如果开发者自定义的一个 NSURLProtocol 并且注册到 app 中,那么在这个自定义的 NSURLProtocol 中我们可以拦截所有的请求((注意,不只是webView内的请求,而是整个app内的所有请求)),进行修改,或者修改 response。即NSURLProtocol这个东西的作用可以让我们在app的内部拦截一切url请求。

「或者这么说吧: NSURLProtocol 就是一个苹果允许的中间人攻击。」

注意事项:

如果我们顺序注册 A B C 三个 Protocol,那么一个 connection 在发送的时候,处理的顺序是 C B A,而且最多只有一个 Protocol 会触发处理。

拦截 UIWebview 的请求,会有被拒的风险。

附:

iOS的Foundation框架提供了 URL Loading System 这个库(后面简写为ULS),所有基于URL(例如http://,https:// ,ftp://这些应用层的传输协议)的协议都可以通过ULS提供的基础类和协议来实现。而ULS库里提供了一个强有力的武器 NSURLProtocol。

NSURLProtocol能做什么?

举几个例子:

  • 全局网络请求设置:我们的APP内的所有请求都需要增加公共的头,像这种我们就可以直接通过NSURLProtocol来实现,当然实现的方式有很多种
  • 自定义 Response (过滤敏感信息)
  • 再比如我们需要将APP某个API进行一些访问的统计
  • 再比如我们需要统计APP内的网络请求失败率

语言:Swift相关知识

Swift相关知识

一、iOS开发中使用OC和swift的对比

1
2
3
1.import的类
OC:某个只要要使用某个类就要将该类import。
swift:如果是用户自己创建类,其他类无需import可以直接使用。pod的一些三方类和系统的一些类,在使用的时候需要import

Guard语句的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
guard expression else { 
//语句
//必须包含一个控制语句:return,break,continue或throw。
}
这里,expression是一个布尔表达式(返回true或者false
如果对表达式求值falseguard则执行代码块内的语句
如果对表达式求值trueguard则从执行中跳过代码块内的语句

func someFunction() {
guard false else {
print("Condition not met")
return
}
print("Condition met")
}

二、【iOS】Swift和OC协议的区别?为何Swift是面向协议的?

【iOS】Swift和OC协议的区别?为何Swift是面向协议的?

相比于OC,Swift 可以做到协议方法的具体实现,而 OC 则不行。

OC中协议(Protocol)类似一个类的接口,只声明,不实现。
只能定义公用的一套接口,但不能提供具体的实现方法。也就是说,它只告诉你要做什么,但具体怎么做不关心。具体的实现要在遵守这个协议的类中实现。

OC的Protocol与继承的区别

继承连同方法的实现也继承了,而protocol只有声明没有实现;
protocol能够作用到不同类型的类上。

OC的Protocol与Category的区别

Category可以给一个类扩充方法,既有申明也有实现;而Protocol只有声明,没有实现。
同:CategoryProtocol都可以声明方法,不能声明属性。

Swift 面试题解

?? 的作用

可选值的默认值, 当可选值为nil 的时候, 会返回后面的值. 如
let someValue = optional1 ?? 0

远程共享文件夹

一、MacBook Pro 怎么设置smb共享文件夹

在Mac要设置smb的共享文件夹,那么如何设置的好的呢?如何去设置一共享文件夹?

MacBook Pro 怎么设置smb共享文件夹

方法/步骤

1.

右键一个桌面新建文件夹,然后点击新建文件夹。

MacBook Pro 怎么设置smb共享文件夹

MacBook Pro 怎么设置smb共享文件夹

2.

把要共享文件,拖入新建文件夹,然后点击logo。

MacBook Pro 怎么设置smb共享文件夹

MacBook Pro 怎么设置smb共享文件夹

3.

点击设置,点击共享。

MacBook Pro 怎么设置smb共享文件夹

MacBook Pro 怎么设置smb共享文件夹

4.

文件共享勾上,点击加。

MacBook Pro 怎么设置smb共享文件夹

MacBook Pro 怎么设置smb共享文件夹

5.

然后点击桌面选择新建文件夹添加,右边默认的读权限,如果要写的,那么该为读写。

MacBook Pro 怎么设置smb共享文件夹

MacBook Pro 怎么设置smb共享文件夹

6.

之后点击用户,点击锁。

MacBook Pro 怎么设置smb共享文件夹

MacBook Pro 怎么设置smb共享文件夹

7.

输入密码解锁,然后把两个允许勾上。

MacBook Pro 怎么设置smb共享文件夹

MacBook Pro 怎么设置smb共享文件夹

远程桌面连接

一、mac被远程连接

1、先请在苹果 Mac 电脑上的“系统偏好设置”窗口中打开“共享”功能

2、接着在共享窗口中的左侧点击启用“屏幕共享”选项

img

3、当屏幕共享功能打开以后,请点击“电脑设置”按钮

img

4、随后请勾选二个选项: VNC 显示程序可以使用密码控制屏幕,并且设置一个连接密码

img

5、随后会提示我们是否真的要连接此电脑,点击 Continue 按钮继续。

img

二、mac连接已设置允许远程连接的mac