iOS 面試寶典

Inlight發表於2017-12-21

tip1.可變集合類 和 不可變集合類的 copy 和 mutablecopy 有什麼區別?

  1. 對於可變與不可變物件:區別在於是否需要在建立物件的時候確定並固定物件的記憶體地址的大小與位置。
  • 不可變物件在初始化之後不能改變自己所儲存的內容大小,也就是不可修改自己的記憶體地址的大小與位置;
  • 而可變物件則可在初始化之後通過自己的方法修改自己的記憶體地址的大小和位置。
  1. 對於深拷貝淺拷貝:區別在於是否對物件拷貝。
  • 淺拷貝:僅僅是對指向物件的指標的拷貝,先後兩個指標指向同一個物件;
  • 深拷貝:是對物件和指標的雙重拷貝,新指標指向新物件,舊指標指向舊物件。
  1. 對於copy和mutableCopy方法:
  • copy:
    • 對於不可變物件,copy相當於做了一次淺拷貝,僅僅拷貝指標,不拷貝物件;
    • 對於可變物件,copy是深拷貝,但是拷貝出來的物件型別為相應的不可變物件。
  • mutableCopy
    • 不管是可變還是不可變物件,統統都是深拷貝,返回的統統都是可變物件。

tip2.@synthesize和@dynamic分別有什麼作用?

  1. @property有兩個對應的詞,一個是 @synthesize,一個是 @dynamic。如果 @synthesize和 @dynamic都沒寫,那麼預設的就是@syntheszie var = _var;
  2. @synthesize 的語義是如果你沒有手動實現 setter 方法和 getter 方法,那麼編譯器會自動為你加上這兩個方法。
  3. @dynamic 告訴編譯器:屬性的 setter 與 getter 方法由使用者自己實現,不自動生成。(當然對於 readonly 的屬性只需提供 getter 即可)。假如一個屬性被宣告為 @dynamic var,然後你沒有提供 @setter方法和 @getter 方法,編譯的時候沒問題,但是當程式執行到 instance.var = someVar,由於缺 setter 方法會導致程式崩潰;或者當執行到 someVar = var 時,由於缺 getter 方法同樣會導致崩潰。編譯時沒問題,執行時才執行相應的方法,這就是所謂的動態繫結。

tip3.實現description方法能取到什麼效果?

  1. NSLog(@"%@", objectA);這會自動呼叫objectA的description方法來輸出ObjectA的描述資訊

  2. description方法預設返回物件的描述資訊(預設實現是返回類名和物件的記憶體地址)

  3. description方法是基類NSObject 所帶的方法,因為其預設實現是返回類名和物件的記憶體地址, 這樣的話,使用NSLog輸出OC物件,意義就不是很大,因為我們並不關心物件的記憶體地址,比較關心的是物件內部的一些成變數的值。因此,會經常重寫description方法,覆蓋description方法的預設實現

  4. 重寫description方法有一些坑要注意

  • 千萬不要在description方法中用以下方式使用%@和self,這樣造成循壞呼叫,導致系統會發生執行時錯誤。
- (NSString *)description {
    return [NSString stringWithFormat:@"%@", self];
}
複製程式碼
  • 當該方法使用NSLog("%@",self) 時候, 系統做了相關的優化,循壞呼叫幾次後就會自動退出

tip4.為什麼把NSTimer物件以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)新增到主執行迴圈以後,滑動scrollview的時候NSTimer卻不動了?

RunLoop只能執行在一種mode下,如果要換mode,當前的loop也需要停下重啟成新的。利用這個機制,ScrollView滾動過程中NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode會切換到UITrackingRunLoopMode來保證ScrollView的流暢滑動:只能在NSDefaultRunLoopMode模式下處理的事件會影響scrllView的滑動。

如果我們把一個NSTimer物件以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)新增到主執行迴圈中的時候, ScrollView滾動過程中會因為mode的切換,而導致NSTimer將不再被排程。

