iOS 一篇文章搞定GCD

想了解NSOperation與GCD的區別可參考iOS多線程之NSOperation及簡單練習


文章內容較長,介紹下主要的目錄
一、GCD介紹
二、任務的執行:同步、異步與柵欄
三、隊列
四、代碼使用示例
五、一次性執行及單例創建
六、延遲操作
七、柵欄調度和線程通信示例
八、調度組(dispatch_group)
九、快速迭代(類似for循環)
十、信號量(dispatch_semaphore)
其他的CGD相關內容我會慢慢補充的~

一、GCD介紹

1.簡介

Grand Central Dispatch,既大名鼎鼎的狗艸的.它是蘋果為多核的并行運算提出的解決方案,會自動合理地利用更多的CPU內核(比如雙核、四核),最重要的是會自動管理線程的生命周期(創建線程、調度任務、銷毀線程),只需要告訴GCD要執行什么任務,不需要編寫任何管理代碼。同時它使用的也是 c語言,不過由于使用了 Block(Swift里叫做閉包),使得使用起來更加方便.

2.GCD的核心概念

2.1 GCD中的主要核心概念是任務與隊列

  • 任務:執行什么操作

  • 隊列:用來存放任務

2.2 簡要執行流程和說明:

  • 1 將任務添加到隊列,并且指定執行任務的函數

  • 2 任務使用 block 封裝
    任務的 block 沒有參數也沒有返回值

  • 3 執行任務的函數

  • 異步 dispatch_async

    • 不用等待當前語句執行完畢,就可以執行下一條語句

    • 會開啟線程執行 block 的任務

    • 異步是多線程的代名詞

  • 同步 dispatch_sync

  • 必須等待當前語句執行完畢,才會執行下一條語句

  • 不會開啟線程

  • 在當前執行 block 的任務

  • 4 隊列 - 負責調度任務

  • 串行隊列

  • 并發隊列

  • 主隊列

  • 全局隊列

3.GCD與NSThread的對比

  • 所有的代碼寫在一起的,讓代碼更加簡單,易于閱讀和維護

    NSThread 通過 @selector 指定要執行的方法,代碼分散。

    GCD 通過 block 指定要執行的代碼,代碼集中。

  • 使用 GCD 不需要管理線程的創建/銷毀/復用的過程!程序員不用關心線程的生命周期。

  • 如果要開多個線程 NSThread 必須實例化多個線程對象。

  • NSThread 靠 NSObject 的分類方法實現的線程間通訊,GCD 靠 block。

二、任務的執行:同步、異步與柵欄

同步(dispatch_sync):任務執行時,只能在當前線程執行任務,不具備開啟其他線程的能力


dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

異步 (dispatch_async):具備開啟新線程的能力,能將任務放在新線程中執行


dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

柵欄 (dispatch_barrier_async):

功能:攔截前面的任務,等前面任務全部執行完畢后才會執行柵欄的任務,待柵欄任務執行完畢后才會繼續執行后面的任務

使用方法:

使用柵欄,就不就能使用全局隊列(蘋果對全局隊列內部進行了修改)且所有的任務都必須添加到同一隊列中,柵欄才能起到攔截作用

詳情請看七的示例代碼


dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block)

barrier執行示意圖.png

三、隊列

1.串行隊列

  • 特點:

  • 以先進先出的方式,順序調度隊列中的任務執行

  • 無論隊列中指定執行任務的方式是同步還是異步,都只會等待前一個任務執行完成后再被調度

  • 創建方式:


dispatch_queue_t queue = dispatch_queue_create("自定義線程名", DISPATCH_QUEUE_SERIAL);

dispatch_queue_t queue = dispatch_queue_create("自定義線程名", NULL);
  • 注意:

當使用同步執行任務的方式(sync)往串行隊列中添加任務,會卡主當前的串行隊列,造成死鎖

2.并發隊列

  • 特點:

  • 以隨機的方式并發調度隊列中的任務執行

  • 若是以同步的方式執行任務,會等待任務執行完成后,再調度隊列中的其他任務(既在同步的方式下,并發功能不會生效)

  • 若是以異步的方式執行任務,只要底層線程池中有可用的資源,就會直接執行隊列中的其他任務

  • 創建方式:


dispatch_queue_t queue = dispatch_queue_create("自定義線程名", DISPATCH_QUEUE_CONCURRENT);

