天天看點

iOS面試

1、weak屬性如何自動置nil的?

runtime會對weak屬性進行記憶體布局,建構hash表。以weak屬性對象記憶體位址為key,weak屬性值為value,當對象引用計數為0時,dealloc方法調用時,會将weak屬性值自動置nil。

2、hash表?

哈希表是一種根據關鍵碼去尋找值的資料映射結構。f(key)找到要找的值,key就是關鍵碼,f()是哈希函數。

3、遇到的問題1

畫面上有UIwebview,他的delegate是控制器,在webview加載完成後需要做某些事情,比如加載某些js方法。

如果在webview載入完成之前,将控制器關閉,控制器就會被釋放掉。但是由于webview正在載入頁面而不會被立馬釋放掉,等到頁面加載完畢,回調delegate裡面的方法,由于此時控制器已經釋放掉了,是以會崩潰。

解決辦法是在dealloc中把webview的delegate釋放掉。

-(void)dealloc
{
    self.webview.delegate = nil;
}
           

4、Block的循環應用、内部修改外部變量、三種block

block強引用self,self強引用block。

block不允許修改外部變量的值,這裡的外部變量指的是棧中指針的記憶體位址。__block的作用是隻要觀察到變量被block使用,就将外部變量在棧中的記憶體位址存放到堆中。block捕獲的外部變量可以改變值的是靜态變量、靜态全局變量、全局變量。

棧block,隻用到外部局部變量、成員屬性變量,沒有強指針引用,由系統控制生命周期,一旦傳回之後,就會被系統銷毀,是不持有對象的;

堆block,有強指針引用或copy修飾的成員屬性引用的block會被複制到堆中成為堆block;

全局block,隻用到全局變量、靜态變量,生命周期從建立到應用程式結束,也不持有對象。

5、全局變量和局部變量在記憶體中是否有差別?如果有,是什麼差別?

有差別,全局變量(和靜态變量)儲存在記憶體的全局存儲區中,占用靜态的存儲單元;局部變量儲存在棧中,隻有在所在函數被調用時才動态的為變量配置設定存儲單元。

6、KVO底層實作原理?手動觸發KVO?

當觀察一個對象時,runtime會動态建立繼承自該對象的類,并重寫被觀察對象的setter方法,重寫的setter方法會負責在調用原setter方法前後通知所有觀察對象值的更改,最後會把該對象的isa指針指向這個建立的子類,對象就變成子類的執行個體;

在setter方法中,手動實作NSObject兩個方法:willChangeValueForKey、didChangeValueForKey;

7、category為什麼不能添加屬性?怎麼實作添加?與Extension的差別?category覆寫原類方法?多個category調用順序?

runtime初始化時category的記憶體布局已經确定,沒有ivar,是以預設不能添加屬性。

使用runtime的關聯對象(objc_getAssociatedObject),并重寫setter和getter方法。

#import "UIButton+ClickBlock.h"
#import <objc/runtime.h>

static const void *associatedKey = "associatedKey";

@implementation UIButton (ClickBlock)