同時因為mode還是可定製的,所以:Timer計時會被scrollView的滑動影響的問題可以通過將timer新增到NSRunLoopCommonModes(kCFRunLoopCommonModes)來解決。

//將timer新增到NSDefaultRunLoopMode中
[NSTimer scheduledTimerWithTimeInterval:1.0
     target:self
     selector:@selector(timerTick:)
     userInfo:nil
     repeats:YES];
//然後再新增到NSRunLoopCommonModes裡
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
     target:self
     selector:@selector(timerTick:)
     userInfo:nil
     repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
複製程式碼

tip5.蘋果是如何實現autoreleasepool的?

autoreleasepool以棧的資料結構實現,主要通過下列三個函式完成.

  • objc_autoreleasepoolPush(壓入)
  • objc_autoreleasepoolPop(彈出)
  • objc_autorelease(釋放內部)

看函式名就可以知道,對autorelease分別執行push、pop操作。銷燬物件時執行release操作

tip6.Block記憶體管理分析

  1. 根據block在記憶體中的位置,block被分為三種型別:
  • NSGlobalBlock是位於全域性區的block,它是設定在程式的資料區域(.data區)中。
  • NSStackBlock是位於棧區,超出變數作用域,棧上的Block以及 __block變數都被銷燬。
  • NSMallocBlock是位於堆區,在變數作用域結束時不受影響。

注意:在 ARC 開啟的情況下,將只會有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 型別的 block。NSStackBlock會被系統自動copy一份到堆區。

  1. block什麼時候在全域性區,什麼時候在棧上,什麼時候又在堆上呢?
  • NSGlobalBlock
    • 定義全域性變數的地方有block語法時
void(^block)(void) = ^ { NSLog(@"Global Block");};
int main() {
}
複製程式碼
  • block語法的表示式中沒有使用應截獲的自動變數時
int(^block)(int count) = ^(int count) {
        return count;
    };
 block(2);
複製程式碼

總結:配置在全域性區的block,從變數作用域外也可以通過指標安全地使用

  • NSStackBlock
NSInteger i = 10; 
block = ^{ 
     NSLog(@"%ld", i); 
};
複製程式碼

總結:設定在棧上的block,如果其作用域結束,該block就被銷燬。

  • NSMallocBlock
- (void)viewDidLoad {
    [super viewDidLoad];  
    NSInteger i = 10;
    self.block = { 
        NSLog(@"%ld", i);
    };
}
複製程式碼

總結:即使變數作用域結束,堆上的Block依然存在

tip7.block裡面使用self會造成迴圈引用嗎?

很顯然答案不都是,有些情況下是可以直接使用self的:

  • block存在於靜態方法中,雖然block對self強引用著,但是self卻不持有這個靜態方法,所以完全可以在block內部使用self
[UIView animateWithDuration:0.3 animations:^{
        [self.tableview reloadData];
    }];
複製程式碼
  • 當block不是self的屬性時,self並不持有這個block,所以也不存在迴圈引用
void(^block)(void) = ^() {
        NSLog(@"%@", self);
    };
block();
複製程式碼

tip8.iOS程式中的記憶體分配

  • 棧區(stack)

    • 棧是向低地址擴充套件的資料結構,是一塊連續的記憶體的區域
    • 由編譯器自動分配並釋放
    • 存放函式的引數值,區域性變數等
    • 優點是快速高效,缺點時有限制,資料不靈活
    • 後進先出(last in first out)
    • 只要棧的剩餘空間大於所申請空間,系統將為程式提供記憶體,否則將報異常提示棧溢位
  • 堆區(heap)

    • 堆是向高地址擴充套件的資料結構,是不連續的記憶體區域
    • 由程式設計師分配和釋放,如果程式設計師不釋放,程式結束時,可能會由作業系統回收 ,比如在ios 中 alloc 都是存放在堆中。
    • 優點是靈活方便,資料適應面廣泛,但是效率有一定降低。
    • 申請方式:
      1. 首先應該知道作業系統有一個記錄空閒記憶體地址的連結串列。
      2. 當系統收到程式的申請時,會遍歷該連結串列,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點連結串列中刪除,並將該結點的空間分配給程式。
      3. 由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多餘的那部分重新放入空閒連結串列中
  • 全域性區(靜態區) (static)

    • 全域性變數和靜態變數的儲存是放在一起的,初始化的全域性變數和靜態變數存放在一塊區域(data區),未初始化的全域性變數和靜態變數在相鄰的另一塊區域(bss區)
    • 程式結束後由系統釋放。
  • 文字常量區

    • 存放常量字串,程式結束後由系統釋放
  • 程式程式碼區

    • 存放函式的二進位制程式碼
