OC之RunLoop瞭解一下

一、進程

     進程是指在系統中正在運行的一個應用程序每個進程之間是獨立的,每個進程均運行在其專用且受保護的內存空間內比如同時打開QQ、Xcode,系統就會分別啓動2個進程

二、線程

        是用來執行任務的,線程只要徹底執行完A才能去執行B,這就是線程的安全,爲了防止任務能同時進行,引入了多線程。

        1、多線程:其實是CPU對任務的調度,任務的優先級越高,CPU被調度的就越快,對於單核CPU只能在同一時間處理一條線程,多線程併發執行實質是任務線程之間的不斷切換,因爲切換的太快,所以造成了同一時間執行的假象;比如兩個線程A、B;A執行到某一時間,切換到B,但是A還沒有執行完,系統會把A當前的位置和數據保存在棧中,依次循環,線程對CPU的消耗是比較大的,ios中不建議開多條線程

         2、線程分爲兩種:主線程和子線程

         在當前線程中開啓多個新線程,不按照順序執行叫異步,在當前線程中的任務按順序執行,不開啓新的線程,叫同步。隊列是狀態線程任務的結構:包括併發隊列和穿行隊列;併發隊列的線程可以同時一起執行,串行隊列只能依次逐一先後有序的執行

三、多線程的優缺點:

優點:1、提高CPU的利用率,不讓它閒着

           2、提高程序的執行效率,避免線程阻塞造成的卡頓

缺點:1、大量的線程降低代碼的可讀性

           2、需要大量的內存空間,降低程序的性能

           3、多個線程同時爭奪同一資源,容易線程不安全,比如mutalArray的例子

死鎖:A在等待B的結果,B在等待A的結果

多線程的實現方法有四種:Pthreads、CGD、NSThread、NSOpreation

多線程就是開闢線程、添加隊列、隊列中同步或者異步執行

四、主線程

一個ios程序運行後,系統會默認開啓一條線程,稱爲UI線程或者主線程,主線程是用來顯示、刷新界面、處理UI事件的,假如沒有RunLoop程序一運行就結束了,看不到持續運行的app

五、RunLoop的作用;學習原鏈接RunLoop深入

1、保持程序的持續運行;程序一啓動就會開一個主線程,主線程一開起來就會跑一個主線程對應的RunLoop,是Runloop保證主線程不被銷燬

2、處理項目中各種事件,比如:selector事件、定時器、觸摸事件等

3、節省CPU的資源,提高程序性能。當有操作的時候runloop會通知CPU該做什麼,沒有事情的時候讓它休息

Runloop實際上就是一個對象,這個對象管理了其需要處理的事件和消息,並提供了一個入口的函數,線程執行了這個函數就會一直處於這個函數內部接受消息----等待消息----處理消息,直到這個循環結束

RunLoop在哪裏開啓:在UIApplicationMain函數中啓動了RunLoop,程序不會馬上退出而是保持運行狀態,程序運行,主線程開啓,和主線程對應的Runloop開啓,那麼runloop肯定是在程序的入口main函數中開啓的

在iOS中有兩套API訪問和使用RunLoop

Foundation:NSRunloop

CoreFoundation :CFRunLoopRef 他是基於pthread來管理的

NSRunloop是基於CFRunLoopRef的一層OC的包裝

