iOS 中精確定時的常用方法

級別: ★☆☆☆☆
標簽:「iOS」「定時 」
作者: dac_1033
審校: QiShare團隊


定時器用于延遲一段時間或在指定時間點執(zhí)行特定的代碼窃诉,之前我們介紹過iOS中處理定時任務常用方法,通過不同方法創(chuàng)建的定時器廓奕,其可靠性與精度都有不同段誊。

  1. 定時器與runLoop:定時器NSTimer闰蚕、CADisplayLink,底層基本都是由 runLoop 支持的连舍。iOS中每個線程內部都會有一個NSRunLoop ,可以通過[NSRunLoop currentRunLoop]獲取當前線程中的runLoop 涩哟,二者是一一對應關系索赏。runLoop 啟動之后,就能夠讓線程在沒有消息時休眠贴彼,在有消息時被喚醒并處理消息潜腻,避免資源長期被占用。定時器可以作為資源被 add 到 runLoop 中器仗,受runLoop循環(huán)的控制及影響融涣。
  2. 可靠性指是否嚴格按照設定的時間間隔按時執(zhí)行selector;精度指支持的最小時間間隔是多少精钮,對程序中的定時器而言威鹿,由于線程的切換,處理任務的耗時程度不同轨香,可靠性和精度只是參考值忽你。

1. NSTimer的精度

影響NSTimer的執(zhí)行selector的因素:NSTimer被添加到特定mode的runLoop中;該mode型的runloop正在運行臂容;到達激發(fā)時間科雳。 runLoop 切換模式時,NSTimer 如果處于default模式下可能不會被觸發(fā)脓杉。每個 runLoop 的循環(huán)間隔也無法保證糟秘,一般時間間隔限制為50-100毫秒比較合理,如果某個任務比較耗時球散,runLoop 的處理下一個就會被順延尿赚,也就是說NSTimer但并不可靠。

測試代碼:

#import "QiNSTimer.h"

#define QiNSTimerInterval    0.0001

@interface QiNSTimer ()

@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) NSLock *lock;

@property (nonatomic, assign) NSInteger count;
@property (nonatomic, assign) NSTimeInterval lastTS;

@end

@implementation QiNSTimer


#pragma mark - NSTimer Methods

- (void)resumeTimer {
    
    if (_timer) {
        [self pauseTimer];
    }
    _timer = [NSTimer scheduledTimerWithTimeInterval:QiNSTimerInterval target:self selector:@selector(onTimeout:) userInfo:nil repeats:YES];
}

- (void)pauseTimer {

    [_timer invalidate];
    _timer = nil;
}

- (void)onTimeout:(NSTimer *)sender {
    
    NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];
    NSLog(@"---QiNSTimer--->>%ld  %.5f", (long)_count++, ts - _lastTS);
    _lastTS = ts;
}

@end

實驗設置:在代碼中我們只通過NSLog打印了兩次執(zhí)行onTimeout的時間差,我們通過對比ts - lastTS與QiNSTimerInterval的值吼畏、1s內執(zhí)行次數(shù)督赤,來確定NSTimer可否滿足QiNSTimerInterval這個精度。
注意:我們避免了onTimeout任何耗時操作泻蚊,從而盡量保證NSLog打印出的定時的精確性躲舌。

//// 實驗結果:

// QiNSTimerInterval為0.01時
2019-07-22 18:42:50.516502+0800 QiTimer[1063:226400] ---QiNSTimer--->>1  0.01002
2019-07-22 18:42:50.526461+0800 QiTimer[1063:226400] ---QiNSTimer--->>2  0.00996
2019-07-22 18:42:50.536480+0800 QiTimer[1063:226400] ---QiNSTimer--->>3  0.01002
.
.
.
2019-07-22 18:42:51.506502+0800 QiTimer[1063:226400] ---QiNSTimer--->>100  0.01055
2019-07-22 18:42:51.516437+0800 QiTimer[1063:226400] ---QiNSTimer--->>101  0.00998
2019-07-22 18:42:51.526183+0800 QiTimer[1063:226400] ---QiNSTimer--->>102  0.00974

