iOS-Interview

11 Nov 2019

面向对象

KVO

KVC

Category

Block

runtime

@interface AYPerson : NSObject
@end
@implementation AYPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        BOOL res1 = [NSObject isKindOfClass:[NSObject class]];
        BOOL res3 = [NSObject isMemberOfClass:[NSObject class]];
        BOOL res2 = [AYPerson isKindOfClass:[AYPerson class]];
        BOOL res4 = [AYPerson isMemberOfClass:[AYPerson class]];
        
        NSLog(@"%d %d %d %d", res1, res2, res3, res4);
        
        NSObject *obj = [[NSObject alloc] init];
        AYPerson *per = [[AYPerson alloc] init];
        BOOL res5 = [obj isKindOfClass:[NSObject class]];
        BOOL res6 = [obj isMemberOfClass:[NSObject class]];
        BOOL res7 = [per isKindOfClass:[AYPerson class]];
        BOOL res8 = [per isMemberOfClass:[AYPerson class]];
        
        NSLog(@"%d %d %d %d", res5, res6, res7, res8);
    }
    return 0;
}

1 0 0 0
1 1 1 1
上面这道题考察的是对 isMemberOfClass,isKindOfClass 运行机制的理解, 前者判断是否是对应类,后者判断是否是对应类的子类,这里需要注意的是使用实例对象调用的结果和使用类对象调用的结果有不同, 使用类对象调用的结果是判断meta-class,而实例对象判断的是class.

查看apple objc4可以找到对应的方法实现

+ (BOOL)isMemberOfClass:(Class)cls {
    // 获取meta-class
    return object_getClass((id)self) == cls;
}

- (BOOL)isMemberOfClass:(Class)cls {
    // 获取class
    return [self class] == cls;
}

+ (BOOL)isKindOfClass:(Class)cls {
    // 获取meta-class,并依次判断是否有一个superclass是相同的
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
    // 获取class,并依次判断是否有一个superclass是相同的
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

上面的题有一点需要注意 BOOL res1 = [NSObject isKindOfClass:[NSObject class]]; res1 == true
因为NSObject比较特殊,NSOjectmetaclasssuperClass指向NSOject, 因为这个特殊性,所以NSObject的类方法找不到时会去调用NSObject同名的实例方法

@interface AYPerson : NSObject
@property(nonatomic, strong) NSString *name;
- (void)print;
@end
@implementation AYPerson
- (void)print
{
    NSLog(@"my name is %@", self.name);
}
@end

@interface ViewController ()
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];    
    id cls = [AYPerson class];
    void *obj = &cls;
    
    [(__bridge id)obj print];
}
@end

my name is <ViewController: 0x7fbcc47036c0>

这段代码可以执行成功,分两个点来解释,1. 为什么可以正常调用实例方法?
2. 为什么打印出来是 <ViewController: 0x7fbcc47036c0>?

  1. 为什么可以正常调用实例方法?

正常的创建一个 AYPerson *person = [[AYPerson alloc] init]; 这个person是一个由isa指针和_name组成的结构体,然后person指针指向isaisa指向[AYPerson class]. 而上面的obj指向cls, cls指向[AYPerson class], 所以objperson都存有指向[AYPerson class]的指针, 因此obj可以正常调用print方法。

  1. 为什么打印出来是 <ViewController>?
    personself.name在运行时会去找内存中跟isa挨着的下一块内存地址上面的值。而跟cls挨着的是前面定义的变量。

这里讲一下大端下端存储的问题,运行如下代码

int a = 2;
int b = 4;
int c = 8;
NSLog(@"\n%p\n%p\n%p", &a, &b, &c);
/*
0x7ffee3d4313c
0x7ffee3d43138
0x7ffee3d43134
*/

struct {
    int a;
    int b;
}test;

