iOS基础相关
1. 什么是ARC?
引用计数机制
在 Objective-C 中,利用 引用计数器 来进行内存管理:每个对象都有一个对应的引用计数,当这个对象被持有的时,其引用计数就会递增,当这个对象的某个持有被释放时,对象的引用计数就会递减,当这个对象的引用计数为 0 的时候,这个对象就会被释放。
每个对象都对应着一个引用计数,在内存中,通过一个 SideTable RefcountMap 来存储这个对应关系:对象的地址作为 Key,引用计数的值作为 Value。
struct SideTable {
// 保证原子操作的自旋锁
spinlock_t slock;
// 引用计数的 hash 表
RefcountMap refcnts;
// weak 引用全局 hash 表
weak_table_t weak_table;
}
struct weak_table_t {
// 保存了所有指向指定对象的 weak 指针
weak_entry_t *weak_entries;
// 存储空间
size_t num_entries;
// 参与判断引用计数辅助量
uintptr_t mask;
// hash key 最大偏移值
uintptr_t max_hash_displacement;
};
在 iOS 中,Objective-C中提供了 两种机制来管理对象的引用计数器,第一种是MRC
(内存的手动管理),第二种是ARC
(自动管理内存)
- MRC: 需要
手动
添加的用来处理内存管理的引用计数
的代码,与对变量的管理相关的方法有:retain,release和autorelease。retain和release方法操作的是引用计数,当引用计数为零时,便自动释放内存 - 在ARC的内存管理中,都是由
系统去管理
的,编译器自动帮我们添加代码,不需要我们去做任何内存操作。
2. block一般用那个关键字修饰,为什么?
copy
修饰,把MRC
下的栈block拷贝到堆里,防止访问的时候block销毁,造成崩溃
3. 用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?
- 用@property声明 NSString、NSArray、NSDictionary 经常使用copy关键字,是因为他们
有对应的可变类型
:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动
,应该在设置新属性值时拷贝一份。 - 如果我们使用是strong,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。
如果我们使用是strong,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。
5. @property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的。
“属性”(property)有两大概念:ivar(实例变量)、存取方法(access method=getter),即@property = ivar + getter + setter
Xcode5.0 之后编译器默认帮我们实现了这些
6. 分别写一个setter方法用于完成
@property (nonatomic,retain)NSString *name
@property (nonatomic,copy) NSString *name
-(void)setName:(NSString *)name
{
[name retain];
[_name release];
_name = name;
}
-(void)setName:(NSString *)name
{
[_name release];
_name = [name copy];
}
7. 说说assign vs weak,_block vs _weak的区别
8. 请说出下面代码是否有问题,如果有问题请修改?
@autoreleasepool {
for (int i=0; i[largeNumber; i++) { (因识别问题,该行代码中尖括号改为方括号代替)
Person *per = [[Person alloc] init];
[per autorelease];
}
}
autorelease
虽然会使引用计数减一,但是它并不是立即减一,它的本质功能只是把对象放到离他最近的自动释放池里。当自动释放池销毁了,才会向自动释放池中的每一个对象发送release消息。这道题的问题就在autorelease。因为largeNumber是一个很大的数,autorelease又不能使引用计数立即减一,所以在循环结束前会造成大次数循环内存暴涨溢出
。
@autoreleasepool {
for (int i=0; i[100000; i++) { (因识别问题,该行代码中尖括号改为方括号代替)
@autoreleasepool {
Person *per = [[Person alloc] init];
[per autorelease];
}
}
}
9. 请问下面代码是否有问题,如有问题请修改?
@autoreleasepool {
NSString *str = [[NSString alloc] init];
[str retain];
[str retain];
str = @"jxl";
[str release];
[str release];
[str release];
}
这道题跟上题一样存在内存泄露问题
1.内存泄露
2.指向常量区的对象不能release。
指针变量str原本指向一块开辟的堆区空间,但是经过重新给str赋值,str的指向发生了变化,由原来指向堆区空间,到指向常量区。常量区的变量根本不需要释放,这就导致了原来开辟的堆区空间没有释放,照成内存泄露。
10. 什么情况下使用weak关键字,相比assign有什么不同?什么情况使用weak关键字?
什么情况使用 weak 关键字?
- 在 ARC 中,在有可能出现
循环引用
的时候,往往要通过让其中一端使用 weak 来解决,比如: delegate、block。 - 自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义
IBOutlet 控件属性
一般也使用 weak,使用 storyboard(xib 不行)创建的 vc,会有一个叫 _topLevelObjectsToKeepAliveFromStoryboard 的私有数组强引用所有 top level 的对象,所以这时即便 outlet 声明成 weak 也没关系。当然,也可以使用 strong。
weak 和 assign 的不同点:
- weak、assign 修饰的属性指向一个对象时
都不会增加对象的引用计数
。然而在所指的对象被释放时,weak 属性值会被置为 nil
,而assign 属性不会
。 - assign 可以用非 OC 对象以及基本类型,而 weak 必须用于 OC 对象。
11.内存管理语义(assign、strong、weak等的区别)
- assign
主要用于修饰基本数据类型
,例如NSInteger,CGFloat,存储在栈中,内存不用程序员管理。assign是可以修饰对象
的,但是会出现野指针
问题。 - weak
weak 修饰符指向但是并不持有该对象(弱引用
),引用计数也不会加1。在 Runtime 中对该属性进行了相关操作,无需处理,可以自动销毁。weak用来修饰对象,多用于避免循环引用
的地方。weak不可以
修饰基本数据类型 - unsafe_unretained
此特质的语义和assign
相同,但是它适用于“对象类型”
,该特质表达一种“非拥有关系”,当目标对象遭到推毁时,属性值不会自动清空
,这一点与weak有区别。 - copy
copy关键字和 strong类似,copy 多用于修饰有可变类型的不可变对象上 NSString,NSArray,NSDictionary上 - Strong
Strong 修饰符表示指向并持有该对象(强引用
),其修饰对象的引用计数会加1。该对象只要引用计数不为0就不会被销毁。当然可以通过将变量强制赋值nil
来进行销毁。 - retain
retain属性的setter方法是保留新值并释放旧值,然后更新实例变量,令其指向新值。
12. @synthesize和@dynamic分别有什么作用?
- @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 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
13. @property中有哪些属性关键字?
属性可以拥有的特质分为四类:
-
原子性
--- nonatomic 特质
在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备 nonatomic 特质,则不使用自旋锁。请注意,尽管没有名为“atomic”的特质(如果某属性不具备 nonatomic 特质,那它就是“原子的” ( atomic) ),但是仍然可以在属性特质中写明这一点,编译器不会报错。若是自己定义存取方法,那么就应该遵从与属性特质相符的原子性。 -
读/写权限
---readwrite(读写)、readonly (只读) -
内存管理语义
---assign、strong、 weak、unsafe_unretained、copy -
方法名
---getter=<name> 、setter=<name>
14. ARC下,不显式指定任何属性关键字时,默认的关键字都有哪些?
- 对应基本数据类型默认关键字是
atomic, readwrite, assign
- 对于普通的 Objective-C 对象
atomic, readwrite, strong
15. 如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?
若想令自己所写的对象具有
拷贝功
能,则需实现NSCopying
协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying
与NSMutableCopying
协议。
具体步骤:
- 需声明该类遵从 NSCopying 协议
- 实现 NSCopying 协议。该协议只有一个方法:
- (id)copyWithZone:(NSZone *)zone;
对于很多现有类,如NSString,NSDictionary,。。。这个方法已经实现
至于如何重写带 copy 关键字的 setter这个问题,
如果抛开本例来回答的话,如下:
- (void)setName:(NSString *)name {
//[_name release]; MRC
_name = [name copy];
}
15. 如何调试BAD_ACCESS错误
野指针:指针指向的对象已经被回收掉了.这个指针就叫做野指针
僵尸对象 : 一个OC对象引用计数为0被释放后就变成僵尸对象了,僵尸对象的内存已经被系统回收,虽然可能该对象还存在,数据依然在内存中,但僵尸对象已经是不稳定对象了,不可以再访问或者使用,它的内存是随时
可能被别的对象申请而占用
的
BAD_ACCESS
:野指针错误,主要的原因是,当某个对象被完全释放,也就是retainCount引用计数为0后。再去通过该对象去调用release或者访问成员变量就会发生野指针错误
- 重写object的respondsToSelector方法,现实出现EXEC_BAD_ACCESS前访问的最后一个object
- 通过Zombie
- 设置全局断点快速定位问题代码所在行
-
Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer。 用法如下:在配置中勾选✅Enable Address Sanitizer
image.png
使用野指针访问僵尸对象.有的时候会出问题报错(EXC_BAD_ACCESS),有的时候不会出问题
- 当野指针指向的僵尸对象所占用的空间
还没有分配
给别人的时候,这个时候其实是可以访问的.因为对象的数据还在. - 当野指针指向的对象所占用的空间
分配给了别人
的时候 这个时候访问就会出问题. 所以,你不要通过1个野指针去访问1个僵尸对象.
16. iOS nil,Nil,NULL,NSNULL的区别
nil (id)0
是OC对象的空指针,可正常调用方法(返回空值,false,零值等)
Nil (Class)0
是OC类的空指针,主要运用于runtime中,Class c = Nil; 其他特性与nil一致
NULL (void *)0
是C指针的空值,在OC中对非对象指针赋空值,如C指针,int *p = NULL
NSNULL [NSNULL null]
是OC中的空对象,可补足NSArray,NSDictinory中不能存储nil的缺陷,在命令行输出一般为"null"
17. 反射机制
内省(反射
)机制是面向对象语言的一个强大特性 , 检查对象自己在运行时
的信息(在继承树上的位置,是否遵循特定的协议,是否可以响应特定的消息)来避免出现未识别方法等问题。
1、获取Class对象
// 获取Class对象
+ (Class)class;
Class对象其实本质上就是一个结构体,这个结构体中的成员变量还是自己,这种设计方式非常像链表的数据结构。
typedef struct objc_class *Class;
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
}
//实例对象获取Class对象
[self class]
//类对象获取Class对象
[Person class]
2、动态的调用方法
// SEL和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
// Class和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
FOUNDATION_EXPORT Class __nullable NSClassFromString(NSString *aClassName);
// Protocol和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto) NS_AVAILABLE(10_5, 2_0);
FOUNDATION_EXPORT Protocol * __nullable NSProtocolFromString(NSString *namestr) NS_AVAILABLE(10_5, 2_0);
通过这些方法,我们可以在运行时选择创建那个实例,并动态选择调用哪个方法。
3、检查继承关系
// 当前对象是否这个类或其子类的实例
- (BOOL)isKindOfClass:(Class)aClass;
// 当前对象是否是这个类的实例
- (BOOL)isMemberOfClass:(Class)aClass;
// 当前对象是否遵守这个协议
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
// 当前对象是否实现这个方法
- (BOOL)respondsToSelector:(SEL)aSelector;
18. 一个NSObject对象占多大的内存?
![](https://img.haomeiwen.com/i1932148/bc00de258f1ec142.png)
![](https://img.haomeiwen.com/i1932148/506406af0e230c28.png)
所以我们可以很好的回答这个问题,系统分配了16个字节
空间给NSObject对象,但是在64位环境下,NSObject只使用了8个字节
19. 你知道有哪些情况会导致app崩溃,分别可以用什么方法拦截并化解?
- unrecognized selector crash
可以利用
Runtime
的消息转发机制,通过重写NSObject的forwardingTargetForSelector
方法,我们就可以将无法识别
的方法进行拦截
并且将消息转发到安全的桩类对象
- KVO crash
参考
KVOViewCOntroller
,创建一个中间代理对象
,被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉
,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash
- NSNotification crash
NSNotification Crash的防护原理很简单, 利用
method swizzling
hook NSObject的dealloc
函数,再对象真正dealloc之前先调用一下removeObserver:
即可。
同时hook
了NSNotificationCenter
的addObserver
函数,在其添加observer的时候,对observer动态添加标记flag。这样在observer dealloc的时候,就可以通过flag标记来判断其是否有必要调用removeObserver函数了。
- NSTimer的crash
参考
NSTimer
防止循环应用
的方法
- Container crash(数组越界NSRangeException,插nil等)
- NSString crash (字符串操作的crash)
NSArray/NSMutableArray/NSDictionary/NSMutableDictionary/NSCache
的一些常用的会导致崩溃的API
进行method swizzling
,然后在swizzle的新方法
中加入一些条件限制和判断
,从而让这些API变的安全,比如AvoidCrash
就是给各个系统类的添加分类
实现method swizzling
- UI not on Main Thread Crash (非主线程刷UI)
- 野指针、僵尸对象
20. [self class] 与 [super class]
下面代码输出什么?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self)
{
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
self和super的区别:
self 是类的一个隐藏参数,每个方法的实现的第一个参数即为self。
super 并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用方法时,去调用父类的方法,而不是本类中的方法。
在调用[super class]的时候,runtime会去调用objc_msgSendSuper方法,而不是objc_msgSend
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
struct objc_super {
id receiver;
Class cls; // the class to search
}
在objc_msgSendSuper方法中,第一个参数是一个 objc_super 的结构体,这个结构体里面有两个变量,一个是接收消息的 receiver,一个是 当前类的父类 super_class。
objc_msgSendSuper 的工作原理:
从objc_super结构体指向的superClass父类的方法列表开始查找selector,找到后以objc->receiver去调用这个selector。注意,最后的调用者是objc->receiver
objc_super->receiver = self
21. isKindOfClass 与 isMemberOfClass
下面代码输出什么?
@interface Sark : NSObject
@end
@implementation Sark
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];
BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];
NSLog(@"%d %d %d %d", res1, res2, res3, res4);
}
return 0;
}
先来分析一下源码
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
inline Class
objc_object::getIsa()
{
if (isTaggedPointer()) {
uintptr_t slot = ((uintptr_t)this >> TAG_SLOT_SHIFT) & TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
return ISA();
}
inline Class
objc_object::ISA()
{
assert(!isTaggedPointer());
return (Class)(isa.bits & ISA_MASK);
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
+ (BOOL)isMemberOfClass:(Class)cls {
return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
![](https://img.haomeiwen.com/i1932148/917814ecc4c1201d.png)
第一行res1输出应该为YES
第二行res2输出NO
第三行的res3输出为NO
第四行res4输出NO
Root class(meta) 的 superclass 就是 Root class(class),也就是NSObject本身。所以第二次循环相等,于是第一行res1输出应该为YES。
22. Objective-C 如何实现多重继承?
Object-c的类没有多继承,只支持单继承,如果要实现多继承的话,可使用如下几种方式间接实现
1. 通过组合实现
A和B组合,作为C类的组件
2. 通过协议实现
C类实现A和B类的协议方法
3. 消息转发实现
forwardInvocation:方法
23. LLDB常用的调试命令有哪些?
po:print object的缩写,表示显示对象的文本描述,如果对象不存在则打印nil。
p:可以用来打印基本数据类型。
call:执行一段代码 如:call NSLog(@"%@", @"yang")
bt:打印当前线程堆栈信息 (bt all打印所有线程堆栈信息)
expr:动态执行指定表达式
image:常用来寻找栈地址对应代码位置 如:image lookup --address 0xxxx
breakpoint:断点操作