// QiNSTimerInterval為0.001時
2019-07-22 18:45:59.655696+0800 QiTimer[1075:227871] ---QiNSTimer--->>1  0.00095
2019-07-22 18:45:59.656705+0800 QiTimer[1075:227871] ---QiNSTimer--->>2  0.00101
2019-07-22 18:45:59.657709+0800 QiTimer[1075:227871] ---QiNSTimer--->>3  0.00100
.
.
.
2019-07-22 18:46:00.654778+0800 QiTimer[1075:227871] ---QiNSTimer--->>1000  0.00104
2019-07-22 18:46:00.655737+0800 QiTimer[1075:227871] ---QiNSTimer--->>1001  0.00096
2019-07-22 18:46:00.656741+0800 QiTimer[1075:227871] ---QiNSTimer--->>1002  0.00100

// QiNSTimerInterval為0.0001時
2019-07-22 18:48:07.960160+0800 QiTimer[1085:228783] ---QiNSTimer--->>1  0.00040
2019-07-22 18:48:07.960422+0800 QiTimer[1085:228783] ---QiNSTimer--->>2  0.00027
2019-07-22 18:48:07.960646+0800 QiTimer[1085:228783] ---QiNSTimer--->>3  0.00022
.
.
.
2019-07-22 18:48:09.316050+0800 QiTimer[1085:228783] ---QiNSTimer--->>10001  0.00012
2019-07-22 18:48:09.316157+0800 QiTimer[1085:228783] ---QiNSTimer--->>10002  0.00011
2019-07-22 18:48:09.316253+0800 QiTimer[1085:228783] ---QiNSTimer--->>10003  0.00009

說明:
在設置不同timeInterval值實驗時,對比log左側時間戳及l(fā)og數(shù)量性雄。當QiNSTimerInterval為0.001時没卸,1秒鐘內打印了1000條log,兩條log的時間間隔可控秒旋,也即NSTimer允許1ms的時間精度约计。當QiNSTimerInterval為0.0001時,進行以上對比迁筛,數(shù)據(jù)出現(xiàn)偏差煤蚌。因此,我們得出细卧,理想狀態(tài)下NSTimer的精度為1ms尉桩。

注意:

  1. NSTimer的時間精度雖然為1ms,但是只是理想狀態(tài)下贪庙,任何操作都可能會使onTimeout延時執(zhí)行蜘犁。例如,現(xiàn)實中止邮,我們在界面輸出一個倒計時这橙,如果設置QiNSTimerInterval為0.001,界面中秒位的變化明顯變慢导披,正常使用NSTimer進行毫秒刷新時屈扎,一般只精確到100ms才不會感到異常。
  2. 在一定程度上保證timer“準時”的方法:在子線程中創(chuàng)建timer盛卡,在子線程中進行定時任務的操作助隧,需要UI操作時切換回主線程進行操作;或者在子線程中創(chuàng)建timer滑沧,在主線程進行定時任務的操作并村。

2. GCDTimer 的精度

回顧一下 GCDTimer 的基本實現(xiàn)過程:

// 1. 創(chuàng)建 dispatch source,指定檢測事件為定時
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue("Timer_Queue", 0));
// 2. 設置定時器啟動時間滓技、間隔
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC,  0 * NSEC_PER_SEC); 
// 3. 設置callback
dispatch_source_set_event_handler(timer, ^{
        NSLog(@"timer fired");
    });
dispatch_source_set_event_handler(timer, ^{
       //取消定時器時一些操作
    });
// 4. 啟動定時器(剛創(chuàng)建的source處于被掛起狀態(tài))
dispatch_resume(timer);
// 5. 暫停定時器
dispatch_suspend(timer);
// 6. 取消定時器
dispatch_source_cancel(timer);
timer = nil;

GCDTimer相較于NSTimer的代碼處理過程優(yōu)點很明顯哩牍,NSTimer必須保證有一個活躍的runloop、創(chuàng)建與撤銷必須在同一個線程操作令漂、內存管理有潛在泄露的風險等膝昆,從上面的實現(xiàn)過程就可以看出使用GCDTimer基本沒有這些顧慮丸边。按照NSTimer的測試邏輯對GCDTimer也進行相應測試,代碼如下:

#import "QiGCDTimer.h"

@interface QiGCDTimer ()

@property (strong, nonatomic) dispatch_source_t timer;

