我們在探究Runloop的本質前首先要知道什么是Runloop?
runloop定義:iOS程序中的運行循環(huán)機制,它能夠保證程序一直處于運行中狀態(tài)而不是執(zhí)行完任務后就立即退出
那么在項目的實際開發(fā)過程中阵具,我們又有哪些開發(fā)場景中使用到了runloop的循環(huán)機制尼碍遍?定铜,這里列舉runloop的常用場景如下:
- 定時器
- PerformSelector()
- GCD Async
- 所有的事件響應,手勢怕敬,列表滾動
- 多線程
- Autoreleasepool
- ...
當我們新建一個iOS程序時揣炕,系統(tǒng)就會默認在主線程給我們創(chuàng)建了一個runloop對象,代碼如下:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
// 在UIApplicationMain函數內部东跪,系統(tǒng)會自動創(chuàng)建一個runloop對象畸陡,并添加到主線程中
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
當我們創(chuàng)建一個MacOs的命令行項目,系統(tǒng)沒有默認為我們創(chuàng)建runloop對象虽填,我們發(fā)現程序執(zhí)行完語句后丁恭,就會立即退出,代碼如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
NSLog(@"111");
}
NSLog(@"2222");
return 0;
}
上面的代碼當執(zhí)行完NSLog(@"2222");
打印后斋日,程序就直接退出了牲览。也就是說當前主線程沒有runloop時程序執(zhí)行完任務就直接退出,不能夠一直保持運行狀態(tài)恶守。
我們如何才能獲取到當前的runloop對象尼第献?
蘋果為開發(fā)者提供了2套框架來訪問和使用runloop對象
- NSRunloop:Foundation框架API
- CFRunLoopRef:Core Foundation框架API
其中NSRunloop
是對CFRunLoopRef
進行的一層更加面向對象的OC語法封裝
// 獲取當前runloop
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
CFRunLoopRef currentRunLoop2 = CFRunLoopGetCurrent();
// 獲取主線程runloop
NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
CFRunLoopRef mainRunLoop2 = CFRunLoopGetMain();
接下來我們來探究下runloop相關API的底層源碼,源碼查看路徑:CF框架 -> CFRunLoop.c文件 -> _CFRunLoopGet0
我們跟蹤下runloop的核心函數代碼流程如下:
// __CFRunLoops變量是CFMutableDictionaryRef類型的兔港,它就是一個全局的字典類型對象庸毫,用來存儲runloop和對應線程的集合
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;
// 核心函數`_CFRunLoopGet0`,這個函數就是用來獲取runloop對象的
// 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();
}
// 執(zhí)行加鎖操作
__CFLock(&loopsLock);
// 判斷__CFRunLoops集合是否有值
if (!__CFRunLoops) {
// __CFRunLoops集合沒有值飒赃,解鎖蜗帜,往集合中添加值
__CFUnlock(&loopsLock);
// 初始化__CFRunLoops字典對象
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 創(chuàng)建一個主線程的runloop:mainLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 設置CFMutableDictionaryRef字典的值涣雕,主線程作為key,根據主線程創(chuàng)建出來的mainLoop作為value
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
// 釋放mainLoop
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 根據參數線程`t`作為key难礼,去CFMutableDictionaryRef字典中查找對應的runloop對象
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
// 沒有找到對應的runloop臀栈,根據傳遞進來的線程`t`刚盈,創(chuàng)建一個新的runloop對象
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
// 加鎖操作
__CFLock(&loopsLock);
// 再次根據參數線程`t`去全局字典中獲取對應的runloop對象
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
// 還是沒有找到對應的runloop
// 就將剛剛新建的newLoop作為value,參數線程`t`作為key挂脑,添加到全局字典中
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);
}
// 判斷傳遞過來的線程是否為當前線程(pthread_self())
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);
}
}
// 返回runloop
return loop;
}
通過上面函數_CFRunLoopGet0
中的底層源碼實現藕漱,我們可以知道runloop和線程的關系,得出如下結論:
- 每一個runloop對象中必定有一個與之對應的線程崭闲,因為程序中的所有runloop對象都保存在
CFMutableDictionaryRef
這個全局的字典集合中肋联,并且是以線程作為key
,runloop對象作為value
存儲在這個全局的集合中 - 手動新創(chuàng)建的子線程刁俭,默認是沒有runloop的橄仍,從上面的底層源碼可以知道,runloop是在調用
CFRunLoopRef
API獲取runloop對象的時候創(chuàng)建的,通過判斷使用線程作為key在全局字典中取出對應的runloop侮繁,如果沒有取到對應的runloop就用傳遞的線程新創(chuàng)建一個runloop
接下來我們再來看看runloop的底層數據結構虑粥,源碼查找路徑:CF框架 -> CFRunLoop.c -> struct __CFRunLoop
__CFRunLoop
結構體:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
uint32_t _winthread;
pthread_t _pthread; // 與之對應的線程
// commonMode集合,存儲在_commonModes中的模式宪哩,都可以運行在kCFRunLoopCommonModes這種模式下
CFMutableSetRef _commonModes;
// 所有在commonModes這個模式下工作的`timer\source\observer等`都放到`_commonModeItems`集合中
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode; // runloop當前正在運行的Mode
CFMutableSetRef _modes; // modes集合中存放的都是CFRunLoopModeRef對象
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
在__CFRunLoop
結構體中有一個CFMutableSetRef _modes
成員娩贷,modes
集合中又包含了多個CFRunLoopModeRef
對象
__CFRunLoopMode
結構體:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
Boolean _stopped;
char _padding[3];
CFStringRef _name; // Mode的名稱
CFMutableSetRef _sources0; // _sources0集合中存放的都是CFRunLoopSourceRef
CFMutableSetRef _sources1; // _sources1集合中存放的都是CFRunLoopSourceRef
CFMutableArrayRef _observers; // _observers集合中存放的都是CFRunLoopObserverRef
CFMutableArrayRef _timers; // _observers集合中存放的都是CFRunLoopTimerRef
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
在__CFRunLoopMode
中又包含有_sources0
、_sources1
锁孟、_observers
彬祖、_timers
這四個集合對象
__CFRunLoopSource
結構體
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
__CFRunLoopObserver
結構體
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
// runloop
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
__CFRunLoopTimer
結構體
typedef struct __CFRunLoopTimer * CFRunLoopTimerRef;
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
// runloop
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
上面的這些runloop相關的類的對應關系如圖:
runloop的運行模式,最常用的就兩種模式:
- kCFRunLoopDefaultMode
- UITrackingRunLoopMode
我們在平時的開發(fā)過程中也有使用過kCFRunLoopCommonModes
這種模式品抽,但是需要注意:
kCFRunLoopCommonModes并不是真正意義是上的mode储笑,它只是一個標記符,也就是說
kCFRunLoopDefaultMode
和UITrackingRunLoopMode
這兩種mode都被標記為common
圆恤,存儲在CFMutableSetRef _commonModes
集合中突倍,當我們設置runloop的模式為kCFRunLoopCommonModes
時,系統(tǒng)就會在_commonModes
這個集合中查找所有可以運行的模式來使用
我們從上面的CFRunLoopModeRef
結構體成員中知道盆昙,runloop的modes集合中含有_sources0
赘方、_sources1
、_observers
弱左、_timers
這四個,那么這些_sources0
炕淮、_sources1
拆火、_observers
、_timers
到底有什么作用涂圆?
runloop在運行循環(huán)中不停的處理的任務就是這些
_sources0
们镜、_sources1
、_observers
润歉、_timers
runloop中循環(huán)處理_observers
模狭,也可以理解為runloop在運行循環(huán)中一直監(jiān)聽著_observers
的以下這幾種狀態(tài)的變化
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 準備進入runloop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer事件
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理Sources事件
kCFRunLoopBeforeWaiting = (1UL << 5), // 準備進入休眠狀態(tài)
kCFRunLoopAfterWaiting = (1UL << 6), // 即將從休眠狀態(tài)喚醒
kCFRunLoopExit = (1UL << 7), // 退出runloop狀態(tài)
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有的狀態(tài)
};
下面我們通過代碼來驗證下在runloop中手動添加observer
,來觀察observer
的狀態(tài)變化踩衩,代碼如下:
// 創(chuàng)建一個Observe
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observerHandler, NULL);
// 創(chuàng)建一個runloop
CFRunLoopRef loop = CFRunLoopGetMain();
// 將observere添加到runloop
CFRunLoopAddObserver(loop, observer, kCFRunLoopDefaultMode);
// 釋放observer
CFRelease(observer);
observerHandler
監(jiān)聽函數
void observerHandler(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
}
我們通過打印可以看出嚼鹉,runloop確實是在各種observer
的狀態(tài)間不停的切換
接下來我們再通過滾動列表示例,驗證runloop的mode的切換過程驱富,代碼如下:
// 創(chuàng)建一個runloop
CFRunLoopRef loop = CFRunLoopGetMain();
// 創(chuàng)建一個Observe
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
{
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(loop);
NSLog(@"kCFRunLoopEntry == %@", mode);
CFRelease(mode);
}
break;
case kCFRunLoopExit:
{
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(loop);
NSLog(@"kCFRunLoopExit == %@", mode);
CFRelease(mode);
}
break;
default:
break;
}
});
// 將observere添加到runloop
CFRunLoopAddObserver(loop, observer, kCFRunLoopCommonModes);
// 釋放observer
CFRelease(observer);
我們通過打印可以看到锚赤,當我們拖動列表時,runloop的mode
從kCFRunLoopDefaultMode
切換至UITrackingRunLoopMode
當我們停止列表拖動后褐鸥,runloop的mode
又從UITrackingRunLoopMode
切換至kCFRunLoopDefaultMode
接下來我們通過底層源碼來研究runloop的整個循環(huán)執(zhí)行過程线脚,底層源碼查找路徑:CF框架 -> CFRunLoop.c文件 -> CFRunLoopRunSpecific -> __CFRunLoopRun
,底層核心源碼如下:
CFRunLoopRunSpecific
函數核心代碼
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
__CFRunLoopLock(rl);
// 獲取當前的Mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
// 判斷是否進入runloop
if (currentMode->_observerMask & kCFRunLoopEntry)
// 通知observer,進入runloop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// __CFRunLoopRun:此函數中真正的開始處理runnloop中的任務
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 判斷是否退出runloop
if (currentMode->_observerMask & kCFRunLoopExit )
// 通知observer浑侥,退出runloop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
__CFRunLoopRun
函數核心代碼姊舵,此函數內代碼經過了優(yōu)化刪除,只保留了核心流程的關鍵代碼
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
// 此do-while循環(huán)就是runloop能夠保證程序一直運行而不退出的的核心
do {
if (rlm->_observerMask & kCFRunLoopBeforeTimers) {
// 通知observer寓落,處理timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
}
if (rlm->_observerMask & kCFRunLoopBeforeSources) {
// 通知observer括丁,處理Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 處理blocks
__CFRunLoopDoBlocks(rl, rlm);
// 處理sources0
__CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
// 處理blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 判斷是否有source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
// 如果有source1,則跳轉到`handle_msg`標記處零如,執(zhí)行標記后的代碼
goto handle_msg;
}
}
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) {
// 通知observer躏将,即將進入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 設置runloop開始休眠
__CFRunLoopSetSleeping(rl);
// runloop在此處就開始處于休眠狀態(tài),等待消息來喚醒runloop考蕾,使用內核機制來進行線程阻塞祸憋,而不是死循環(huán)
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// runloop 取消休眠設置
// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);
// 通知observer,即將喚醒runloop
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
}
// `handle_msg`標記
handle_msg:;
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// 1肖卧、runloop被timer喚醒
CFRUNLOOP_WAKEUP_FOR_TIMER();
// 處理timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
} else if (livePort == dispatchPort) {
// 2蚯窥、runloop被dispatch喚醒
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
// 處理gcd相關事情
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {
// 3、runloop被source喚醒
CFRUNLOOP_WAKEUP_FOR_SOURCE();
// 處理source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
}
// 處理blocks
__CFRunLoopDoBlocks(rl, rlm);
// 判斷retVal的值
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
我們在對上面的核心流程進行一個簡要的梳理總結:
- 通知observer塞帐,進入runloop
- 執(zhí)行
__CFRunLoopRun
函數- 通知observer拦赠,處理timers
- 通知observer,處理sources
- 處理blocks
- 處理sources0
- 如果
sourceHandledThisLoop
條件滿足葵姥,處理blocks - 判斷是否有sources1荷鼠,有則跳轉到
handle_msg
- 通知observer,runloop即將進入休眠
- 通知observer榔幸,runloop即將結束休眠
- 如果runloop被timer喚醒允乐,處理timers
- 如果runloop被dispatch喚醒,處理gcd(dispatch_async(dispatch_get_main_queue(), ^{})
- 如果runloop被source喚醒削咆,處理source1
- 處理blocks
- 判斷retVal的值牍疏,決定是跳到循環(huán)第一步還是退出runloop
- 通知observer,退出runloop
上面runloop的循環(huán)執(zhí)行流程圖如下圖:
講解示例Demo地址:https://github.com/guangqiang-liu/08-Runloop
更多文章
- ReactNative開源項目OneM(1200+star):https://github.com/guangqiang-liu/OneM:歡迎小伙伴們 star
- iOS組件化開發(fā)實戰(zhàn)項目(500+star):https://github.com/guangqiang-liu/iOS-Component-Pro:歡迎小伙伴們 star
- 簡書主頁:包含多篇iOS和RN開發(fā)相關的技術文章http://www.reibang.com/u/023338566ca5 歡迎小伙伴們:多多關注拨齐,點贊
- ReactNative QQ技術交流群(2000人):620792950 歡迎小伙伴進群交流學習
- iOS QQ技術交流群:678441305 歡迎小伙伴進群交流學習