創建串行和并發隊列的函數參數:

第一個參數: 隊列的名稱

第二個參數: 告訴系統需要創建一個并發隊列還是串行隊列

-DISPATCH_QUEUE_SERIAL 串行

-DISPATCH_QUEUE_CONCURRENT 并發

3.主隊列

  • 特點:

  • 專用用在主線程上調度任務的隊列

  • 是一種系統提供的特殊串行隊列

  • 如果當前主線程正在執行任務,那么主隊列中所添加的任務就不會被調度

  • 主隊列只需要獲取,不用手動創建

  • 獲取方式:


dispatch_queue_t queue = dispatch_get_main_queue();

4.全局隊列

  • 特點:

  • 是系統提供的一種并發隊列,以供全局使用

  • 無需創建,可直接獲取

  • 若要使用柵欄阻塞任務,就不能使用全局隊列,應自己創建并發隊列

  • 獲取方式:


dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  • 全局隊列參數:

/* 第一個參數:服務質量(隊列對任務調度的優先級)/iOS 7.0 之前,是優先級

iOS 8.0(新增)
  ○ QOS_CLASS_USER_INTERACTIVE 0x21, 用戶交互(希望最快完成-不能用太耗時的操作)
  ○ QOS_CLASS_USER_INITIATED 0x19, 用戶期望(希望快,也不能太耗時)
  ○ QOS_CLASS_DEFAULT 0x15, 默認(用來底層重置隊列使用的,不是給程序員用的)
  ○ QOS_CLASS_UTILITY 0x11, 實用工具(專門用來處理耗時操作!)
  ○ QOS_CLASS_BACKGROUND 0x09, 后臺
  ○ QOS_CLASS_UNSPECIFIED 0x00, 未指定,可以和iOS 7.0 適配

iOS 7.0
  ○ DISPATCH_QUEUE_PRIORITY_HIGH 2 高優先級
  ○ DISPATCH_QUEUE_PRIORITY_DEFAULT 0 默認優先級
  ○ DISPATCH_QUEUE_PRIORITY_LOW (-2) 低優先級
  ○ DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN 后臺優先級 為未來保留使用的,應該永遠傳入0

第二個參數:無用,傳入0既可

結論:如果要適配 iOS 7.0 & 8.0,使用以下代碼: dispatch_get_global_queue(0, 0);

*/

四、代碼使用示例


#import "ViewController.h"

@implementation ViewController
/*
如果是在子線程中調用 同步函數 + 主隊列, 那么沒有任何問題
*/
- (void)syncExceptMain
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
    // 該block會在子線程中執行
        NSLog(@"%@", [NSThread currentThread]);
        dispatch_queue_t queue = dispatch_get_main_queue();
        dispatch_sync(queue, ^{
    // 該block會在主線程執行
            NSLog(@"%@", [NSThread currentThread]);
        });
    });
}

/*
如果是在主線程中調用同步函數 + 主隊列, 那么會導致死鎖
導致死鎖的原因:
若在主線程中執行sync函數,此時sync內的block在等待主線程執行完畢,而sync本身有在被主線程執行,導致雙發都無法執行完畢,造成死鎖。
*/
- (void)syncMain
{
    NSLog(@"%@", [NSThread currentThread]);
    // 主隊列:
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
    //此時內部代碼在等待主線程執行完畢,而主線程又在執行該sync操作,從而造成死鎖
        NSLog(@"----------");
        NSLog(@"%@", [NSThread currentThread]);
    });
}

/*
異步 + 主隊列 : 任務在主線程中執行,且異步失效,只會同步執行
*/
- (void)asyncMain
{
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{ 
        NSLog(@"%@", [NSThread currentThread]);
    });
}

/*
同步 + 并發 : 不開啟新的線程,只會在當前線程順序執行
*/
- (void)syncConCurrent
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_sync(queue, ^{
        NSLog(@"任務1  == %@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"任務2  == %@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"任務3  == %@", [NSThread currentThread]);
    });
}