@property (nonatomic, assign) NSInteger count;
@property (nonatomic, assign) NSTimeInterval lastTS;

@end

@implementation QiGCDTimer

+ (QiGCDTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats queue:(dispatch_queue_t)queue block:(void (^)(void))block {
    
    QiGCDTimer *timer = [[QiGCDTimer alloc] initWithInterval:interval repeats:repeats queue:queue block:block];
    return timer;
}

- (instancetype)initWithInterval:(NSTimeInterval)interval repeats:(BOOL)repeats queue:(dispatch_queue_t)queue block:(void (^)(void))block {
    
    self = [super init];
    if (self) {
        _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
        dispatch_source_set_event_handler(self.timer, ^{
            if (!repeats) {
                dispatch_source_cancel(self.timer);
            }
            block();
            
            
            //// 測試
            [self onTimeout];
        });
        dispatch_resume(self.timer);
    }
    return self;
}

- (void)dealloc {
    
    [self invalidate];
}

- (void)invalidate {
    
    if (self.timer) {
        dispatch_source_cancel(self.timer);
    }
}

- (void)onTimeout {
    
    NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];
    NSLog(@"---QiGCDTimer--->>%ld  %.5f", (long)_count++, ts - _lastTS);
    _lastTS = ts;
}

@end

測試結果及應說明的事項基本與NSTimer一致荚孵。

3. CADisplayLink

CADisplayLink 屬于 QuartzCore框架妹窖,它調用間隔與屏幕刷新頻率一致,每秒 60 幀收叶,間隔 16.67ms骄呼。 當需與顯示更新同步的定時時(如刷新界面動畫等),建議CADisplayLink判没,可以省去一些多余的計算蜓萄。我們之前沒有介紹過CADisplayLink,下面我們看一下CADisplayLink的用法和精度:

3.1 調用形式
- (void)resumeCADisplayLink {

        _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(rotate)];
        _displayLink.frameInterval = 1;
        [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

- (void) pauseCADisplayLink {

    [_displayLink invalidate];
    _displayLink = nil;
}

3.2 幾個屬性
  • frameInterval
    表示間隔多少幀調用一次selector澄峰,默認為1嫉沽,即每幀都調用一次。官方文檔中強調俏竞,當該值被設定小于1時绸硕,結果是不可預知的。
  • duration
    表示兩次屏幕刷新之間的時間間隔魂毁,只讀屬性臣咖,該屬性在target的selector被首次調用以后才會被賦值,我們可以計算出selector的調用間隔時間為duration * frameInterval漱牵。
    現(xiàn)存的iOS設備屏幕的刷新頻率為60Hz,這一點可以從CADisplayLink的duration屬性看出來疚漆。duration的值為1/60酣胀,即0.166666...
  • timestamp
    表示屏幕顯示的上一幀的時間戳,只讀屬性娶聘,CFTimeInterval類型闻镶,該屬性通常被target用來計算下一幀中應該顯示的內容。
  • preferredFramesPerSecond
    可以通過該屬性來設置CADisplayLink每秒刷新次數(shù)丸升,默認值為屏幕最大幀率60Hz铆农,如果在特定幀率內無法提供對象的操作,可以通過降低幀率解決狡耻,實際的屏幕幀率會和手動設置的preferredFramesPerSecond值有一定的出入墩剖。
3.3 CADisplayLink的精度

iOS設備的屏幕刷新頻率(FPS)是60Hz,CADisplayLink調用間隔與屏幕刷新頻率一致夷狰,即最小精度為 16.67 ms岭皂。

同樣按照NSTimer的測試邏輯對CADisplayLink也進行相應測試,代碼如下:

#import "QiCADisplayLink.h"
#import <QuartzCore/QuartzCore.h>

@interface QiCADisplayLink ()

@property (nonatomic, strong) CADisplayLink *displayLink;

@property (nonatomic, assign) NSInteger count;
@property (nonatomic, assign) NSTimeInterval lastTS;

@end

@implementation QiCADisplayLink


#pragma mark - NSTimer Methods

- (void)resumeDisplayLink {

    _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onTimeout)];
    [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)pauseDisplayLink {

    [_displayLink invalidate];
    _displayLink = nil;
}