test.a = 10;
test.b = 20;
NSLog(@"\nstruct a: %p\nstruct b:
%p", &(test.a), &(test.b));
/*
struct a: 0x7ffee3d43128
struct b: 0x7ffee3d4312c
*/

可以看出前面定义的变量会存在栈中的高位,从大到小,而结构体中的变量在栈中的地址根据定义的顺序升位,从小到大。

下面解释为什么跟obj挨着的下一块内存地址上面的值是<ViewController: 0x7fbcc47036c0>

在创建cls的代码出打一个断点,查看汇编代码

可以看到在创建cls之前调用了 objc_msgSendSuper2

查看apple objc4源码

从汇编的实现的注释中可以看出,传进去了两个参数,real receiver, class, 然后会再通过class获取superclass。 方法的声明如下, 需要传入一个objc_super的结构体。

#if __OBJC2__
// objc_msgSendSuper2() takes the current search class, not its superclass.
OBJC_EXPORT id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)

所以可以推测调用[super viewDidLoad]; 会生成一个这样的结构体

struct objc_super super = {self, [self class]};

因此obj挨这的下一块内存地址上面的值是<ViewController: 0x7fbcc47036c0>

可以使用lldb调试验证上面从源码理解是否准确

和前面分析的结论一致

Runloop

runloop 是怎么响应用户操作的, 具体流程是什么样的?

iOS程序启动时会在主线程启动一个runloop,让程序保持运行状态,当有交互事件发生时,会触发runloopsource0事件,source0再调用Application的响应方法,applicaiton根据响应链的流程把事件传递到对应方法中,如果没有实现响应方法,程序就什么都不处理。

多线程

一般使用GCD

GCD的队列分串行队列,并发队列,还有主队列,主队列是一个特殊的串行队列。
放到串行队列的任务会按顺序执行,只有执行完上一个任务才会执行下一个任务;如果是async异步执行,并发队列可以同时执行多个任务,如果使用同步sync执行,即使并发队列还是不会并发执行任务

追问二:使用以上锁需要注意哪些?

自旋锁等待中会一直占用CPU资源,可能会出现优先级反转的问题,如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。 CPU比较紧张的时候不建议用自旋锁。避免使用进入死锁状态,当需要重复加锁的时候,使用递归锁。

追问三:用C/OC/C++,任选其一,实现自旋或互斥?口述即可!

自旋锁使用OSSpinLock, 互斥锁使用pthread_mutex. 代码实现自旋锁, 定义一个 Bool 类型的变量false代表未加锁,true代表已加锁,我们把它叫做lock。加锁的时候,如果是true就进入while循环,直到lock变成false,如果是false就直接把lock设置为true。解锁的时候,就把lock设置为false

内存管理

@interface ViewController ()
@property (strong, nonatomic) NSString *name;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

    for (int i = 0; i < 1000; i++) {
        dispatch_async(queue, ^{
            self.name = [NSString stringWithFormat:@"abc"];
        });
    }
}

前者会崩溃,后者可以正常运行
因为[NSString stringWithFormat:@"abcdefghijk"]对象类型的字符串,在setName的方法中,底层调用的是这样的方法

- (void)setName:(NSString *)name
{
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}

上面是并发调用setName方法,有可能在某个时间段release调用的次数比 retain 多,这样会出现崩溃EXC_BAD_ACCESS,因为要release的对象已经被释放了。 ** 后者虽然也是并发调用setName方法, 但是[NSString stringWithFormat:@"abc"]创建的字符串,其实是tagged-pointer类型的字符串,字符串的内容直接存在栈上的指针,在setName 方法中,不需要调用 releaseretain

另外代码一,可以通过把属性设置为atomic类型来保证set方法的线程安全,或者调用set时手动加锁来保证线程安全

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
for (int i = 0; i < 1000; i++)
{
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        self.name = [NSString stringWithFormat:@"asdfghjklzxcvbnm"];
        dispatch_semaphore_signal(semaphore);
    });
}

性能优化