/*
同步 + 串行: 不開啟新的線程,只會在當前線程順序執行
*/
- (void)syncSerial
{
    dispatch_queue_t queue = dispatch_queue_create("com.520it.lnj", NULL);
    dispatch_sync(queue, ^{
        NSLog(@"任務1  == %@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{
        NSLog(@"任務2  == %@", [NSThread currentThread]);
    });

    dispatch_sync(queue, ^{  
        NSLog(@"任務3  == %@", [NSThread currentThread]);
    });
}

/*
異步 + 串行:會開啟新的線程
但是只會開啟一個新的線程
*/
- (void)asynSerial
{
/*
能夠創建新線程的原因:我們是使用"異步"函數調用
只能創建1個子線程的原因:我們的隊列是串行隊列
*/
    dispatch_queue_t queue = dispatch_queue_create("自定義線程名", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"任務1  == %@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"任務2  == %@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"任務3  == %@", [NSThread currentThread]);
    });
}

/*
異步 + 并發 : 會開啟多個線程并發執行
*/
- (void)asynConcurrent
{
    dispatch_queue_t queue = dispatch_queue_create("自定義線程名", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"任務1  == %@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"任務2  == %@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"任務3  == %@", [NSThread currentThread]);
    });
}

@end

/*

兩個注意點:

當調用同步函數時,同步函數范圍后的代碼只能在同步函數全部執行完畢后執行;

當調用異步函數時,異步函數范圍后的代碼不用等函數執行完畢就會執行

*/

同步執行 異步執行
串行隊列 不開線程,在當前線程,順序執行 開1條線程,順序執行
并發隊列 不開線程,在當前線程,順序執行 開多條線程,一起執行
主隊列 不開線程,會造成死鎖 不開線程,在主線程空閑的時候執行
全局隊列 不開線程,在當前線程,順序執行 開多條線程,一起執行

五、一次性執行及單例創建

  • 使用dispatch_once函數讓某段代碼在程序運行時只執行一次(通過函數內部的鎖來保證線程安全)

  • 主要作用:實現單例模式


// 使用 dispatch_once 實現單例

+ (instancetype)sharedSingleton {
    static id instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

// 模擬dispatch_once來實現單例

// 使用互斥鎖實現單例

+ (instancetype)sharedSync {
    static id syncInstance;
    @synchronized(self) {
        if (syncInstance == nil) {
            syncInstance = [[self alloc] init];
        }
    }
    return syncInstance;
}

//兩種方法的性能對比
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    long largeNumber = 1000 * 1000;

// 測試互斥鎖
    CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
    for (long i = 0; i < largeNumber; ++i) {
        [Singleton sharedSync];
    }
    NSLog(@"互斥鎖: %f", CFAbsoluteTimeGetCurrent() - start);

// 測試 dispatch_once
    start = CFAbsoluteTimeGetCurrent();
    for (long i = 0; i < largeNumber; ++i){
        [Singleton sharedSingleton];
    }
    NSLog(@"dispatch_once: %f", CFAbsoluteTimeGetCurrent() - start);
}

//結果表明用dispatch_once方法來實現單例模式效率遠高與用互斥鎖模擬實現

六、延遲操作

  • 通過dispatch_after函數實現延緩代碼執行

#pragma mark - 延遲執行

- (void)delay {
/**

方法解釋:

從現在開始,經過多少納秒,由"隊列"調度  異步執行 block 中的代碼

參數

1. when    從現在開始,經過多少納秒

2. queue   隊列

3. block   異步執行的任務

*/

    dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));
    void (^task)() = ^ {
        NSLog(@"%@", [NSThread currentThread]);
  };

// 主隊列

    dispatch_after(when, dispatch_get_main_queue(), task);

// 全局隊列

//    dispatch_after(when, dispatch_get_global_queue(0, 0), task);

// 串行隊列

//  dispatch_after(when, dispatch_queue_create("itheima", NULL), task);

NSLog(@"come here");

}

七、柵欄調度和線程通信示例

首先先配置下plist文件

info.plist