- (void)onTimeout {
    
    NSTimeInterval ts = [[NSDate date] timeIntervalSince1970];
    NSLog(@"---QiCADisplayLink--->>%ld  %.5f", (long)_count++, ts - _lastTS);
    _lastTS = ts;
}

@end
//// 測試結果
2019-07-23 10:10:49.027269+0800 QiTimer[659:82685] ---QiCADisplayLink--->>1  0.01681
2019-07-23 10:10:49.043827+0800 QiTimer[659:82685] ---QiCADisplayLink--->>2  0.01659
2019-07-23 10:10:49.060542+0800 QiTimer[659:82685] ---QiCADisplayLink--->>3  0.01671
.
.
.
2019-07-23 10:10:50.010421+0800 QiTimer[659:82685] ---QiCADisplayLink--->>60  0.01664
2019-07-23 10:10:50.027155+0800 QiTimer[659:82685] ---QiCADisplayLink--->>61  0.01673
2019-07-23 10:10:50.043830+0800 QiTimer[659:82685] ---QiCADisplayLink--->>62  0.01669

注意:

  1. 理想狀態(tài)下沼头,1s內執(zhí)行60次爷绘,最小精度為16.7ms左右书劝,精度誤差一般在 0.1 ~ 0.5 毫秒之間,精度比 NSTimer 要高土至。CADisplayLink運行在主線程中在耗時任務之后购对,精度也不可控,需要借助多線程處理陶因。
  2. 如果想保證精度骡苞,需要先確保任務能夠在最小時間間隔內執(zhí)行完成,CADisplayLink 就比較可靠(例如毫秒級倒計時坑赡,這種比較簡單非耗時任務可以保證質量烙如,但是每次倒計時應以16.7ms為單位累加)。

4. iOS/OS X 中的高精度定時器

上述的幾種定時器雖然形式與用法不一毅否,但核心邏輯實際是一樣的亚铁,都受限于蘋果為提高性能采用的各種策略,可能導致下一次無法實時地執(zhí)行selector螟加。如果你確有需求要使用更高精度的定時器(一般視頻/音頻徘溢、精確幀速率的游戲等相關數(shù)據(jù)流操作中會需要),蘋果也提供了相應方法 iOS/OS X 中的高精度定時器捆探。這里說的高精度定時器與之前介紹的幾個定時器處理邏輯不一樣然爆,它是基于高優(yōu)先級的線程調度類創(chuàng)建的定時器,在沒有多線程沖突的情況下黍图,這類定時器的請求會被優(yōu)先處理曾雕。

iOS/OS X 中的高精度定時器邏輯:把定時器所在的線程,移到高優(yōu)先級的線程調度類助被;使用底層更精確的計時器API(以CPU時鐘為參照的計時API)剖张。

4.1 使用過程
  • 將計時線程,調度為實時線程
    把定時器所在的線程揩环,移到高優(yōu)先級的線程調度類搔弄,即the real time scheduling class中:
#include <mach/mach.h>
#include <mach/mach_time.h>
#include <pthread.h>
 
void move_pthread_to_realtime_scheduling_class(pthread_t pthread)
{
    mach_timebase_info_data_t timebase_info;
    mach_timebase_info(&timebase_info);
 
    const uint64_t NANOS_PER_MSEC = 1000000ULL;
    double clock2abs = ((double)timebase_info.denom / (double)timebase_info.numer) * NANOS_PER_MSEC;
 
    thread_time_constraint_policy_data_t policy;
    policy.period      = 0;
    policy.computation = (uint32_t)(5 * clock2abs); // 5 ms of work
    policy.constraint  = (uint32_t)(10 * clock2abs);
    policy.preemptible = FALSE;
 
    int kr = thread_policy_set(pthread_mach_thread_np(pthread_self()),
                   THREAD_TIME_CONSTRAINT_POLICY,
                   (thread_policy_t)&policy,
                   THREAD_TIME_CONSTRAINT_POLICY_COUNT);
    if (kr != KERN_SUCCESS) {
        mach_error("thread_policy_set:", kr);
        exit(1);
    }
}
  • 會用到的計時API
    使用更精確的計時API mach_wait_until(),如下代碼使用mach_wait_until()等待10秒:
#include <mach/mach.h>
#include <mach/mach_time.h>
 