在源碼中創建的線程核心代碼

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
	t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    //如果沒有線程,創建一個
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
        //創建一個可變的字典
	CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //將主線程作爲參數創建一個Runloop
	CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        //將主線程的RunLoop和主線程以key/value的形式保存,可以看到線程和RunLoop是一一對應的
        
	CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
	if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
	    CFRelease(dict);
	}
	CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    //當輸入一個 currentRunLoop 時,會通過當前線程這個Key,在字典中尋找對應的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    
    //如果沒有找到
    if (!loop) {
        
        //重新創建一個
	CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        //然後將runloop和線程以 key/value的形式保存
	loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
	if (!loop) {
	    CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
	    loop = newLoop;
	}
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
	CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

很好的runloop帖子:Runloop全面學習

六、線程與runloop的關係

程序啓動時,系統會自動創建主線程的RunLoop,每條線程都有唯一的一個與之對應的RunLoop對象,主線程的Runloop已經自動創建好了,子線程的runloop需要自己手動創建,runloop在第一次獲取時創建,在線程結束時銷燬

獲取當前線程的Runloop,runloop是懶加載的,在發送消息時自動創建對象

[NSRunLoop currentRunLoop];

在iOS開發中,會遇到兩個線程對象:pthread_t 和 NSThread,NSThread只是對pthread的封裝,兩者肯定是一一對應的,我們可以通過pthread_main_thread_np()或者 [NSThread mainThread]來獲取主線程,也可以通過pthread_self() [NSThread currentThread]來獲取當前的線程。

蘋果是不允許直接來創建Runloop的。他只提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()

七、RunLoop相關類

我嘗試着打印了 [NSRunLoop mainRunLoop];

 mainRunLoop == <CFRunLoop 0x6040001f9a00 [0x103291c80]>
{wakeup port = 0xc03, stopped = false, ignoreWakeUps = true, 
  current mode = (none),
  common modes = <CFBasicHash 0x6040002557b0 [0x103291c80]>
{type = mutable set, count = 1,
entries =>
	2 : <CFString 0x103267818 [0x103291c80]>{contents = "kCFRunLoopDefaultMode"}
}
,
common mode items = (null),
modes = <CFBasicHash 0x604000255780 [0x103291c80]>{type = mutable set, count = 1,
entries =>
	2 : <CFRunLoopMode 0x604000192f10 [0x103291c80]>{name = kCFRunLoopDefaultMode, port set = 0xd03, queue = 0x60400014f4c0, source = 0x600000192e40 (not fired), timer port = 0x1503, 
	sources0 = (null),
	sources1 = (null),
	observers = (null),
	timers = (null),
	currently 556728920 (19355243452143) / soft deadline in: 1.84467247e+10 sec (@ -1) / hard deadline in: 1.84467247e+10 sec (@ -1)
},

}
}

可以看到Corefoundation中關於runloop的五個類

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopTimerRef

CFRunLoopObserverRef

 從打印中可以看出,一個RunLoop想要跑起來,必須要有Model支持,而Mode裏邊必須有(NSSet *)soure (NSArray *)Timer

這時候的(NSArray *)Observer是用來監聽runloop的狀態,這時候是不會激活的

1、CFRunloopModeRef

代表runloop運行模式,每個runloop都包含幾個mode。每個mode又包含若干個source、timer、observer,每次runloop啓動時,只能指定其中一個mode,這個mode被稱作currentMode,如果需要切換mode,只能退出runloop,再重新指定一個mode進入,這樣做主要是爲了分開不同的source、timer、observer,讓他們互不影響

runloop默認註冊了5個mode

NSDefaultRunLoopMode:app默認的mode,通常主線程是在這個mode下運行的

UITrackingRunLoopMode:界面跟蹤mode。用於scorllview追蹤觸摸滑動,保證界面滑動時不受影響

UIInitializationRunLoop:剛啓動時app進入第一個mode,啓動完成就不再使用

GSRunLoopReceiveRunLoopMode:接受系統時間的內部mode,繪圖服務,通常用不到

NSRunLoopCommonModes:這是一個佔位用的mode,不是一種真的mode

幾個mode的區別

    NSTimer * time = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(runloopIsRun) userInfo:nil repeats:YES];

    [[NSRunLoop currentRunLoop] addTimer:time forMode:NSDefaultRunLoopMode];

    [[NSRunLoop currentRunLoop] addTimer:time forMode:UITrackingRunLoopMode];

    [[NSRunLoop currentRunLoop] addTimer:time forMode:NSRunLoopCommonModes];

後面這一句相當於前兩者寫在一起的模式下運行(####其實這裏我也不是很明白####)

2、CFRunLoopTimeRef

CFRunLoopTimeRed是基於時間的觸發器,應該說就是NSTimer,它手RunLoop的mode影響

創建Timer的兩種方式

1> NSTimer * time = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(runloopIsRun) userInfo:nil repeats:YES];

這種方式必須手動添加到 RunLoop 中去纔會被調用

2> 通過方法scheduledTimerWithTimeInterval: target :  selector

    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    /*
     注意:調用了 scheduledTimer 返回的定時器,已經自動被添加到當前runLoop 中,而且是NSDefaultRunLoopMode ,想讓上述方法起作用,必須先讓添加了上述 timer的RunLoop 對象 run 起來,通過scheduledTimerWithTimeInterval 創建的 timer 可以通過以下方法修改 mode
     */
    [[NSRunLoop currentRunLoop] addTimer:time forMode:UITrackingRunLoopMode];

****提示****  GCD的定時器不受RunLoop的Mode影響

 CFRunLoopSourceRef  事件的輸入源

source的分類:

1 》port-Based Source  基於端口,跟其他線程進行交互的,屬於mac內核發過來的一些消息

2》 custom input Source 自定義輸入源

3》cocoa perform selector  sourcees

按照函數條用棧可分爲

1》source0:基於port的觸摸事件、點擊事件

2》source1:基於port的,通過內核和其他線程通信,接受分發系統事件,是和硬件的交互,觸摸首先在屏幕上包裝成一個event事件,在通過source1進行分發到source0,最後通過source0來處理

邏輯圖如下 