- (void)barrier
{
    dispatch_queue_t queue = dispatch_queue_create("隊列名", DISPATCH_QUEUE_CONCURRENT);
//    dispatch_queue_t queue2 = dispatch_queue_create("隊列名", DISPATCH_QUEUE_CONCURRENT);
//    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    __block UIImage *image1 = nil;
    __block UIImage *image2 = nil;

// 1.開啟一個新的線程下載第一張圖片
    dispatch_async(queue, ^{
        NSURL *url = [NSURL URLWithString:@"http://www.shaimn.com/uploads/allimg/150509/1-150509233453.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];
        image1 = image;
        NSLog(@"圖片1下載完畢");
    });

// 2.開啟一個新的線程下載第二張圖片
    dispatch_async(queue, ^{
        NSURL *url = [NSURL URLWithString:@"http://m.818today.com/imgsy/image/2016/0215/6359115208730406558041085.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];
        image2 = image;
        NSLog(@"圖片2下載完畢");
    });

// 3.開啟一個新的線程, 合成圖片
// 柵欄
    dispatch_barrier_async(queue, ^{
// 圖片下載完畢
        NSLog(@"%@ %@", image1, image2);
// 1.開啟圖片上下文
        UIGraphicsBeginImageContext(CGSizeMake(200, 200));
// 2.將第一張圖片畫上去
        [image1 drawInRect:CGRectMake(0, 0, 100, 200)];
// 3.將第二張圖片畫上去
        [image2 drawInRect:CGRectMake(100, 0, 100, 200)];
// 4.從上下文中獲取繪制好的圖片
        UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
// 5.關閉上下文
        UIGraphicsEndImageContext();
// 6.回到主線程更新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = newImage;
        });
    NSLog(@"柵欄執行完畢了");
    });
    dispatch_async(queue, ^{
        NSLog(@"---------");
    });
    dispatch_async(queue, ^{
        NSLog(@"---------");
    });
    dispatch_async(queue, ^{
        NSLog(@"---------");
    });
}

八、調度組

功能:dispatch_group_async可以實現監聽一組任務是否完成,完成后得到通知執行其他的操作。調度組能實現的柵欄也可實現。

代碼示例:異步線程中,下載兩種圖片,在dispatch_group_notify通知中說明圖片下載完-> 合并圖片,合并圖片結束后 ->再回到主線程中更新UI


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%s", __func__);
    dispatch_queue_t queue = dispatch_queue_create("隊列名", DISPATCH_QUEUE_CONCURRENT);
    __block UIImage *image1 = nil;
    __block UIImage *image2 = nil;
    dispatch_group_t group = dispatch_group_create();
// 1.開啟一個新的線程下載第一張圖片
    dispatch_group_async(group, queue, ^{
        NSURL *url = [NSURL URLWithString:@"http://www.shaimn.com/uploads/allimg/150509/1-150509233453.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];
        image1 = image;
        NSLog(@"圖片1下載完畢");
    });

// 2.開啟一個新的線程下載第二張圖片
    dispatch_group_async(group, queue, ^{
        NSURL *url = [NSURL URLWithString:@"http://m.818today.com/imgsy/image/2016/0215/6359115208730406558041085.jpg"];
        NSData *data = [NSData dataWithContentsOfURL:url];
        UIImage *image = [UIImage imageWithData:data];
        image2 = image;
        NSLog(@"圖片2下載完畢");
    });

// 3.開啟一個新的線程, 合成圖片
// 只要將隊列放到group中, 隊列中的任務執行完畢, group就會發出一個通知
    dispatch_group_notify(group, queue, ^{
        NSLog(@"%@ %@", image1, image2);

// 1.開啟圖片上下文
        UIGraphicsBeginImageContext(CGSizeMake(200, 200));

// 2.將第一張圖片畫上去
        [image1 drawInRect:CGRectMake(0, 0, 100, 200)];

// 3.將第二張圖片畫上去
        [image2 drawInRect:CGRectMake(100, 0, 100, 200)];

// 4.從上下文中獲取繪制好的圖片
        UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();

// 5.關閉上下文
        UIGraphicsEndImageContext();

// 4.回到主線程更新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = newImage;
         });
    });
}

調度組的實現原理:


#pragma mark - dispatch_group_async 的實現原理

//在終端輸入命令:man dispatch_group_async可得到如下信息

/*

The dispatch_group_async() convenience function behaves like so:

void

dispatch_group_async(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block)

{

dispatch_retain(group);

dispatch_group_enter(group);

dispatch_async(queue, ^{

block();

dispatch_group_leave(group);

dispatch_release(group);

});

}

*/

// MARK: - 仿照上述信息來實現group原理