-(void)setClick:(ClickBlock)click
{
    objc_setAssociatedObject(self, associatedKey, click, OBJC_ASSOCIATION_COPY_NONATOMIC);
    [self removeTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
    if (click)
    {
        [self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
    }
}

-(ClickBlock)click
{
    return objc_getAssociatedObject(self, associatedKey);
}

-(void)buttonClick
{
    if (self.click)
    {
        self.click();
    }
}

@end
           

Extension編譯期建立,可以添加成員變量。必須要有類的源碼才可以添加。

category方法會在runtime初始化的時候copy到原來方法的前面,調用分類的方法的時候直接傳回,不再調用原類的方法。

多個category的調用順序将按照編譯的順序來。

8、load方法和initialize方法的異同。主要說一下執行時間,各自用途,沒實作子類的方法會不會調用父類的?

load方法,如果類自身沒有定義,并不會調用其父類的load方法。initialize方法,如果類自身沒有定義,就會調用父類的initialize方法。

load隻要類所在的檔案被引用,就會執行。initialize方法在類或者其子類的第一個方法被調用之前才會執行,load方法不視為類的第一個方法。

方法都隻會執行一次。load方法執行順序:父類>子類>,類方法>類别中方法。initialize隻會執行最近的一次調用。

load的一般應用場景:1、hook方法的時候;2、涉及到元件化開發中不同元件間通信,在load中注冊相關協定等。

initialize方法主要用來對一些不友善在編譯期初始化的對象進行指派。比如NSMutableArray這種類型的執行個體化依賴于runtime的消息發送,是以顯然無法在編譯期初始化。

9、iOS中的hook方法

改變程式運作流程的一種技術。

1、Method Swizzle,利用OC的Runtime特性,動态改變SEL(方法編号)和IMP(方法實作)的對應關系,達到OC方法調用流程改變的目的。

2、fishhook。

3、Cydia Substrate。

10、對runtime的了解。

每個執行個體對象都有一個isa指針,當調用方法的時候,runtime庫會在類的方法清單及父類的方法清單中去尋找與消息對應的selector指向的方法,找到後運作這個方法。

調用類方法時,會在這個類的meta-class的方法清單中查找。

消息發送步驟:

1、檢測這個selector是不是要忽略的;

2、檢測target是不是nil,是nil會被忽略掉;

3、根據SEL去cache裡面去找IMP,找到就執行,找不到去類的方法清單中去找,還找不到就去父類中找,直到NSObject類為止;

4、如果還找不到就要開始進入動态方法解析。

@dynamic表示我們會為屬性動态提供存取方法。動态方法解析就是,runtime會調用resolveInstanceMethod或resolveClassMethod來給程式員一次動态添加方法實作的機會。

- (void)myInstanceMethod:(NSString *)string 
{
    NSLog(@"myInstanceMethod = %@", string);
}

+(BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(resolveThisMethodDynamically))
    {
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
           

resolveInstanceMethod(YES完成NO繼續)、forwardingTargetForSelector(備用的接收者)、forwardInvocation(消息轉發)。

11、autoreleasepool的原理和使用場景?

AutoreleasePoolPage以雙向連結清單組合構成棧結構,實作autoreleasepool。

runloop和線程是一一對應的,對應的方式是以key-value的方式儲存在一個全局字典中。子線程的runloop會在第一次擷取的時候建立,如果不擷取的話就一直不會建立。runloop會線上程銷毀時銷毀。

iOS應用啟動後會注冊兩個Observer管理和維護autoreleasepool。(_wrapRunLoopWithAutoreleasePoolHandler)。第一個監聽runloop進入,調用objc_autoreleasePoolPush()向目前的AutoreleasePoolPage增加一個哨兵對象标志建立自動釋放池。這個observer是優先級最高的。

第二個Observer會監聽RunLoop的進入休眠和即将退出RunLoop。在即将休眠時調用objc_autoreleasePoolPop()和objc_autoreleasePoolPush()根據情況從最新加入的對象一直往前清理直到遇到哨兵對象并建立新的池子。在即将退出RunLoop時會調用objc_autoreleasePoolPop()釋放自動釋放池内對象。優先級最低,確定發生在所有回調操作之後。

在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調内的。這些都是在主線程的autoreleasepool包裹下的。

有兩種情況需要手動建立自動釋放池:

1、在子線程中,使用便利構造器建立的對象,這些對象是autorelease的,需要自動釋放池才能釋放。

2、一段代碼裡面,大量使用便利構造器建立的對象,需要手動添加自動釋放池。

12、iOS中使用的鎖、死鎖的發生與避免

@synchronized、信号量、NSLock、NSRecursiveLock是遞歸鎖、NSConditionLock條件鎖、NSCondition可以對線程進行挂起和喚醒、dispatch_semaphore、pthread_mutex和pthread_mutext(recursive)、OSSpinLock、os_unfair_lock。

死鎖:就是有多個線程都卡住了,多個線程在等待對方完成後執行,結果都執行不了了。

GCD使用異步線程、并行隊列。

13、NSOperation和GCD的差別

GCD使用C語言編寫高效,NSOperation是對GCD的面向對象的封裝。對于特殊需求,如取消任務、設定任務優先級、任務狀态監聽,NSOperation使用起來更加友善。

可以設定依賴關系,GCD隻能通過dispatch_barrier_async實作。

可以通過KVO觀察目前operation執行狀态(執行或取消)。

NSOperation可以設定自身優先級,GCD隻能設定隊列的優先級,不能對block設定優先級。

NSOperation可以自定義如NSInvationOperation/NSBlockOperation。

GCD高效,NSOperation開銷相對高。

14、OC與JS互動

15、iOS crash防護

1、unrecognized selector,方法在類和父類中都找不到後,會走消息轉發機制。

resolveInstanceMethod、forwardingTargetForSelector、forwardInvocation。

我們就在forwardingTargetForSelector裡面做處理。因為resolveInstanceMethod需要在類本身動态添加不存在的方法。forwardInvocation開銷較大,而且forwardInvocation經常被使用者調用,不适合多次重寫。forwardingTargetForSelector将消息轉發給一個對象,開銷小,被重寫的機率低。

建立一個繼承自NSObject的類,實作resolveInstanceMethod,在裡面動态的添加以轉發消息為SEL的IMP,在裡面列印,并且傳回0。在frowardingTargetForSelector中傳回該類的執行個體。

2、KVO crash,導緻crash的兩種情形:被觀察者dealloc時仍然注冊着KVO;KVO注冊觀察者與移除觀察者不比對。被觀察對象的KVO關系圖混亂導緻。

可以用代理模式來管理被觀察者的KVO關系圖。在dealloc的時候,可以通過方法交換,将其對應的KVODelegate所有和KVO相關的資料清空,然後将KVODelegate置空。

3、NSNotification crash,當一個對象添加了notification之後,如果dealloc的時候,仍然持有notification,就會出現NSNotification類型的crash。

蘋果在iOS9之後專門針對這種情況做了處理,開發者沒有移除也不會産生crash了。針對iOS9之前的使用者,需要做防護。

hook NSNotificationCenter的添加觀察者的方法,在觀察者身上添加标記。交換觀察者原有的dealloc函數,在對象真正dealloc之前判斷是否有标記,如果有的話先移除觀察者再dealloc。

4、NSTimer crash,有兩個問題:一是NSTimer會強引用target執行個體,target強引用timer,造成循環引用。二是NSTimer無限重複執行一個任務,導緻target的selector一直被重複調用且處于無效狀态。

定義一個抽象類(繼承自NSProxy),抽象類中弱引用target。建立NSTimer的category,交換系統方法,實作NSTimer強引用抽象類。

5、container 類型crash,指的是容器類的crash,數組、字典、NSCache。

在一些常用的會導緻崩潰的API中進行方法交換,然後在新的方法中加入一些條件限制和判斷,進而讓API變得安全。

6、NSString類型crash防護,在一些常用的會導緻崩潰的API中進行方法交換,然後在新的方法中加入一些條件限制和判斷,進而讓API變的安全。

7、野指針crash防護,hook NSObject的dealloc方法,運作時動态生成新類,用_zoombie_做字首拼接原始類名,将僵屍對象的isa指針指向_zoombile_新類,給新類添加forwardingTargetForSelector方法,在該方法中去掉類名的字首,列印出來哪個類調用了哪個方法,終止程式。

16、架構與設計模式

1、MVC設計模式介紹

2、MVVM介紹、MVC與MVVM的差別

MVVM是在MVC的基礎上演化而來,MVVM想要解決的問題是盡可能地減少Controller的任務。

View(ViewController)不再是UIView的子類,而變成了UIViewController的子類,不再負責資料的請求以及處理邏輯,是以不再臃腫。

ViewModel代替了MVC中的Controller成為了協調者的角色,ViewModel被View(ViewController)持有,同時持有Model。資料請求以及處理邏輯都放在ViewModel中。

如果Controller的代碼量很多,并且View的計算很複雜,維護成本很高,改動一個小點還可能會導緻蝴蝶效應,測試也要回歸目前頁面所有的用例。

3、ReactiveCocoa的熱信号與冷信号

冷信号:隻有有訂閱者的時候才會發送信号,一對一,如果有其他的訂閱者訂閱會重新完整的發送信号,給訂閱者發送消息則一定會收到。

熱信号:不在乎是否有訂閱者,一對多,如果發送當時有訂閱者,就會同時接收到信号,如果沒有就不會接收到消息,給訂閱者發送消息不一定會收到。

4、緩存架構設計LRU方案(最久最少使用的資料删除)

緩存是本地資料存儲,存儲方式主要包含兩種:記憶體存儲和磁盤存儲。

磁盤存儲方式主要有檔案管理和資料庫,讀取慢、空間大、可持久。

在程式中聲明的容器(數組、字典)都可看作記憶體中的存儲,讀取快、空間小、不可持久。

iOS主要提供四種磁盤存儲方式:

NSKeyedArchiver,歸檔,需要遵守NSCoding協定,并提供encode和initWithCoder方法。隻能一次性歸檔儲存以及一次性解壓,針對小量資料。

NSUserDefaults,用來儲存應用程式設定和屬性、使用者儲存的資料。

Write寫入方式,寫入到檔案中。

SQLite。

磁盤保守測量低于記憶體讀取幾十倍。

先去找記憶體中的資源,如果沒有去找磁盤中的,找到後存儲到記憶體中,再從記憶體中讀取内容。

記憶體優化,提高記憶體命中率:

用雙向連結清單實作LRU,每個資料是這個串上的一個節點,經常通路的資料移動到頭部,等資料超出容量之後從連結清單後面的一些節點銷毀。

磁盤優化,資料分類存儲:

資料比較大的時候使用檔案存儲,資料較小的時候使用sqlite,一般在20KB左右。

5、SDWebImage源碼,如何實作解碼

UIImage有兩種加載圖檔方法,imageNamed:和imageWithContentsOfFile:。

imageNamed方法的特點在于可以緩存已經加載的圖檔,使用時,先根據檔案名在系統緩存中尋找圖檔,如果沒有,從Bundle中找到該檔案,在渲染到螢幕的時候才解碼圖檔,并将解碼結果保留到緩存中,當收到記憶體警告時,緩存會被清空。當頻繁加載同一張圖檔時,使用imageNamed效果比較好,imageWithContentsOfFile僅加載圖檔,不緩存圖像資料。

imageNamed第一次加載圖檔時,隻在渲染的時候才在主線程解碼,性能并不高效。

圖像分為矢量圖和位圖,顯示到螢幕中的圖像是位圖圖像。我們經常使用的圖檔是JPG或PNG格式的圖檔,他們是經過編碼壓縮後的圖檔格式,在顯示到螢幕之前,需要解碼成位圖圖像。解碼比較耗時,不能使用GPU硬解碼,隻能通過CPU軟解碼實作。

//位圖大小的計算公式,其中bytesPerPixel = 48
bitmap_size = imageSize.width * imageSize.height * bytesPerPixel;
           

将耗時的解碼工作放在子線程中實作。

顯示之前還有圖檔重采樣,圖檔放大和縮小都會引

6、AFNetWorking源碼分析

17、iOS事件傳遞機制

UIResponder

事件分發機制:

使用者點選螢幕産生事件,UIApplication事件分發,UIWindow,hitTest:withEvent:,pointInside:withEvent:,subviews(在裡面的UIView上走同樣的point範圍校驗,如果在某一個UIView中傳回了YES,說明觸摸點在這個UIView的範圍内,則周遊它的子視圖,看觸摸點最終落在哪個子視圖上,如果傳回NO,說明觸摸點不在這個UIView範圍内,那麼就周遊跟他同級的下一個UIView)

找到合适的view後進行處理,如果這個view不進行處理,就按照分發順序回傳。

視圖響應:是和事件傳遞過程相反的。

18、離屏渲染

油畫算法:計算機圖層的疊加繪制大概遵循油畫算法,會按照層級繪制,先繪制距離較遠的場景,再用繪制距離較近的場景覆寫較遠的部分,但是無法修改前面的圖層。

離屏渲染:油畫算法無法滿足我們的需求,我們可以再另開辟一個空間,用于臨時渲染,這個臨時渲染就是離屏渲染。開辟臨時緩存空間,上下文切換,記憶體拷貝,額外的渲染。

離屏渲染是GPU無法按油畫算法一次性渲染完我們的視圖才會觸發。

優化圓角問題:盡量避免使用裁切(masksToBounds),如果要用到可以放到子view中,離屏渲染需要的空間就會變小。提前切好需要的圓角,避免渲染的時候再切。

19、iOS常見三種加密

哈希hash:

1、md5加密,不可逆運算,定長32位字元,相同資料加密結果一緻,不同資料結果不一緻。可以用作一緻性驗證。是不安全的。加鹽,一串比較複雜的字元串。

2、SHA加密,

3、HMAC加密,

對稱加密:

非對稱加密RSA:公鑰和私鑰,HTTPS,