CFRunLoopObserver : 是觀察者,能夠監聽runloop的狀態改變,主要監聽以下幾個時間節點,在文檔的.h中這麼聲明
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //即將進入Loop
    kCFRunLoopBeforeTimers = (1UL << 1), //即將處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), //即將處理source
    kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),//剛從休眠中喚醒
    kCFRunLoopExit = (1UL << 7),         //即將退出  Loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU  //監聽所有事件
};
    /**
    創建觀察者,監聽runloop
     
     該方法可以在添加timer之前做點事情。可以在添加source之前搞點事情
     參數一:CFAllocatorRef allocator CFAllocatorGetDefault 默認值
     參數二:CFOptionFlags activities  監聽runloop的活動,也就是之前的枚舉
     參數三:Boolean repeats  是否重複監聽
     參數四:CFIndex order  寫0
     參數五:block
     */
   CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopRunHandledSource, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        
    });
    //添加觀察者,監聽當前的runloop
    CFRunLoopAddObserver(CFRunLoopGetCurrent(),observer , kCFRunLoopCommonModes);
    //cf的是不ARC管理內存的,需要釋放
    CFRelease(observer);

 整個runloop的運行邏輯如下圖:

八、RunLoop的應用

1 >    NSTimer  需求讓定時器在其他線程開啓

  /*

    > 需求

    > 有時候,用戶拖拽scrollView的時候,mode:UITrackingRunLoopMode,顯示圖片,如果圖片很大,會渲染比較耗時,造成不好的體驗,因此,設置當用戶停止拖拽的時候再顯示圖片,進行延遲操作

    - 方法1:設置scrollView的delegate  當停止拖拽的時候做一些事情

    - 方法2:使用performSelector 設置模式爲default模式 ,則顯示圖片這段代碼只能在RunLoop切換模式之後執行

   */

 

// 加載比較大的圖片時,

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event

{

    // inModes 傳入一個 mode 數組,這句話的意思是

    // 只有在 NSDefaultRunLoopMode 模式下才會執行 seletor 的方法顯示圖片

    UIImageView * imageView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 100, 100)];

    [imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"avater"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

}

效果爲:當用戶點擊之後,下載圖片,但是圖片太大,不能及時下載。這時用戶可能會做些其他 UI 操作,比如拖拽,但是如果用戶正在拖拽瀏覽其他的東西時,圖片下載完畢了,此時如果要渲染顯示,會造成不好的用戶體驗,所以當用戶拖拽完畢後,顯示圖片。這是因爲,用戶拖拽,處於 UITrackingRunLoopMode 模式下,所以圖片不會顯示

2、常駐線程

> 需求

  搞一個線程一直存在,一直在後臺做一些操作 比如監聽某個狀態, 比如監聽是否聯網。

    //一個線程對應一個runloop,創建一個線程會自動開闢一個runloop
    NSThread * runloopTherd = [[NSThread alloc]initWithTarget:self selector:@selector(runLoop) object:nil];
    [runloopTherd start];
    

-(void)runLoop
{
    NSLog(@"-----");
    /*
     * 創建RunLoop,如果RunLoop內部沒有添加任何Source Timer
     * 會直接退出循環,因此需要自己添加一些source才能保持RunLoop運轉
     */
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    
}

3、自動釋放池

Runloop循環時,在進入睡眠之前會清掉自動釋放池,並且創建一個新的釋放池,用於內部變量的銷燬。在子線程開RunLoop的時候一定要自己寫一個autoreleasepool。一個runloop對應一條線程,自動釋放池是針對當前線程裏邊的對象

    NSThread * releaseTherd = [[NSThread alloc]initWithTarget:self selector:@selector(releaseRun) object:nil];
    [releaseTherd start];

-(void)releaseRun
{
    //這樣保證了內存的安全
    @autoreleasepool{
        NSTimer * timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(runLoop) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    }
}

4、runloop與網絡請求、AFNetWorking ---摘自博客

iOS中,關於網絡請求的接口從上到下有:

CFSocket    最底層的接口,只負責socket通信

CFNetwork        -----> ASIHttpRequest   基於CFSocket的上層封裝

NSURLConnection   ----->  AFNetworking   基於CFNetwork的分裝,提供面向對象的接口

NSURLSession        ------->AFNetworking  iOS7中新增的接口,內部用到了NSURLConnection

五、PerformSelecter,這個單獨放出來,這裏只做簡單介紹

當調用NSObject的performSelecter:afterDelay:方法後,其實內部會創建一個timer,並添加到當前的線程的runloop中,所以如果當前的線程沒有runloop,則這個方法會失效

========稍後我會將RunLoop源代碼的註釋貼上=========

=======可能理解有錯或者不到位歡迎指出========

=========相關帖子=======

iOS底層原理總結 - RunLoop

深入理解RunLoop

OS底層原理總結 - RunLoop2

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章