- (void)group2 {

// 1. 調度組
    dispatch_group_t group = dispatch_group_create();

// 2. 隊列
    dispatch_queue_t q = dispatch_get_global_queue(0, 0);

// 3. 將任務添加到隊列
// 進入群組
    dispatch_group_enter(group);
    dispatch_async(q, ^{
        NSLog(@"任務 1 %@", [NSThread currentThread]);
// 離開群組
        dispatch_group_leave(group);
});

// 進入群組
    dispatch_group_enter(group);
    dispatch_async(q, ^{
        NSLog(@"任務 2 %@", [NSThread currentThread]);
// 離開群組
        dispatch_group_leave(group);
    });

// 進入群組
    dispatch_group_enter(group);
    dispatch_async(q, ^{
        NSLog(@"任務 3 %@", [NSThread currentThread]);
// 離開群組
        dispatch_group_leave(group);
    });

// 4. 監聽所有任務完成
//    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//        NSLog(@"OVER %@", [NSThread currentThread]);
//    });

// dispatch_group_wait 是同步的,DISPATCH_TIME_FOREVER參數表示一直等待到前面執行完畢

    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

// 5. 判斷異步
    NSLog(@"come here");
}

九、快速迭代(類似for循環)

  • 功能:使用dispatch_apply函數能進行快速迭代遍歷,但是它和for循環又有些不同,for是按順序循環,而_apply函數是隨機調度

for (int index = 0;index < 100;index ++){

    //在當前線程執行100次代碼, index是從0到99

}

dispatch_apply(100, 隊列queue, ^(size_t index){

    //開啟多個線程來執行100次代碼,index順序不確定
  //相比于for循環執行的效率高

});

/*

第一個參數: 設置要執行幾次

第二個參數: 決定第三個參數的block在哪個線程中執行

第三個參數: 調用的代碼,其中參數i

*/

十、信號量(dispatch_semaphore)

功能:使用dispatch_semaphore_signal使信號量資源+1,設置dispatch_semaphore_wait等待來判斷信號量是否為0,不為0時使信號量資源-1并返回,為0時繼續執行,從而達到線程同步的目的和同步鎖一樣能夠解決資源搶占的問題

//通過對象方法直接返回數組(將從網絡獲取數據的過程要在子線程中)
- (__kindof NSArray  *)getData{

        //參數0表示一開始創建的信號量內部是沒有資源的
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

        //創建用來存放數據的數組
    NSMutableArray * array = [NSMutableArray arrayWithCapacity:0];
        //用全局隊列來加載數據
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        for (int i=0; i<10; i++) {
            //模擬加載延時
            [NSThread sleepForTimeInterval:1.0];
            [array addObject:[NSNumber numberWithInt:i]];
        }
            //發送信號,使信號量資源數+1
        dispatch_semaphore_signal(semaphore);

    });
        //信號等待,判斷信號量資源數,不為0時就阻塞當前線程,并使資源數-1,循環執行
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);


    return array;
}

調度組也可以實現上述功能

- (__kindof NSArray  *)getData{

    NSMutableArray * array = [NSMutableArray arrayWithCapacity:0];

    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
        [NSThread sleepForTimeInterval:1.0];
        for (int i=0; i<10; i++) {
            [array addObject:[NSNumber numberWithInt:i]];
        }
    });
    //阻塞當前線程,直到group的內容全部執行完成
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    return array;
}

GCD和NSOperation的一些區別

  • GCD是底層的C語言構成的API,而NSOperationQueue及相關對象是Objc的對象。在GCD中,在隊列中執行的是由block構成的任務,這是一個輕量級的數據結構;而Operation作為一個對象,為我們提供了更多的選擇;
  • 在NSOperationQueue中,我們可以隨時取消已經設定要準備執行的任務(當然,已經開始的任務就無法阻止了),而GCD沒法停止已經加入queue的block(其實是有的,但需要許多復雜的代碼);
  • NSOperation能夠方便地設置依賴關系,我們可以讓一個Operation依賴于另一個Operation,這樣的話盡管兩個Operation處于同一個并行隊列中,但前者會直到后者執行完畢后再執行;
  • 我們能將KVO應用在NSOperation中,可以監聽一個Operation是否完成或取消,這樣子能比GCD更加有效地掌控我們執行的后臺任務;
  • 在NSOperation中,我們能夠設置NSOperation的priority優先級,能夠使同一個并行隊列中的任務區分先后地執行,而在GCD中,我們只能區分不同任務隊列的優先級,如果要區分block任務的優先級,也需要大量的復雜代碼;
    我們能夠對NSOperation進行繼承,在這之上添加成員變量與成員方法,提高整個代碼的復用度,這比簡單地將block任務排入執行隊列更有自由度,能夠在其之上添加更多自定制的功能。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容