#import "ViewController.h"

int age = 24;//全域性初始化區(data區)
NSString *name;//全域性未初始化區(BSS區)
static NSString *sName = @"Dely";//全域性(靜態初始化)區

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

int a = 10;  全域性初始化區
  char *p;  全域性未初始化區

 main{
    int b; 棧區
    char s[] = "abc" 棧
    char *p1; 棧 
    char *p2 = "123456";  123456\0在常量區,p2在棧上。
    NSString *number = @"abc"; //abc在堆區,number在棧上。
    NSMutableArray *array = [NSMutableArray arrayWithCapacity:1];//分配而來的8位元組的區域就在堆中,array在棧中,指向堆區的地址
    NSInteger total = [self getTotalNumber:1 number2:1];
}

- (NSInteger)getTotalNumber:(NSInteger)number1 number2:(NSInteger)number2{
    return number1 + number2;//number1和number2 棧區
}
複製程式碼

tip9.NSCache優於NSDictionary的幾點?

  1. NSCache勝過NSDictionary之處在於,當系統資源將要耗盡時,它可以自動刪減快取。如果採用普通的字典,那麼就要自己編寫掛鉤,在系統發出“低記憶體”通知時手工刪減快取。

  2. NSCache並不會“拷貝”鍵,而是會“保留”它。此行為用NSDictionary也可以實現,然而需要編寫相當複雜的程式碼。NSCache物件不拷貝鍵的原因在於:很多時候,鍵都是不支援拷貝操作的物件來充當的。因此,NSCache不會自動拷貝鍵,所以說,在鍵不支援拷貝操作的情況下,該類用起來比字典更方便。

  3. NSCache是執行緒安全的,而NSDictionary不是。

tip10.NStimer準嗎?談談你的看法?如果不準該怎樣實現一個精確的NSTimer?

不準原因:

  1. NSTimer加在main runloop中,模式是NSDefaultRunLoopMode,main負責所有主執行緒事件,例如UI介面的操作,複雜的運算,這樣在同一個runloop中timer就會產生阻塞。

  2. 模式的改變。主執行緒的 RunLoop 裡有兩個預置的 Mode:kCFRunLoopDefaultMode(是 App 平時所處的狀態) 和 UITrackingRunLoopMode(追蹤 ScrollView 滑動時的狀態)。當你建立一個 Timer 並加到 DefaultMode 時,Timer 會得到重複回撥,但此時滑動一個 ScrollView 時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回撥,並且也不會影響到滑動操作。所以也會影響到 NSTimer 不準。

如何實現一個精確的 NSTimer:

  1. 在主執行緒中進行 NSTimer 操作,但是將 NSTimer 例項加到 main runloop 的 NSRunLoopCommonModes 中。避免被複雜運算操作或者UI介面重新整理所干擾。
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(showTime) userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
複製程式碼
  1. 在子執行緒中進行NSTimer的操作,再在主執行緒中修改UI介面顯示操作結果.
__block TestViewController *blockSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    blockSelf->_timer=[NSTimer scheduledTimerWithTimeInterval:1.0
                                            target:blockSelf
                                        selector:@selector(caculateLeftTimeForTomorrow)
                                          userInfo:nil
                                           repeats:YES] ;
    [[NSRunLoop currentRunLoop] addTimer:blockSelf->_timer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
});
複製程式碼

相關文章