前言
我們借助幾道面試題,來探究一下iOS的內(nèi)存管理
一、使用CADisplayLink、NSTimer有什么注意點?
需要注意兩個地方:小心循環(huán)引用、不準時
1. 小心循環(huán)引用
- (1).
CADisplayLink、NSTimer會對target產(chǎn)生強引用,如果target又對它們產(chǎn)生強引用,那么就會引用循環(huán)引用,例如下面的代碼,就會產(chǎn)生循環(huán)引用
class ViewController: UIViewController {
var displayLink: CADisplayLink!
var timer: Timer!
override func viewDidLoad() {
super.viewDidLoad()
displayLink = CADisplayLink(target: self, selector: #selector(testDisplayLink))
displayLink.add(to: RunLoop.current, forMode: RunLoop.Mode.default)
timer = Timer.init(timeInterval: 1, target: self, selector: #selector(testTimer), userInfo: nil, repeats: true)
RunLoop.current.add(timer, forMode: RunLoop.Mode.default)
}
}
-
(2).
CADisplayLink調(diào)用次數(shù)跟屏幕刷新頻率一致,當?shù)魩瑫r,會響應(yīng)減少;CADisplayLink需要加到RunLoop中才能生效;CADisplayLink會對target產(chǎn)生強引用,所以需要通過代理類規(guī)避循環(huán)引用,代理類的方法有兩種,如下所示:- 第一種,通過
forwardingTarget將消息轉(zhuǎn)發(fā)給target,VC控制器對CADisplay對象產(chǎn)生了強引用,CADisplay對像對proxy代理產(chǎn)生了強引用,proxy代理對VC控制器產(chǎn)生了弱引用,無法形成閉環(huán),也就沒有循環(huán)引用的問題了
- 第一種,通過
class TFTProxy1: NSObject{
weak var target: AnyObject?
override init() {
super.init()
}
convenience init(target: AnyObject?) {
self.init()
self.target = target
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
return target
}
}
使用的時候,這樣寫:
let proxy = TFTProxy1(target: self)
displayLink = CADisplayLink(target: proxy, selector: #selector(testDisplayLink))
displayLink.add(to: RunLoop.current, forMode: RunLoop.Mode.default)
- 第二種,通過
methodSignatureForSelector方法簽名,將消息轉(zhuǎn)發(fā)給proxy,同樣也是proxy代理對VC控制器產(chǎn)生了弱引用,以避免循環(huán)引用
@interface TFTProxy2 ()
@property (nonatomic,weak) id target;
@end
@implementation TFTProxy2
+ (instancetype)proxyWithTarget:(id)target{
TFTProxy2 * proxy = [TFTProxy2 alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
[invocation invokeWithTarget:self.target];
}
@end
- (3).
Timer需要添加到RunLoop中才能執(zhí)行,Timer.scheduledTimer()會自動加入到Runloop中,不需要我們手動加了,而Timer.init()就需要我們手動加到Runloop了;Timer也會對target產(chǎn)生強引用,所以需要通過代理類或者Block來規(guī)避,如下所示:
let proxy = TFTProxy(target: self)
//let proxy = TFTProxy2(target: self)
timer1 = Timer.init(timeInterval: 1, target: proxy, selector: #selector(testTimer), userInfo: nil, repeats: true)
RunLoop.current.add(timer1, forMode: RunLoop.Mode.default)
timer2 = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { [weak self] (timer) in
self?.testTimer()
})
2. 小心不準時
CADisplayLink和Timer都需要依賴RunLoop,如果RunLoop的任務(wù)過于繁重,那么就會造成不準時,所以想準時的話,我們應(yīng)該使用GCD的定時器,如下所示,我們對GCD的定時器做了封裝:
class TFTimer: NSObject{
static var timers_ = Dictionary<String,DispatchSourceTimer>()
static let semaphore_ = DispatchSemaphore(value: 1)
/*
描述:執(zhí)行某個定時任務(wù)
參數(shù):task-任務(wù)閉包
start-任務(wù)開始時間,單位是秒
intercval-定時器間隔,單位是秒
isReapeat-是否重復(fù)執(zhí)行任務(wù)
isAsync-任務(wù)是否異步執(zhí)行,true在全局隊列中,false在主隊列中
*/
class func executeTask(task: @escaping () -> Void, start: DispatchTimeInterval, interval: DispatchTimeInterval, isRepeat: Bool, isAsync: Bool) -> String?{
let queue = isAsync ? DispatchQueue.global() : DispatchQueue.main
let timer = DispatchSource.makeTimerSource(queue: queue)
timer.setEventHandler {
task()
}
let deadline = DispatchTime.now() + start
if isRepeat {
timer.schedule(deadline: deadline, repeating: interval)
}else{
timer.schedule(deadline: deadline, repeating: .never)
}
timer.resume()
semaphore_.wait()
let taskID = "\(timers_.count)"
timers_[taskID] = timer
semaphore_.signal()
return taskID
}
class func executeTask(target: NSObject?, selector: Selector?, start: DispatchTimeInterval, interval: DispatchTimeInterval, isRepeat: Bool, isAsync: Bool) -> String?{
guard let target = target, let selector = selector else { return nil}
let taskID = self.executeTask(task: {
if target.responds(to: selector){
target.perform(selector)
}
}, start: start, interval: interval, isRepeat: isRepeat, isAsync: isAsync)
return taskID
}
/*
取消某個定時任務(wù)
*/
class func cancelTask(taskID: String?){
guard let taskID = taskID , taskID.count > 0 else {
return
}
semaphore_.wait()
let timer = timers_[taskID]
if let timer = timer{
timer.cancel()
timers_.removeValue(forKey: taskID)
}
semaphore_.signal()
}
}
二、介紹下內(nèi)存的幾大區(qū)域?
iOS的虛擬內(nèi)存地址由低至高,可分為:代碼段、數(shù)據(jù)段、堆、棧、內(nèi)核區(qū),如下所示:

代碼段存放:編譯之后的代碼
-
數(shù)據(jù)段存放:
字符串常量,例如:NSString * str = @"123"
已初始化的全局變量、已初始化的靜態(tài)變量等
未初始化的全部變量、未初始化的靜態(tài)變量等
棧:函數(shù)調(diào)用開銷,比如局部變量,分配的內(nèi)存空間地址越來越小
堆:通過alloc、malloc、calloc等動態(tài)分配的空間,分配的內(nèi)存空間地址越來越大
三、講一下你對iOS內(nèi)存管理的理解
- iOS中使用引用計數(shù)來管理OC對象的內(nèi)存,新建的
OC對象引用計數(shù)默認是1,當引用計數(shù)減為0時,OC對象就會被銷毀,其內(nèi)存就會被釋放掉
- iOS中使用引用計數(shù)來管理OC對象的內(nèi)存,新建的
- 調(diào)用
retain會讓OC對象的引用計數(shù)+1,調(diào)用release會讓OC對象的引用計數(shù)-1
- 調(diào)用
-
MRC時,當調(diào)用alloc、new、copy、mutableCopy方法返回了一個對象,在不需要這個對象時,要調(diào)用release或者autorelease來釋放它
-
- 可以通過一下私有函數(shù)來查看自動釋放池的情況
先聲明此私有函數(shù)
extern void _objc_autoreleasePoolPrint(void);
然后就可以調(diào)用了,系統(tǒng)會自動幫我們找到此函數(shù)的實現(xiàn)
_objc_autoreleasePoolPrint();
四、ARC都幫我們做了什么?
ARC是LLVM編譯器和RunLoop相互協(xié)作的一個結(jié)果,幫我們自動進行內(nèi)存管理(如下所示),并且處理了弱引用(對象銷毀時,指向這個對象的弱引用,都會被置為nil)
- 使用
assign修飾普通數(shù)據(jù)類型時,ARC會幫我們自動生成get、set方法,如下所示
@property (nonatomic,assign) NSInteger age;
- (void)setAge:(NSInteger)age{
_age = age;
}
- (NSInteger)age{
return _age;
}
- 使用
retain、strong修飾對象類型,ARC會幫我們自動生成get、set方法,并且在set方法里幫我們retain、release對象,如下所示:
@property (nonatomic,retain) NSObject *status;
- (void)setStatus:(NSObject *)status{
if (_status != status){
//如果新對象和舊對象不相同,就先讓舊對象的引用計數(shù)-1
[_status release];
//然后把新對象的引用計數(shù)+1,在賦值給_status
_status = [status retain];
}
}
- (NSObject *)status{
return _status;
}
- 使用
copy修飾對象類型,ARC會幫我們自動生成get、set方法,并且在set方法里幫我們copy、release對象,如下所示:
@property (nonatomic,copy) NSString *name;
- (void)setName:(NSString *)name{
if (_name != name){
//如果新對象和舊對象不相同,就先讓舊對象的引用計數(shù)-1
[_name release];
//然后把新對象的引用計數(shù)+1,在賦值給_status
此處會使用copy產(chǎn)生一個不可變的對象,這就是為什么NSMutableArray等可變類型不能使用copy的根本原因?。。? _name = [name copy];
}
}
- (NSString *)name{
return _name;
}
五、思考下面兩段代碼能發(fā)生什么事情?有什么區(qū)別

- 第一段代碼會崩潰報,并且報下面這個錯誤;第二段代碼不會崩潰
Thread 3: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
- 因為第一段代碼字符串過長,
name存儲的是真正的指針,開啟多個線程對self.name進行賦值時,本質(zhì)上就是多個線程同時調(diào)用下面的代碼,也就是說同一個對象可能會被release多次,從而導(dǎo)致EXC_BAD_INSTRUCTION;
- 因為第一段代碼字符串過長,
- (void)setName:(NSString *)name{
if (_name != name){
[_name release];
_name = [name copy];
}
}
- 第二段代碼不會崩潰,是因為第二段字符串比較短,蘋果對這種小對象的存儲,專門采用了
TaggedPoint技術(shù)做了優(yōu)化,name的指針中存儲的是具體的數(shù)據(jù),也就是字符串a(chǎn)bc的ASCII碼值,只有當指針不夠存儲數(shù)據(jù)的時候,才會使用動態(tài)分配內(nèi)存的方式存儲數(shù)據(jù)。
- 第二段代碼不會崩潰,是因為第二段字符串比較短,蘋果對這種小對象的存儲,專門采用了
-
- 優(yōu)化方法:改成串行隊列、加鎖
- 改成串行隊列
let queue = DispatchQueue(label: "串行隊列")
for i in 0..<10000{
queue.async {
self.name = "ajselfjalskfja;sfja;skjf;lasjfl;a\(i)"
}
}
- 加鎖,例如加上信號量,如下所示
semaphore = DispatchSemaphore(value: 1)
for i in 0..<10000{
DispatchQueue.global().async {
self.semaphore.wait()
self.name = "ajselfjalskfja;sfja;skjf;lasjfl;a\(i)"
self.semaphore.signal()
}
}
-
objc_msgSend能識別Tagged Pointer,如果發(fā)現(xiàn)消息接受者是Tagged Pointer類型,就會從指針中提取數(shù)據(jù),節(jié)省了以前調(diào)用開銷,例如:NSNumber的intValue方法
-
- 如何判斷一個指針是否是
Tagged Pointer呢?在iOS平臺,最高有效位是1(也就是第64bit)就是Tagged Pointer;在Mac平臺,最低有效位是1,就是Tagged Pointer,如下所示:
- 如何判斷一個指針是否是
判斷是否為Tagged Pointer
#if TARGET_OS_OSX &&__x86_64__
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
#endif
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
六、weak指針的實現(xiàn)原理
為了處理weak指針,Runtime專門維護了一個hash表,用于存儲指向某個對象的所有weak指針,這個hash表的Key是所指對象的地址,Value是指向這個對象的weak指針組成的數(shù)組;
當這個對象銷毀時,就會從hash表中取出指向這個對象的弱引用置為nil,并且從hash表中刪除
七、autorelease對象在什么時機會被調(diào)用release?
在RunLoop休眠之前或者離開的時候調(diào)用release
iOS在RunLoop注冊了兩個Observer觀察者:
第1個Observer監(jiān)聽了
kCFRunLoopEntry事件,會調(diào)用objc_autoreleasePoolPush()-
第2個Observer
監(jiān)聽了
kCFRunLoopBeforeWaiting事件,會調(diào)用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()監(jiān)聽了
kCFRunLoopBeforeExit事件,會調(diào)用objc_autoreleasePoolPop()
八、方法里有局部對象,出了方法以后會立即釋放嗎?
一般情況下ARC是通過release管理內(nèi)存的,所以出了作用域會立即釋放;
但是,如果ARC是通過autorelease管理內(nèi)存的,就是在RunLoop休眠之前或者RunLoop退出的時候進行的釋放