一、基础篇
1 . 一个字节多少位?一个汉字多少位?一个字母多少位
一个汉字等于两个英文字母等于两个字节、一个字节是八位
2. 写一个标准的宏MIN,输入两个参数并返回较小的一个
define MIN (A,B) ( (A) <= (B) ? (A) : (B) )
3. define定义和const定义的常量的区别
const定义的常量,是在程序运行时存放在常量表中的,系统会为它自动分配内存,而且在编译时进行类型检查
4. static
- 作用于变量:
用static声明局部变量时,则改变变量的存储方式(生命期),使变量成为静态的局部变量,即编译时就为变量分配内存,直到程序退出才释放存储单元。这样,使得该局部变量有记忆功能,可以记忆上次的数据,不过由于仍是局部变量,因而只能在代码块内部使用(作用域不变)。
用static声明外部变量-------外部变量指在所有代码块{}之外定义的变量,它缺省为静态变量,编译时分配内存,程序结束时释放内存单元。同时 其作用域很广,整个文件都有效甚至别的文件也能引用它。为了限制某些外部变量的作用域,使其只在本文件中有效,而不能被其他文件引用,可以用static 关键字对其作出声明- 作用于函数:
使用static用于函数定义时,对函数的连接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的。这样的函数又叫作静态函数。使用静态函数的好处是,不用担心与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。- 如果想要其他文件可以引用本地函数,则要在函数定义时使用关键字extern,表示该函数是外部函数,可供其他文件调用。另外在要引用别的文件中定义的外部函数的文件中,使用extern声明要用的外部函数即可。
5. 一个函数中有异步的网络请求如何返回一个值
// GCD信号量的方法
+ (NSString *)httpNet {
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
__block NSString *objectID;
// 模拟block异步
[UIView animateWithDuration:3 animations:^{
objectID = @"222";
dispatch_semaphore_signal(signal);
}];
dispatch_semaphore_wait(signal, DISPATCH_TIME_FOREVER);
return objectID;
}
// 直接给函数定义一个block返回
+ (NSString *)httpNetBlock:(void(^)(UIImage *Image))block
6. 将一个函数在主线程执行的4种方法
- GCD方法,通过向主线程队列发送一个block块,使block里的方法可以在主线程中执行
dispatch_async(dispatch_get_main_queue(), ^{
//需要执行的方法
});
- NSOperation 方法
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; //主队列
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
//需要执行的方法
}];
[mainQueue addOperation:operation];
- NSThread 方法
[self performSelector:@selector(method) onThread:[NSThread mainThread] withObject:nil waitUntilDone:YES modes:nil];
[self performSelectorOnMainThread:@selector(method) withObject:nil waitUntilDone:YES];
[[NSThread mainThread] performSelector:@selector(method) withObject:nil];
- RunLoop方法
[[NSRunLoop mainRunLoop] performSelector:@selector(method) withObject:nil];
7.NSTimer创建后,会在哪个线程运行
- 用scheduledTimerWithTimeInterval创建的,在哪个线程创建就会被加入哪个线程的RunLoop中就运行在哪个线程
- 自己创建的Timer,加入到哪个线程的RunLoop中就运行在哪个线程
8. NSNotification接受通知是在那个线程,接收通知是异步的还是同步的
- 接收通知的线程,和发送通知所处的线程是同一个线程。
- NSNotification是同步的 ,因为 NSNotificationCenter 会一直等待所有的 接收者 (observer)都收到并且处理了通知才会返回到poster。
9. id 和 NSObject* 的区别
typedef struct objc_object *id
- id 可以理解为指向对象的指针。所有oc的对象 id 都可以指向,编译器不会做类型检查,id 调用任何存在的方法都不会在编译阶段报错,当然如果这个 id 指向的对象没有这个方法,该崩溃还是会崩溃的。
- NSObject * 指向的必须是NSObject的子类,调用的也只能是NSObjec里面的方法否则就要做强制类型转换。
- 不是所有的OC对象都是NSObject的子类,还有一些继承自NSProxy。NSObject *可指向的类型是id的子集。
10. NSProxy 和 NSObject
11. id和instancetype的区别
- instancetype在类型表示上,跟id一样,可以表示任何对象类型
- instancetype只能用在返回值类型上,不能像id一样用在参数类型上
- instancetype比id多一个好处:编译器会检测instancetype的真实类型
+ (id)person {
return [[self alloc] init];
}
NSString *str = [DZPerson person];
+ (instancetype)person {
return [[self alloc] init];
}
NSString *str = [DZPerson person];
上述例子:
- 当返回的是id类型时可以编译通过,因为任何指针都可以指向id类型。
- 当返回的是instancetype类型时,编译会有如下警告:
Incompatible pointer types initializing 'NSString *' with an expression of type 'DZPerson *'
二、底层篇
1. Runtime是什么?
runtime是由 C、C++、汇编 实现的一套API,为OC语言加入了面向对象及运行时功能。- 运行时(
Runtime)是指将数据类型的确定由编译时推迟到了运行时。- 平时编写的OC代码,在程序运行过程中,其实最终会转换成
Runtime的 C语言 代码,Runtime是Objective-C的幕后工作者。
ro: read-only在编译期就确定
rw: read-write在运行时确定
2. 方法的本质,sel是什么?IMP是什么?两者之间的关系又是什么?
方法的本质是发送消息,消息会有以下几个流程:
- 快速查找:objc_msgSend -> cache_t 缓存消息
- 慢速查找:递归自己和父类 -> lookUpImpOrForward
- 查找不到消息,进行动态方法解析 -> resolveInstanceMethod
- 消息快速转发 -> forwardingTargetForSelector
- 消息慢速转发 -> methodSignatureForSelector & forwardInvocation
- 最终仍未找到消息:程序crash,报经典错误信息 -> unrecognized selector sent to instance xxx
sel是方法编号 -> 在read_images期间就编译进入了内存。
imp就是我们函数实现指针,找imp就是找函数的过程。
两者的关系:
sel相当于书本的目录标题,imp就是书本的页码。查找具体的函数就是想看这本书里面具体篇章的内容:
- 我们首先知道想看什么,也就是title ->
sel- 然后根据目录对应的页码 ->
imp- 打开具体的内容 -> 方法的具体实现
3. 能否向编译后得到的类中增加实例变量?能否像运行时创建的类中添加实例变量?
- 不能像编译后得到的类中增加实例变量,因为我们编译好的实例变量存储在
ro,一旦编译完成,内存结构就完全确定无法修改。- 只要类没有注册到内存就还可以添加,因为在运行时是可以添加属性和方法的。
4. isKindOfClass和isMemberOfClass
详细分析请看 isKindOfClass和isMemberOfClass的区别详细分析,这里只做面试总结。
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; // 1
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; // 0
BOOL re3 = [(id)[DZPerson class] isKindOfClass:[DZPerson class]]; // 0
BOOL re4 = [(id)[DZPerson class] isMemberOfClass:[DZPerson class]]; // 0
NSLog(@"\n re1:%hhd re2:%hhd re3:%hhd re4:%hhd",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]]; // 1
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; // 1
BOOL re7 = [(id)[DZPerson alloc] isKindOfClass:[DZPerson class]]; // 1
BOOL re8 = [(id)[DZPerson alloc] isMemberOfClass:[DZPerson class]]; // 1
NSLog(@"\n re5:%hhd re6:%hhd re7:%hhd re8:%hhd",re5,re6,re7,re8);
// 打印
re1:1 re2:0 re3:0 re4:0
re5:1 re6:1 re7:1 re8:1
isKindOfClass:确定对象是当前类或者是该类继承链上的类的成员。
isMemberOfClass:只能确定对象是否是当前类的成员
5. [self class] 和 [super class]打印结果
@implementation DZPerson
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@",NSStringFromClass([self class]));
NSLog(@"%@",NSStringFromClass([super class]));
}
return self;
}
//打印
2020-06-18 14:13:34.585366+0800 DZTest[64521:6436961] DZPerson
2020-06-18 14:13:34.595804+0800 DZTest[64521:6436961] DZPerson
输出都是当前类DZPerson
原因分析:
使用clang -rewrite-objc DZPerson.m -o DZPerson.cpp生成.cpp文件,得到如下代码:
static instancetype _I_DZPerson_init(DZPerson * self, SEL _cmd) {
self = ((DZPerson *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("DZPerson"))}, sel_registerName("init"));
if (self) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kx_p_4mn02529l53n53r5mggdl00000gn_T_DZPerson_abf3c2_mi_0,NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kx_p_4mn02529l53n53r5mggdl00000gn_T_DZPerson_abf3c2_mi_1,NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("DZPerson"))}, sel_registerName("class"))));
}
return self;
}
// 关键代码
NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")))
NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("DZPerson"))}, sel_registerName("class")))
我们会发现self的底层调用的是objc_msgSend,super在底层调用的是objc_msgSendSuper,源码如下:
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
struct objc_super {
__unsafe_unretained _Nonnull id receiver; // 消息接受者
__unsafe_unretained _Nonnull Class super_class;
};
总结:结合
C++源码,我们会发现消息的接受者都是self,只不过objc_msgSendSuper是从self的父类开始查找方法,但是消息接受者还是self本身,类似于让self去调用父类的方法,所以[self class] 和 [super class]返回的都是DZPerson。
6. 关于对象本质的内存偏移(寻址)面试题
A). 下面程序是否能够执行?为什么?
@interface DZPerson : NSObject
- (void)saySomething;
@end
@implementation DZPerson
- (void)saySomething {
NSLog(@"%s",__func__);
}
@end
// 调用
id pcls = [DZPerson class];
void *pclsPointer= &pcls;
[(__bridge id)pclsPointer saySomething];
DZPerson *p = [DZPerson alloc];
[p saySomething];
运行成功,打印结果:
2020-06-22 11:20:05.471189+0800 DZTest[27568:3295098] -[DZPerson saySomething]
2020-06-22 11:20:05.471377+0800 DZTest[27568:3295098] -[DZPerson saySomething]
疑问:
saySomething是实例方法,但是我们只通过一个类指针就可以调用该方法,这是为什么呢?
原因分析:
对象的本质是一个结构体,元素组成为:isa、superClass、cache_t、bits,第一个元素是isa,isa其实就是一个类的指针,从 内存偏移 分析,isa指针和&class是相等的,都指向类(元类的实例对象)。题目中,
pcls指向的是DZPerson的类对象,而*pclsPointer指针指向&pcls地址,这样相当于pcls指向DZPerson的类对象,也就可以正常调用方法了。
B). 上面程序增加属性调用后运行分析
@interface DZPerson : NSObject
@property (nonatomic, copy)NSString *name;
- (void)saySomething;
@end
@implementation DZPerson
- (void)saySomething {
NSLog(@"%s ———— name:%@",__func__,self.name);
}
@end
// 调用
id pcls = [DZPerson class];
void *pclsPointer= &pcls;
[(__bridge id)pclsPointer saySomething];
//DZPerson *p = [DZPerson alloc];
//[p saySomething];
打印如下:
2020-06-22 14:07:29.840933+0800 DZTest[43305:3637724] -[DZPerson saySomething] ———— name:ViewController
我们把调用注释放开,打印如下:
2020-06-22 14:20:20.768535+0800 DZTest[44091:3666750] -[DZPerson saySomething] ———— name:ViewController
2020-06-22 14:20:20.768928+0800 DZTest[44091:3666750] -[DZPerson saySomething] ———— name:(null)
疑问:
我们会发现内存偏移方式获取的name是ViewController,对象获取的name是(null),这又是为什么呢?
原因分析:
- 在程序运行时,执行的不同函数,其实就是在不停的压栈-出栈。
- 当我们并没有给属性赋值的时候,栈里其实并没有
name的值,所以对象点语法获取name时,值为(null)。- 而当我们用内存偏移获取元素的时候,会指向上一个入栈的对象地址,也就是
- viewDidLoad,所以输出的是ViewController。严格来说,此时的.name是野指针!!!
我们可以再次验证上述说法,我们只添加一行字符串赋值代码,修改如下:
NSString *nickname = @"Dezi";
DZPerson *p = [DZPerson alloc];
[p saySomething];
id pcls = [LGPerson class];
void *pp= &pcls;
[(__bridge id)pp saySomething];
打印如下:
2020-06-22 14:36:19.810450+0800 DZTest[45199:3695110] -[DZPerson saySomething] ———— name:(null)
2020-06-22 14:36:19.810676+0800 DZTest[45199:3695110] -[DZPerson saySomething] ———— name:Dezi
我们会发现,它获取的是上一次入栈的地址的内容。
重点:
当我们上一步入栈的是int型*(4字节),此时代码就会出现典型的访问野指针崩溃问题。这是由于对象访问第二个成员的时候,会根据第一个成员isa的大小8字节进行内存偏移。
所以直接操作内存是危险的行为,我们还是要保持良好的编程习惯。












网友评论