前言
我們借助幾道面試題,來探究一下iOS的內(nèi)存管理
一息楔、使用CADisplayLink杏节、NSTimer有什么注意點?
需要注意兩個地方:小心循環(huán)引用乐导、不準(zhǔ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ù)跟屏幕刷新頻率一致棵磷,當(dāng)?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. 小心不準(zhǔn)時
CADisplayLink
和Timer
都需要依賴RunLoop
,如果RunLoop
的任務(wù)過于繁重续搀,那么就會造成不準(zhǔn)時塞琼,所以想準(zhǔn)時的話,我們應(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ù)默認(rèn)是1司浪,當(dāng)引用計數(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
時吁伺,當(dāng)調(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的根本原因!1薄蒸播!
_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碼值
,只有當(dāng)指針不夠存儲數(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ù)組;
當(dāng)這個對象銷毀時犬绒,就會從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退出的時候進行的釋放