static const uint64_t NANOS_PER_USEC = 1000ULL;
static const uint64_t NANOS_PER_MILLISEC = 1000ULL * NANOS_PER_USEC;
static const uint64_t NANOS_PER_SEC = 1000ULL * NANOS_PER_MILLISEC;
 
static mach_timebase_info_data_t timebase_info;
 
static uint64_t abs_to_nanos(uint64_t abs) {
    return abs * timebase_info.numer  / timebase_info.denom;
}
 
static uint64_t nanos_to_abs(uint64_t nanos) {
    return nanos * timebase_info.denom / timebase_info.numer;
}
 
void example_mach_wait_until(int argc, const char * argv[])
{
    mach_timebase_info(&timebase_info);
    uint64_t time_to_wait = nanos_to_abs(10ULL * NANOS_PER_SEC);
    uint64_t now = mach_absolute_time();
    mach_wait_until(now + time_to_wait);
}
4.2 該定時器的精度

mach_absolute_time() 用于獲取機器時間(單位是納秒)丰滑,測試代碼來源于網絡顾犹,其功能展示了高精度定時器與NSTimer的對比。

5. 總結

  1. NSTimer 最常用褒墨,需要注意的就是加入的 runLoop 的 Mode 炫刷,若是子線程,需要手動 run 這個 RunLoop 貌亭;同時注意使用 invalidate 手動停止定時柬唯,否則引起內存泄漏;NSTimer的創(chuàng)建與撤銷必須在同一個線程操作圃庭,不能跨越線程操作锄奢;
  2. GCD Timer 較 NSTimer 精度高失晴,一般用于對文件資源等定期讀寫操作很方便,使用時需要注意 dispatch_resume 與 dispatch_suspend 配套拘央,并且要給 dispatch source 設置新值或者置nil涂屁,需先 dispatch_source_cancel(timer) ,否則會導致崩潰灰伟;
  3. 需與顯示更新同步的定時拆又,建議 CADisplayLink ,可以省去多余計算栏账;
  4. 高精度定時帖族,一般視頻/音頻、精確幀速率的游戲等相關數(shù)據(jù)流操作中會需要挡爵;
  5. iOS中任何定時器的精度竖般,都只是個參考值。

工程源碼GitHub地址


推薦文章:
Sign In With Apple(一)
算法小專欄:動態(tài)規(guī)劃(一)
Dart基礎(一)
Dart基礎(二)
Dart基礎(三)
Dart基礎(四)
iOS 短信驗證碼倒計時按鈕
iOS 環(huán)境變量配置
iOS 中處理定時任務的常用方法

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末茶鹃,一起剝皮案震驚了整個濱河市涣雕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌闭翩,老刑警劉巖挣郭,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異疗韵,居然都是意外死亡兑障,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門蕉汪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來旺垒,“玉大人,你說我怎么就攤上這事肤无。” “怎么了骇钦?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵宛渐,是天一觀的道長。 經常有香客問我眯搭,道長窥翩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任鳞仙,我火速辦了婚禮寇蚊,結果婚禮上,老公的妹妹穿的比我還像新娘棍好。我一直安慰自己仗岸,他們只是感情好允耿,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扒怖,像睡著了一般较锡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盗痒,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天蚂蕴,我揣著相機與錄音,去河邊找鬼俯邓。 笑死骡楼,一個胖子當著我的面吹牛,可吹牛的內容都是我干的稽鞭。 我是一名探鬼主播鸟整,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼川慌!你這毒婦竟也來了吃嘿?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤梦重,失蹤者是張志新(化名)和其女友劉穎兑燥,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體琴拧,經...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡降瞳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蚓胸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挣饥。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖沛膳,靈堂內的尸體忽然破棺而出扔枫,到底是詐尸還是另有隱情,我是刑警寧澤锹安,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布短荐,位于F島的核電站,受9級特大地震影響叹哭,放射性物質發(fā)生泄漏忍宋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一风罩、第九天 我趴在偏房一處隱蔽的房頂上張望糠排。 院中可真熱鬧,春花似錦超升、人聲如沸入宦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽云石。三九已至唉工,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間汹忠,已是汗流浹背淋硝。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宽菜,地道東北人谣膳。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像铅乡,于是被迫代替她去往敵國和親继谚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內容