iOS面試題整理筆記

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)控卡頓的目的

暫時先整理這些,如果有錯誤,感謝各位大佬指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容