OC語法篇
面向?qū)ο?/h3>
1. 一個NSObject對象占用多少內(nèi)存?
系統(tǒng)分配了16個字節(jié)給NSobject對象(通過malloc_size函數(shù)獲得),但NSobject對象內(nèi)部只是用了8個字節(jié)的空間(64bit環(huán)境下,可以通過class_getInstanceSize函數(shù)獲得)
2. 對象的isa指針指向哪里?
instance對象的isa指針指向class對象;class對象的isa指針指向meta-class對象;meta-class對象的isa指針指向基類的meta-class對象
3. OC的類信息存放在哪里?
對象方法、屬性、成員變量、協(xié)議信息,存放在class對象中;類方法存放在meta-class對象中;成員變量的具體值,存放在instance對象中。
OC對象分為三種對象
instance對象:實例對象(包含isa指針、成員變量)
class對象:類對象(包含isa指針、superclass、屬性、對象方法、協(xié)議、成員變量···)
meta-class對象:元類對象(包含isa指針、superclass、類方法)
4. isa、superclass總結(jié)
instance的isa指向class
class的isa指向meta-class
meta-class的isa指向基類的meta-class
class的superclass指向父類的class
如果沒有父類,superclass指針為nil
meta-class的superclass指向父類的meta-class
基類的meta-class的superclass指向基類的class
instance調(diào)用對象方法的軌跡
isa找到class,方法不存在,就通過superclass找父類
class調(diào)用類方法的軌跡
isa找meta-class,方法不存在,就通過superclass找父類
KVO and KVC
1. iOS用什么方式實現(xiàn)對一個對象的KVO?(KVO的本質(zhì)是什么?)
利用RuntimeAPI動態(tài)生成一個子類,并且讓instance對象的isa指向這個全新的子類
當(dāng)修改instance對象的屬性時,會調(diào)用Foundation的_NSSetXXXValueAndNotify函數(shù)
willChangeValueForKey:
父類原來的setter
didChangeValueForKey:
內(nèi)部會觸發(fā)監(jiān)聽器(Oberser)的監(jiān)聽方法( observeValueForKeyPath:ofObject:change:context:)
2. 如何手動觸發(fā)KVO?
手動調(diào)用willChangeValueForKey:和didChangeValueForKey:
3. 通過KVC修改屬性會觸發(fā)KVO么?
會觸發(fā)KVO
4. KVC的賦值和取值過程是怎樣的?原理是什么?
KVC的全稱是Key-Value Coding,俗稱“鍵值編碼”,可以通過一個key來訪問某個屬性
常見的API有
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
賦值過程:
按照setKey:、_setKey順序查找方法,找到了方法傳遞參數(shù),調(diào)用方法;若沒找到,查看accessInstanceVariablesDirectly方法的返回值,若為NO則調(diào)用setValue:forUndefinedKey:并拋出異常NSUnknownKeyException,若返回YES則按照_key、_isKey、key、isKey順序查找成員變量,找到直接賦值,找不到則調(diào)用setValue:forUndefinedKey:并拋出異常NSUnknownKeyException
取值過程
按照getKey:、key、_isKey、key順序查找方法,找到了方法,調(diào)用方法;若沒找到,查看accessInstanceVariablesDirectly方法的返回值,若為NO則調(diào)用valueForUndefinedKey:并拋出異常NSUnknownKeyException,若返回YES則按照_key、_isKey、key、isKey順序查找成員變量,找到直接取值,找不到則調(diào)用valueForUndefinedKey:并拋出異常NSUnknownKeyException
Category
1. Category的使用場合是什么?
2. Category的實現(xiàn)原理
Category編譯之后的底層結(jié)構(gòu)是struct category_t,里面存儲著分類的對象方法、類方法、屬性、協(xié)議信息
在程序運行的時候,runtime會將Category的數(shù)據(jù),合并到類信息中(類對象、元類對象中)
3. Category和Class Extension的區(qū)別是什么?
Class Extension在編譯的時候,它的數(shù)據(jù)就已經(jīng)包含在類信息中
Category是在運行時,才會將數(shù)據(jù)合并到類信息中
4. Category中有l(wèi)oad方法嗎?load方法是什么時候調(diào)用的?load 方法能繼承嗎?
有l(wèi)oad方法
load方法在runtime加載類、分類的時候調(diào)用
load方法可以繼承,但是一般情況下不會主動去調(diào)用load方法,都是讓系統(tǒng)自動調(diào)用
5. load、initialize方法的區(qū)別什么?它們在category中的調(diào)用的順序?以及出現(xiàn)繼承時他們之間的調(diào)用過程?
+load方法會在runtime加載類、分類時調(diào)用
每個類、分類的+load,在程序運行過程中只調(diào)用一次
調(diào)用順序:先調(diào)用類的+load,按照編譯先后順序調(diào)用(先編譯,先調(diào)用),調(diào)用子類的+load之前會先調(diào)用父類的+load;再調(diào)用分類的+load,按照編譯先后順序調(diào)用(先編譯,先調(diào)用)
+initialize方法會在類第一次接收到消息時調(diào)用,調(diào)用順序:先調(diào)用父類的+initialize,再調(diào)用子類的+initialize,(先初始化父類,再初始化子類,每個類只會初始化1次)
+initialize和+load的很大區(qū)別是,+initialize是通過objc_msgSend進行調(diào)用的,所以有以下特點:如果子類沒有實現(xiàn)+initialize,會調(diào)用父類的+initialize(所以父類的+initialize可能會被調(diào)用多次);如果分類實現(xiàn)了+initialize,就覆蓋類本身的+initialize調(diào)用
6. Category能否添加成員變量?如果可以,如何給Category添加成員變量?
不能直接給Category添加成員變量,但是可以間接實現(xiàn)Category有成員變量的效果
添加關(guān)聯(lián)對象:void objc_setAssociatedObject(id object, const void * key,
id value, objc_AssociationPolicy policy)
獲得關(guān)聯(lián)對象:id objc_getAssociatedObject(id object, const void * key)
移除所有的關(guān)聯(lián)對象:void objc_removeAssociatedObjects(id object)
block
1. block的原理是怎樣的?本質(zhì)是什么?
block本質(zhì)上也是一個OC對象,它內(nèi)部也有個isa指針,封裝了函數(shù)調(diào)用以及調(diào)用環(huán)境的OC對象
block有3種類型,可以通過調(diào)用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型
__NSGlobalBlock__(_NSConcreteGlobalBlock):存儲在數(shù)據(jù)區(qū),沒有訪問auto變量
__NSStackBlock__(_NSConcreteStackBlock):存儲在棧區(qū),訪問auto變量
__NSMallocBlock__(_NSConcreteMallocBlock):存儲在堆區(qū),__NSStackBlock__調(diào)用了copy
2. __block的作用是什么?有什么使用注意點?
__block可以用于解決block內(nèi)部無法修改auto變量值的問題
__block不能修飾全局變量、靜態(tài)變量(static)
編譯器會將__block變量包裝成一個對象
3. block的屬性修飾詞為什么是copy?使用block有哪些使用注意?
block一旦沒有進行copy操作,就不會在堆上
使用注意:循環(huán)引用問題
__weak typeof(self)weakSelf = self;
__unsafe _unretained id weakSelf = self;
__block id weakSelf = self;
Runtime
1. 講一下 OC 的消息機制
OC中的方法調(diào)用其實都是轉(zhuǎn)成了objc_msgSend函數(shù)的調(diào)用,給receiver(方法調(diào)用者)發(fā)送了一條消息(selector方法名)
objc_msgSend底層有3大階段
消息發(fā)送(當(dāng)前類、父類中查找)、動態(tài)方法解析、消息轉(zhuǎn)發(fā)
2. 什么是Runtime?平時項目中有用過么?
OC是一門動態(tài)性比較強的編程語言,允許很多操作推遲到程序運行時再進行
OC的動態(tài)性就是由Runtime來支撐和實現(xiàn)的,Runtime是一套C語言的API,封裝了很多動態(tài)性相關(guān)的函數(shù)
平時編寫的OC代碼,底層都是轉(zhuǎn)換成了Runtime API進行調(diào)用
具體應(yīng)用
利用關(guān)聯(lián)對象(AssociatedObject)給分類添加屬性;遍歷類的所有成員變量(修改textfield的占位文字顏色、字典轉(zhuǎn)模型、自動歸檔解檔); 交換方法實現(xiàn)(交換系統(tǒng)的方法);利用消息轉(zhuǎn)發(fā)機制解決方法找不到的異常問題
RunLoop
1. 講講 RunLoop,項目中有用到嗎?
運行循環(huán),在程序運行過程中循環(huán)做一些事情
應(yīng)用范疇:定時器(Timer)、PerformSelector;GCD Async Main Queue;事件響應(yīng)、手勢識別、界面刷新;網(wǎng)絡(luò)請求;AutoreleasePool
RunLoop的基本作用:保持程序的持續(xù)運行;處理App中的各種事件(比如觸摸事件、定時器事件等);節(jié)省CPU資源,提高程序性能:該做事時做事,該休息時休息
2. runloop內(nèi)部實現(xiàn)邏輯?
3. runloop和線程的關(guān)系?
每條線程都有唯一的一個與之對應(yīng)的RunLoop對象
RunLoop保存在一個全局的Dictionary里,線程作為key,RunLoop作為value
線程剛創(chuàng)建時并沒有RunLoop對象,RunLoop會在第一次獲取它時創(chuàng)建
RunLoop會在線程結(jié)束時銷毀
主線程的RunLoop已經(jīng)自動獲?。▌?chuàng)建),子線程默認沒有開啟RunLoop
4. timer 與 runloop 的關(guān)系?
5. 程序中添加每3秒響應(yīng)一次的NSTimer,當(dāng)拖動tableview時timer可能無法響應(yīng)要怎么解決?
6. runloop 是怎么響應(yīng)用戶操作的, 具體流程是什么樣的?
7. 說說runLoop的幾種狀態(tài)
eg:添加Observer監(jiān)聽RunLoop的所有狀態(tài)
CFRunLoopObserverRef observe = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, yearMask, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");//即將進入Loop
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");//即將處理Timers
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");//即將處理Sources
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");//即將進入休眠
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");//剛從休眠中喚醒
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");//即將退出Loop
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observe, kCFRunLoopCommonModes);
CFRelease(observe);
8. runloop的mode作用是什么?
CFRunLoopModeRef代表RunLoop的運行模式
一個RunLoop包含若干個Mode,每個Mode又包含若干個Source0/Source1/Timer/Observer
RunLoop啟動時只能選擇其中一個Mode,作為currentMode
如果需要切換Mode,只能退出當(dāng)前Loop,再重新選擇一個Mode進入
不同組的Source0/Source1/Timer/Observer能分隔開來,互不影響
如果Mode里沒有任何Source0/Source1/Timer/Observer,RunLoop會立馬退出
常見的2種Mode
kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默認Mode,通常主線程是在這個Mode下運行
UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
多線程
1. iOS中的線程同步方案
性能從高到低排序
OSSpinLock
OSSpinLock叫做”自旋鎖”,等待鎖的線程會處于忙等(busy-wait)狀態(tài),一直占用著CPU資源
目前已經(jīng)不再安全,可能會出現(xiàn)優(yōu)先級反轉(zhuǎn)問題
如果等待鎖的線程優(yōu)先級較高,它會一直占用著CPU資源,優(yōu)先級低的線程就無法釋放鎖
需要導(dǎo)入頭文件#import <libkern/OSAtomic.h>
//初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//嘗試加鎖(如果需要等待就不加鎖,直接返回false;如果不需要等待就加鎖,返回true)
bool result = OSSpinLockTry(&lock);
//加鎖
OSSpinLockLock(&lock);
//解鎖
OSSpinLockUnLock(&lock);
os_unfair_lock
os_unfair_lock
os_unfair_lock用于取代不安全的OSSpinLock ,從iOS10開始才支持
從底層調(diào)用看,等待os_unfair_lock鎖的線程會處于休眠狀態(tài),并非忙等
需要導(dǎo)入頭文件#import <os/lock.h>
//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//嘗試加鎖
os_unfair_lock_trylock(&lock);
//加鎖
os_unfair_lock_lock(&lock);
//解鎖
os_unfair_lock_unlock(&lock);
pthread_mutex
mutex叫做”互斥鎖”,等待鎖的線程會處于休眠狀態(tài)
需要導(dǎo)入頭文件#import <pthread.h>
dispatch_semaphore:
semaphore叫做”信號量”
信號量的初始值,可以用來控制線程并發(fā)訪問的最大數(shù)量
信號量的初始值為1,代表同時只允許1條線程訪問資源,保證線程同步
//信號量的初始值
int value = 1;
//初始化信號量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
//如果信號量的值<=0,當(dāng)前線程就會進入休眠等待(直到信號量的值>0)
//如果信號量的值>0,就減1,然后往下執(zhí)行后面的代碼
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
//讓信號量的值加1
dispatch_semaphore_signal(semaphore);
dispatch_queue(DISPATCH_QUEUE_SERIAL)
直接使用GCD的串行隊列,也是可以實現(xiàn)線程同步的
dispatch_queue_t queue = dispatch_queue_create("lock_queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
//任務(wù)
});
NSLock:對mutex普通鎖的封裝
NSRecursiveLock
NSCondition:對mutex和cond的封裝
NSConditionLock:對NSCondition的進一步封裝,可以設(shè)置具體的條件值
@synchronized:是對mutex遞歸鎖的封裝
什么情況使用自旋鎖比較劃算?
預(yù)計線程等待鎖的時間很短
加鎖的代碼(臨界區(qū))經(jīng)常被調(diào)用,但競爭情況很少發(fā)生
CPU資源不緊張
多核處理器
什么情況使用互斥鎖比較劃算?
預(yù)計線程等待鎖的時間較長
單核處理器
臨界區(qū)有IO操作
臨界區(qū)代碼復(fù)雜或者循環(huán)量大
臨界區(qū)競爭非常激烈
內(nèi)存管理
1. 使用CADisplayLink、NSTimer有什么注意點?
CADisplayLink、NSTimer會對target產(chǎn)生強引用,如果target又對它們產(chǎn)生強引用,那么就會引發(fā)循環(huán)引用
解決方案
使用block 弱引用weakSelf
使用代理對象(NSProxy)
NSTimer依賴于RunLoop,如果RunLoop的任務(wù)過于繁重,可能會導(dǎo)致NSTimer不準時,而GCD的定時器會更加準時
//創(chuàng)建一個定時器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
//設(shè)置時間(start是幾秒后開始執(zhí)行,interval是時間間隔)
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, (int64_t)(start * NSEC_PER_SEC)), (uint64_t)(interval ) * NSEC_PER_SEC);
//設(shè)置回調(diào)
dispatch_source_set_event_handler(timer, ^{
});
//啟動定時器
dispatch_resume(timer);
2. 介紹下內(nèi)存的幾大區(qū)域
代碼段:編譯之后的代碼
數(shù)據(jù)段
字符串常量:比如NSString *str = @"123"
已初始化數(shù)據(jù):已初始化的全局變量、靜態(tài)變量等
未初始化數(shù)據(jù):未初始化的全局變量、靜態(tài)變量等
棧:函數(shù)調(diào)用開銷,比如局部變量。分配的內(nèi)存空間地址越來越小
堆:通過alloc、malloc、calloc等動態(tài)分配的空間,分配的內(nèi)存空間地址越來越大
3. 講一下你對 iOS 內(nèi)存管理的理解
在iOS中,使用引用計數(shù)來管理OC對象的內(nèi)存
一個新創(chuàng)建的OC對象引用計數(shù)默認是1,當(dāng)引用計數(shù)減為0,OC對象就會銷毀,釋放其占用的內(nèi)存空間
調(diào)用retain會讓OC對象的引用計數(shù)+1,調(diào)用release會讓OC對象的引用計數(shù)-1
內(nèi)存管理的經(jīng)驗總結(jié)
當(dāng)調(diào)用alloc、new、copy、mutableCopy方法返回了一個對象,在不需要這個對象時,要調(diào)用release或者autorelease來釋放它
想擁有某個對象,就讓它的引用計數(shù)+1;不想再擁有某個對象,就讓它的引用計數(shù)-1
可以通過以下私有函數(shù)來查看自動釋放池的情況
extern void _objc_autoreleasePoolPrint(void);
性能優(yōu)化
1. 列表卡頓的原因可能有哪些?你平時是怎么優(yōu)化的?
CPU
- 盡量用輕量級的對象,比如用不到事件處理的地方,可以考慮使用CALayer取代UIView
- 不要頻繁地調(diào)用UIView的相關(guān)屬性,比如frame、bounds、transform等屬性,盡量減少不必要的修改
- 盡量提前計算好布局,在有需要時一次性調(diào)整對應(yīng)的屬性,不要多次修改屬性
- Autolayout會比直接設(shè)置frame消耗更多的CPU資源
- 圖片的size最好剛好跟UIImageView的size保持一致
- 控制一下線程的最大并發(fā)數(shù)量
- 盡量把耗時的操作放到子線程: 文本處理(尺寸計算、繪制);圖片處理(解碼、繪制)
GPU
- 盡量避免短時間內(nèi)大量圖片的顯示,盡可能將多張圖片合成一張進行顯示
- GPU能處理的最大紋理尺寸是4096x4096,一旦超過這個尺寸,就會占用CPU資源進行處理,所以紋理盡量不要超過這個尺寸
- 盡量減少視圖數(shù)量和層次
- 減少透明的視圖(alpha<1),不透明的就設(shè)置opaque為YES
- 盡量避免出現(xiàn)離屏渲染
2. 什么是離屏渲染?
在OpenGL中,GPU有2種渲染方式
On-Screen Rendering:當(dāng)前屏幕渲染,在當(dāng)前用于顯示的屏幕緩沖區(qū)進行渲染操作
Off-Screen Rendering:離屏渲染,在當(dāng)前屏幕緩沖區(qū)以外新開辟一個緩沖區(qū)進行渲染操作
3. 離屏渲染消耗性能的原因
需要創(chuàng)建新的緩沖區(qū)
離屏渲染的整個過程,需要多次切換上下文環(huán)境,先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結(jié)束以后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上,又需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕
4. 哪些操作會觸發(fā)離屏渲染?
- 光柵化,layer.shouldRasterize = YES
- 遮罩,layer.mask
- 圓角,同時設(shè)置layer.masksToBounds = YES、layer.cornerRadius大于0(考慮通過CoreGraphics繪制裁剪圓角,或者叫美工提供圓角圖片)
- 陰影,layer.shadowXXX(如果設(shè)置了layer.shadowPath就不會產(chǎn)生離屏渲染)
5. 怎么檢測卡頓?
平時所說的“卡頓”主要是因為在主線程執(zhí)行了比較耗時的操作
可以添加Observer到主線程RunLoop中,通過監(jiān)聽RunLoop狀態(tài)切換的耗時,以達到監(jiān)控卡頓的目的
暫時先整理這些,如果有錯誤,感謝各位大佬指正。