在 iOS 平臺實現新的非同步解決方案 async/await

知識小集發表於2019-03-04

本文是 RJIterator 作者 @rkuiRJIterator 實現的詳細總結。分析了 ES7async/await 的實現原理,並依此詳細說明了在 iOS 中實現 async/await 的思路。具體的實現細節有很多值得借鑑的地方,歡迎大家一起討論,也一起來完善這個優秀的作品。

知識小集是一個團隊公眾號,每週都會有原創文章分享,我們的文章都會在公眾號首發。歡迎關注檢視更多內容。

在 iOS 平臺實現新的非同步解決方案 async/await

async/awaitES7 提出的非同步解決方案。對比回撥鏈和 Promise.then 鏈的非同步程式設計模式,基於 async/await 我們可以以同步風格編寫非同步程式碼,程式邏輯清晰明瞭。

如順序讀取三個檔案:

function readFile(name) {
  return new Promise((resolve, reject) => {
    //非同步讀取檔案
    fs.readFile(name, (err, data) => {
        if (err) reject(err);
        else resolve(data);
    });
  });
}

async function read3Files() {
  try {
      //讀取第1個檔案
      let data1 = await readFile(`file1.txt`);
      //讀取第2個檔案
      let data2 = await readFile(`file2.txt`);
      //讀取第3個檔案
      let data3 = await readFile(`file3x.txt`);
      //3個檔案讀取完畢
    } catch (error) {
       //讀取出錯
    }
}
複製程式碼

讀取檔案本身是非同步操作,而在要求順序讀取的前提下,基於 callback 實現將造成很深的回撥巢狀:

function readFile(name, callback) {
  //非同步讀取檔案
  fs.readFile(name, (err, data) => {
     callback(err, data);
  });
}

function read3Files() {
  //讀取第1個檔案
  readFile(`file1.txt`, (err, data) => {
    //讀取第2個檔案
    readFile(`file2.txt`, (err, data) => {
      //讀取第3個檔案
      readFile(`file3.txt`, (err, data) => {
        //3個檔案讀取完畢
      });
    });
  });
}
複製程式碼

基於 Promise.then 鏈需要將邏輯分散在過多的程式碼塊:

function readFile(name) {
  return new Promise((resolve, reject) => {
    //非同步讀取檔案
    fs.readFile(name, (err, data) => {
       if (err) reject(err);
       else resolve(data);
    });
  });
}

function read3Files() {
  //讀取第1個檔案
  readFile(`file1.txt`)
  .then(data => {
    //讀取第2個檔案
    return readFile(`file2.txt`);
  })
  .then(data => {
    //讀取第3個檔案
    return readFile(`file3.txt`);
  })
  .then(data => {
    //3個檔案讀取完畢
  })
  .catch(error => {
    //讀取出錯
  });
}
複製程式碼

對比可見,aync/await 模式的優雅與簡潔。接觸完畢後,深感如果在 iOS 專案中也能像 JS 這般編寫非同步程式碼也是極好。經過研究發現要在 iOS 平臺實現這些特性其實並不是很困難,因此本文主旨便是描述 async/awaitiOS 平臺的一次實現過程,並給出了一個成果專案。

暫時繼續討論JavaScript

生成器與迭代器

要明白 async/await 的機制及運用,需從生成器與迭代器逐步說起。在 ES6 中,生成器是一個函式,和普通函式的有以下幾個區別:

  • 生成器函式 function 關鍵字後多了個 *:
function *numbers() {}
複製程式碼
  • 生成器函式內可以 yield 語法多次返回值
function *numbers() {
    yield 1;
    yield 2;
    yield 3;
}
複製程式碼
  • 直接呼叫生成器函式得到的是一個迭代器,通過迭代器的 next 方法控制生成器的執行:
let iterator = numbers();
let result = iterator.next();
複製程式碼

每一次 next 呼叫將得到結果 resultresult 物件包含兩個屬性:valuedonevalue 表示此次迭代得到的結果值,done 表示是否迭代結束。比如:

function *numbers() {
    yield 1;
    yield 2;
    yield 3;
}

let iterator = numbers();
//第1次迭代
let result = iterator.next();
console.log(result);
//輸出 => { value: 1, done: false }

//第2次迭代
result = iterator.next();
console.log(result);
//輸出 => { value: 2, done: false }

//第3次迭代
result = iterator.next();
console.log(result);
//輸出 => { value: 3, done: false }

//第4次迭代
result = iterator.next();
console.log(result);
//輸出 => { value: undefined, done: true }
複製程式碼

第 1 次呼叫 next,生成器 numbers 開始執行。執行到第一個 yield 語句時,numbers 將中斷,並將結果值 1 返回給迭代器。由於 numbers 並沒有執行完,所以 donefalse

第 2 次呼叫 next,生成器 numbers 從上次中斷的位置恢復執行,繼續執行到下一個 yield 語句時,numbers 再次中斷,並將結果值 2 返回給迭代器,由於 numbers 並沒有執行完,所以 donefalse

第 3 次呼叫 next,生成器 numbers 從上次中斷的位置恢復執行,繼續執行到下一個 yield 語句時,numbers 再次中斷,並將結果值 3 返回給迭代器,由於 numbers 並沒有執行完,所以 donefalse

第 4 次呼叫 next,生成器 numbers 從上次中斷的位置恢復執行,此時已是函式尾,numbers 將直接 return,由於 numbers 已經執行完成,所以 donetrue。由於 numbers 並沒有顯式地返回任何值,因此此次迭代 valueundefined.

到此迭代結束,此後通過此迭代器的 next 方法,都將得到相同的結果 { value: undefined, done: true }

  • 通過迭代器可向生成器內部傳值
function *hello() {
  let age =  yield `want age`;
  let name = yield `want name`;
  console.log(`Hello, my age: ${age}, name:${name}`);
}

let iterator = hello();
複製程式碼

建立迭代器並開始如下迭代過程:

  • 第 1 次迭代,生成器開始執行,到達第一個 yield 語句時,返回 value = want age, done = false 給迭代器, 並中斷。
let result = iterator.next();
console.log(result);
//輸出 => { value: `want age`, done: false }
複製程式碼
  • 第 2 次迭代,給 next 傳參 28,生成器從上次中斷的地方恢復執行,並將 28 作為甦醒後 yield 的內部返回值賦給 age;然後生成器繼續執行,再次遇到yield,返回value = want name, done = false給迭代器,並中斷。
result = iterator.next(28);
console.log(result);
//輸出 => { value: `want name`, done: false }
複製程式碼
  • 第 3 次迭代,給 next 傳參 `LiLei`,生成器從上次中斷的地方恢復執行,並將 `LiLei` 作為甦醒後 yield 的內部返回值賦給 name;然後生成器繼續執行,列印 log
Hello, my age: 28, name:LiLei
複製程式碼

然後到達函式尾,徹底結束生成器,並返回 value = undefined, done = true 給迭代器。

result = iterator.next(`LiLei`);
console.log(result);
//輸出 => { value: undefined, done: true }
複製程式碼

可見通過迭代器可與生成器“互相交換資料”,生成器通過 yield 返回資料 A 給迭代器並中斷,而通過迭代器又可以把資料 B 傳給生成器並讓 yield 語句甦醒後以 B 作為右值。這個特性是下一步”改進非同步程式設計”的重要基礎。

至此已基本瞭解了生成器與迭代器的語法與運用,總結起來:

  • 生成器是一個函式,直接呼叫得到其對應的迭代器,用以控制生成器的逐步執行;
  • 生成器內部通過 yield 語法向迭代器返回值,而且可以多次返回,並多次恢復執行,有別於傳統函式”返回便消亡”的特點;
  • 可以通過迭代器向生成器內部傳值,傳入的值將作為本次生成器 yield 語句甦醒後的右值;

通過生成器與迭代器改進非同步程式設計

回想本文開頭提到的讀取檔案例子,如果以 callback 模式編寫:

function readFile(name, callback) {
  //非同步讀取檔案
  fs.readFile(name, (err, data) => {
     callback(err, data);
  });
}

function read3Files() {
  //讀取第1個檔案
  readFile(`file1.txt`, (err, data) => {
    //讀取第2個檔案
    readFile(`file2.txt`, (err, data) => {
      //讀取第3個檔案
      readFile(`file3.txt`, (err, data) => {
        //3個檔案讀取完畢
      });
    });
  });
}
複製程式碼

基於前面起到的”通過迭代器與生成器交換資料“的特性,擴充出新思路:

  1. 把讀取檔案這個動作封裝為一個非同步操作,通過 callback 輸出結果 :errdata
  2. read3Files 改變為生成器,內部通過 yield 返回非同步操作給執行器(執行器第3步描述);
  3. 執行器通過迭代器接收 read3Files 返回的非同步操作,拿到非同步操作後,發起該非同步操作,得到結果後再其“交換”給生成器 read3Files 內的 yield

即:

function readFile(name) {
  //返回一個閉包作為非同步操作
  return function(callback) {
    fs.readFile(name, (err, data) => {
       callback(err, data);
    });
  };
}

//執行器
function executor(generator) {
  //建立迭代器
  let iterator = generator();
  //開始第一次迭代
  let result = iterator.next();

  let nextStep = function() {
    //迭代還沒結束
    if (!result.done) {
      //從生成器拿到的是一個非同步操作
      if (typeof result.value === "function") {
        //發起非同步操作
        result.value((err, data) => {
          if(err) {
            //在生成器內部引發異常
            iterator.throw(err);
          }
          else {
            //得到結果值,傳給生成器
            result = iterator.next(data);
            //繼續下一步迭代
            nextStep();
          }
        });
      }
      //從生成器拿到的是一個普通物件
      else {
        //什麼都不做,直接傳回給生成器
        result = iterator.next(result.value);
        //繼續下一步迭代
        nextStep();
      }
    }
  };
  //開始後續迭代
  nextStep();
}

複製程式碼

read3Files 改進為:

executor(function *() {
  try {
    //讀取第1個檔案
    let data1 = yield readFile(`file1.txt`);
    //讀取第2個檔案
    let data2 = yield readFile(`file2x.txt`);
    //讀取第3個檔案
    let data3 = yield readFile(`file3.txt`);
  } catch (e) {
    //讀取出錯
  }
});
複製程式碼

此時已經把 callback 模式改進為同步模式。

暫且把傳給執行器的生成器函式叫做”非同步函式”,執行過程總結起來就是:

非同步函式但凡遇到非同步操作,就通過 yield 交給執行器;執行器但凡拿到非同步操作,就發起該操作,拿到實際結果後再將其交換給非同步函式。那麼在非同步函式內,就可以同步風格編寫非同步程式碼,因為有了執行器在背後運作,非同步函式內的 yield 就具有了“你給我非同步操作,我還你實際結果”的能力。

Promoise 同樣可作為非同步操作:

function readFile(name) {
  //返回一個Promise作為非同步操作
  return new Promise((resolve, reject) => {
    fs.readFile(name, (err, data) => {
        if (err) reject(err);
        else resolve(data);
    });
  });
}
複製程式碼

在執行器中新增識別 Promise 的程式碼:

function executor(generator) {
  //建立迭代器
  let iterator = generator();
  //開始第一次迭代
  let result = iterator.next();

  let nextStep = function() {
    //迭代還沒結束
    if (!result.done) {
      if (typeof result.value === "function") {
        ....
      }
      //從生成器拿到的是一個Promise非同步操作
      else if (result.value instanceof Promise) {
        //執行該Promise
        result.value.then(data => {
          //得到結果值,傳給生成器
          result = iterator.next(data);
          //繼續下一步迭代
          nextStep();
        }).catch(err => {
          //在生成器內部引發異常
          iterator.throw(err);
        });
      }
      else {
        ...
      }
    }
  };
  ...
}
複製程式碼

到此已經成功把非同步程式設計化為同步風格,但或許有個疑問:這個例子倒是化非同步為同步風格了,但是那個執行器 executor 看起來好大一坨,並不優雅。實際上執行器當然是複用的,不用每次都實現執行器。

async/await語法糖

到了 ES7async/await 終於出來。async/await 是上述執行器,生成器模式的語法糖,運用 async/await ,再也不需要每次都定義生成器作為非同步函式,然後顯式傳給執行器,只要簡單在函式定義前增加 async,表示這是一個非同步函式,內部將用 await 來等待非同步結果:

async function foo() {
    let value = await 非同步操作;
    let value = await 非同步操作;
    let value = await 非同步操作;
    let value = await 非同步操作;
}
複製程式碼

如讀取檔案例子:

async function read3Files() {
    //讀取第1個檔案
    let data1 = await readFile(`file1.txt`);
    //讀取第2個檔案
    let data2 = await readFile(`file2.txt`);
    //讀取第3個檔案
    let data3 = await readFile(`file3x.txt`);
    //3個檔案讀取完畢
}
複製程式碼

然後直接呼叫即可:

read3Files();
複製程式碼

async 表示該函式內部包含非同步操作,需要把它交給內建執行器;

await 表示等待非同步操作的實際結果。

至此,JSasync/await 的來龍去脈已基本描述完畢。

回到iOS

光描述 JS 生成器,迭代器,async/await 就花了大量篇幅,因為在 iOS 上將以它們的 JS 特性為目標,最終實現 OC 版的迭代器,生成器,async/await

型別定義

暫時無需在意怎麼實現,既然是以前面描述的特性為目標,則可以根據其特性先做如下定義:

先定義 yield 如下:

id yield(id value);
複製程式碼

yield 接受一個物件 value 作為返回給迭代器的值,同時返回一個迭代器設定的新值或者原本值 value

每次迭代的 Result

@interface Result: NSObject
@property (nonatomic, strong, readonly) id value;
@property (nonatomic, readonly) BOOL done;
@end
複製程式碼

value 表示迭代的結果,為 yield 返回的物件,或者nildone 指示是否迭代結束。

根據前面描述的生成器特性,那麼在 OC 裡,生成器首先應該是一個 C函式/OC方法/block,且內部通過呼叫 yield 來返回結果給迭代器:

void generator() {
    yield(value);
    yield(value);
}

- (void)generator {
    yield(value);
    yield(value);
}

^{
    yield(value);
    yield(value);
}
複製程式碼

實際上不論是 OC 方法,還是 block,底層呼叫時都與呼叫 C 函式無異。

只是呼叫 block 會預設以 block 結構體地址作為第一個隱含引數;呼叫方法會以物件自身 self,和選擇器_cmd 作為前兩個隱含引數

所以只要實現了 C 函式版生成器,其實現機制將也無縫適用於 OC 方法,block

迭代器定義:

@interface Iterator : NSObject
{
    void (*_func)(void);
}

- (id)initWithFunc:(void (*)(void))func;
- (Result *)next;
- (Result *)next:(id)value;
@end
複製程式碼

迭代器的建立無法做到像 JS 一樣直接呼叫生成器即可建立,需要顯式建立:

void generator() {
    yield(value);
    yield(value);
}

Iterator *iterator = [[Iterator alloc] initWithFunc: generator];
複製程式碼

然後就可以像 JS 一樣呼叫 next 來進行迭代:

Result *result = iterator.next;
//迭代並傳值
Result *result = [iterator next: value];
複製程式碼

實現生成器與迭代器

根據需求,yield 呼叫會中斷當前執行流,並期望將來能夠從中斷處繼續恢復執行,那麼必定要在觸發中斷時儲存現場,包括:

  1. 當前指令地址
  2. 當前暫存器資訊,包括當前棧幀棧頂

而且中斷後到恢復的這段時間內,應當確保 yield 以及生成器 generator 的棧幀不會被銷燬。

而恢復執行的過程是儲存現場的逆過程,即恢復相關暫存器,並跳轉到儲存的指令地址處繼續執行。

上述過程描述起來看似簡單,但是如果要自己寫彙編程式碼去儲存與恢復現場,並適配各種平臺,要保證穩定性還是很難的,好在有C標準庫提供的現成利器:setjmp/longjmp

setjmp/longjmp 可以實現跨函式的遠端跳轉,對比 goto 只能實現函式內跳轉,setjmp/longjmp 實現遠端跳轉基於的就是儲存現場與恢復現場的機制,非常符合此處的需求。

實現思路

根據前面對生成器,迭代器的定義及需求推敲整理出如下的實現思路:

  1. 迭代器通過 next 方法與生成器進行互動時,在 next 方法內部會將控制流切換到生成器,生成器通過呼叫 yield 設定傳給迭代器的返回值,並將執行流切換回到 next 方法;
  2. 切回 next 方法後,拿到這個值,正常返回給呼叫者;
  3. 為了確保 next 方法返回後,生成器的執行棧不被銷燬,因此生成器方法的執行需要在一個不被釋放的新棧上進行;
  4. 雖然 next 主要通過恢復現場方式切入生成器,但是首次還是需要通過函式呼叫方式來進入生成器,通過中介 wrapper 呼叫生成器的方式,可以檢測到生成器執行結束的事件,然後 wrapper 再切回 next 方法,並設定 doneYES,迭代結束。

整個流程圖解如下:

在 iOS 平臺實現新的非同步解決方案 async/await

乍一看好大一坨,但是隻要跟著箭頭流程走,思路將很快理清。

根據此思路,為迭代器新增屬性如下:

@interface Iterator : NSObject
{
    int *_ev_leave; //迭代器在next方法內儲存的現場
    int *_ev_entry; //生成器通過yield儲存的現場
    BOOL _ev_entry_valid; //指示生成器現場是否可用
    void *_stack; //為生成器新分配的棧
    int _stack_size; //為生成器新分配的棧大小
    void (*_func)(void);//迭代器函式指標
    BOOL _done; //是否迭代結束
    id _value; //生成器通過yield傳回的值
}
- (id)initWithFunc:(void (*)(void))func;
- (Result *)next;
- (Result *)next:(id)value;
@end
複製程式碼

為生成器分配新棧,正如前面所述,在迭代器和生成器的生命週期中,next 方法的每次迭代是要正常返回的,如果直接在 next 自己的呼叫棧上呼叫 wrapperwrapper 再呼叫生成器,那麼 next 返回後,生成器就算保護了暫存器現場,它的棧幀也被破壞了,再次恢復執行將產生無法預料的結果。

//預設為生成器分配256K的執行棧
#define DEFAULT_STACK_SIZE (256 * 1024)

- (id)init {
    if (self = [super init]) {
      //分配一塊記憶體作為生成器的執行棧
        _stack = malloc(DEFAULT_STACK_SIZE);
        memset(_stack, 0x00, DEFAULT_STACK_SIZE);
        _stack_size = DEFAULT_STACK_SIZE;
        //jmp_buf型別來自C標準庫<setjmp.h>
        _ev_leave = malloc(sizeof(jmp_buf));
        memset(_ev_leave, 0x00, sizeof(jmp_buf));
        _ev_entry = malloc(sizeof(jmp_buf));
        memset(_ev_entry, 0x00, sizeof(jmp_buf));
    }
    return self;
}
複製程式碼

實現 next 方法:

#define JMP_CONTINUE 1//生成器還可被繼續迭代
#define JMP_DONE 2//生成器已經執行結束,迭代器應該結束

- (Result *)next:(id)value {
    if (_done) {
       //迭代器已結束,則每次呼叫next都返回最後一次結果
       return [Result resultWithValue:_value error:_error done:_done];
    }    
    //儲存next當前環境
    int leave_value = setjmp(_ev_leave);
    //非恢復執行
    if (leave_value == 0) {
        //已經設定了生成器進入點
        if (_ev_entry_valid) {
            //設定傳給生成器內yield的新值
            if (value) {
                self.value = value;
            }
            //直接從生成器進入點進入
            longjmp(_ev_entry, JMP_CONTINUE);
        }
        else {
            //生成器還沒儲存過現場,從wrapper進入生成器
            
            //next棧會銷燬,所以為wrapper啟用新棧
            intptr_t sp = (intptr_t)(_stack + _stack_size);
            //預留安全空間,防止直接move [sp] 傳參 以及msgsend向上訪問堆疊
            sp -= 256;
            //對齊sp
            sp &= ~0x07;
            //直接修改棧指標sp,指向新棧
#if defined(__arm__)
            asm volatile("mov sp, %0" : : "r"(sp));
#elif defined(__arm64__)
            asm volatile("mov sp, %0" : : "r"(sp));
#elif defined(__i386__)
            asm volatile("movl %0, %%esp" : : "r"(sp));
#elif defined(__x86_64__)
            asm volatile("movq %0, %%rsp" : : "r"(sp));
#endif
            //在新棧上呼叫wrapper,至此可以認為wrapper,以及生成器函式的執行棧和next無關
            [self wrapper];
        }
    }
    //從生成器內部恢復next
    else if (leave_value == JMP_CONTINUE) {
        //還可以繼續迭代
    }
    //從生成器wrapper恢復next
    else if (leave_value == JMP_DONE) {
        //生成器結束,迭代完成
        _done = YES;
    }
        
    return [RJResult resultWithValue:_value error:_error done:_done];
}
複製程式碼

如果沒有中介wrapper,那麼迭代器返回將會造成崩潰,因為迭代器的執行棧和生成器是分開的,如果生成器內部執行return語句,返回後的棧空間將是未定義的,很有可能造成非法記憶體訪問而崩潰.中介wrapper很好地解決了這個問題:

- (void)wrapper {
   //呼叫生成器函式
    if (_func) {
        _func();
    }
    //從生成器返回,說明生成器完全執行結束
    self.value = nil;
    //恢復next
    longjmp(_ev_leave, JMP_DONE);
    //不會到此
    assert(0);
}
複製程式碼

通過中介 wrapper 呼叫方式進入生成器,生成器最終返回後將正確返回到 wrapper 末尾繼續執行,而 wrapper 也就知道,此時生成器結束了,因此以 longjmp 方式恢復 next 的現場,並設定恢復值為 JMP_DONEnext 被恢復後拿到這個值就知道生成器執行結束,迭代該結束了。

yield 的實現就更加簡單,儲存當前現場,將 value 值傳遞給迭代器物件,然後恢復迭代器next方法即可,而當後續從 next 恢復 yield 的現場後,yield 再取迭代器設定的新值返回給生成器內部,如此達到生成器與迭代器的資料交換:

id yield(id value) {
    //獲取當前執行緒正在獲得的生成器
    Iterator *iterator = [IteratorStack top];
    return [iterator yield: value];
}

- (id)yield:(id)value {
    //設定生成器的現場已保護標誌
    _ev_entry_valid = YES;
    //現場保護
    if (setjmp(_ev_entry) == 0) {
        //現場保護完成
        //給迭代器賦值
        self.value = value;
        //恢復迭代器next現場
        longjmp(_ev_leave, JMP_CONTINUE);
    }
    //從迭代器next恢復此現場
    //返回迭代器傳進來的新值,或者預設值value
    return self.value;
}
複製程式碼

這裡的 IteratorStack 是一個執行緒本地儲存的棧,棧頂永遠是當前執行緒正在活動的迭代器,具體實現可以參考後邊給出的結果專案。

至此已經實現了 c 函式版本的生成器,簡單改變即可擴充套件到 OC 方法,block。首先是迭代器需要支援新的初始化方法:

@interface Iterator : NSObject
{
    int *_ev_leave; //迭代器在next方法內儲存的現場
    int *_ev_entry; //生成器通過yield儲存的現場
    BOOL _ev_entry_valid; //指示生成器現場是否可用
    void *_stack; //為生成器新分配的棧
    int _stack_size; //為生成器新分配的棧大小
    void (*_func)(void);//迭代器函式指標
    BOOL _done; //是否迭代結束
    id _value; //生成器通過yield傳回的值
    id _target;//生成器方法所在物件
    SEL _selector;//生成器方法selector
    id _block;//生成器block
    NSMutableArray *_args;//傳遞給生成器的初始引數
}
- (id)initWithFunc:(void (*)(void))func;
- (id)initWithTarget:(id)target selector:(SEL)selector;
- (id)initWithBlock:(id)block;
- (Result *)next;
- (Result *)next:(id)value;
@end
複製程式碼

wrapper 支援新的生成器呼叫方式:

- (void)wrapper {
    if (_func) {
        _func();
    }
    else if (_target && _selector) {
       ((void (*)(id, SEL))objc_msgSend)(_target, _selector);
    }
    else if (_block) {
        ((void (^)(void))_block)();
    }
    //從生成器返回,說明生成器完全執行結束
    self.value = nil;
    //恢復next
    longjmp(_ev_leave, JMP_DONE);
    //不會到此
    assert(0);
}
複製程式碼

通過生成器與迭代器改進非同步程式設計

正如前面描述的 JS 下的改進方法,現在可以用實現的生成器與迭代器來改進 iOS 的非同步程式設計,且思路一模一樣。

首先定義非同步操作為如下閉包:

typedef void (^AsyncCallback)(id  value, id  error);
typedef void (^AsyncClosure)(AsyncCallback  callback);
複製程式碼

JS 下的定義一樣,這種閉包內部可進行任何非同步呼叫,最終以 callback 輸出 errorvalue 即可。

同時 PromiseKit 提供的 AnyPromise 也可以作為非同步操作。

iOS 版本 readFile

- (AsyncClosure)readFileWithPath:(NSString *)path {
    return  ^(void (^resultCallback)(id value, id error)) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSData *data =  [NSData dataWithContentsOfFile:path];
            resultCallback(data, [NSError new]);
        });
    };
}


- (AnyPromise *)readFileWithPath:(NSString *)path {
    return [AnyPromise promiseWithAdapterBlock:^(PMKAdapter  _Nonnull adapter) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSData *data =  [NSData dataWithContentsOfFile:path];
            adapter(data, [NSError new]);
        });
    }];
}

複製程式碼

執行器 executor

@protocol LikePromise <NSObject>
- (id<LikePromise> __nonnull (^ __nonnull)(id __nonnull))then;
- (id<LikePromise>  __nonnull(^ __nonnull)(id __nonnull))catch;
@end

void executor(dispatch_block_t block) {
    Iterator *  iterator = [[Iterator alloc] initWithBlock:block];
    Result * __block result = nil;
    
    dispatch_block_t __block step;
    step = ^{
        if (!result.done) {
            id value = result.value;
            //oc閉包
            if ([value isKindOfClass:NSClassFromString(@"__NSGlobalBlock__")] ||
                [value isKindOfClass:NSClassFromString(@"__NSStackBlock__")] ||
                [value isKindOfClass:NSClassFromString(@"__NSMallocBlock__")]
                ) {
                ((AsyncClosure)value)(^(id value, id error) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [result release];
                        //將此次非同步操作的結果包裝成Result,傳給生成器
                        result = [iterator next: [Result resultWithValue:value error:error done:NO]].retain;
                        step();
                    });
                });
            }
            //AnyPromise
            else if (NSClassFromString(@"AnyPromise") &&
                     [value isKindOfClass:NSClassFromString(@"AnyPromise")] &&
                     [value respondsToSelector:@selector(then)] &&
                     [value respondsToSelector:@selector(catch)]
                     ) {
                id <LikePromise> promise = (id <LikePromise>)value;
                void (^__block then_block)(id) = NULL;
                void (^__block catch_block)(id) = NULL;
                
                then_block = Block_copy(^(id value){
                    if (then_block) { Block_release(then_block); then_block = NULL; }
                    if (catch_block) { Block_release(catch_block); catch_block = NULL; }
                    
                    [result release];
                    result = [iterator next: [Result resultWithValue:value error:nil done:NO]].retain;
                    step();
                });
                
                catch_block = Block_copy(^(id error){
                    if (then_block) { Block_release(then_block); then_block = NULL; }
                    if (catch_block) { Block_release(catch_block); catch_block = NULL; }
                    
                    [result release];
                    result = [iterator next: [Result resultWithValue:nil error:error done:NO]].retain;
                    step();
                });
                
                promise.then(then_block).catch(catch_block);
            }
            //普通物件
            else {
                Result *old_result = result;
                result = [iterator next: old_result].retain;
                [old_result release];
                
                step();
            }
        }
        else {
            //執行過程結束
            Block_release(step);
            [result release];
            [iterator release];
        }
    };
    
    step =  Block_copy(step);
    
    dispatch_async(dispatch_get_main_queue(), ^{
        result = iterator.next.retain;
        step();
    });
}
複製程式碼

有了執行器 executor,那麼順去讀取檔案的例子在 iOS 下可如下實現:

executor(^{
    Result *result1 = yield( [self readFileWithPath:@".../path1"] );
    if (result1.error) {/*第1步出錯*/}
    NSData *data1 = result1.value;
    
    Result *result2 = yield( [self readFileWithPath:@".../path2"] );
    f (result1.error) {/*第2步出錯*/}
    NSData *data2 = result2.value;
    
    Result *result3 = yield( [self readFileWithPath:@".../path3"] );
    f (result1.error) {/*第3步出錯*/}
    NSData *data3 = result3.value;
});
複製程式碼

更好聽的名字: async/await

將上一步實現的的執行器 executor 改名為 async,新增 await 函式:

RJResult * await(id value);

RJResult * await(id value) {
    return (Result *)yield(value);
}
複製程式碼

其實 await 本質上就是 yield。那麼讀取檔案的例子就寫成:

async(^{
    Result *result1 = await( [self readFileWithPath:@".../path1"] );
    if (result1.error) {/*第1步出錯*/}
    NSData *data1 = result1.value;
    
    Result *result2 = await( [self readFileWithPath:@".../path2"] );
    f (result1.error) {/*第2步出錯*/}
    NSData *data2 = result2.value;
    
    Result *result3 = await( [self readFileWithPath:@".../path3"] );
    f (result1.error) {/*第3步出錯*/}
    NSData *data3 = result3.value;
});
複製程式碼

總結

至此便在 iOS 平臺實現了 async/await,且通過async/await 可以化非同步程式設計為同步風格。單靠短短字面描述無法面面俱到,比如 setjmp/longjmp 的原理及使用,函式呼叫過程與棧的聯絡,如有生疏要額外研究。本文旨在描述在 iOS 平臺上的一次對 async/await 的實現歷程,可以通過下面的專案檢視完整實現程式碼。

成果專案

RJIterator https://github.com/renjinkui2719/RJIterator 是我根據本文描述的思路實現的迭代器、生成器、yieldasync/await 的完整專案,歡迎交流與探討。

歡迎加入知識小集討論群

另外,我們開了微信群,方便大家溝通交流技術。目前知識小集1號群已滿,新開了知識小集2號群,有興趣的可以加入。沒有iOS限制,移動開發的同學都歡迎。由於微信群掃碼加群有100的限制,所以如果掃碼無法加入的話,可以先加微訊號 coldlight_hh 或者 wsy9871,再拉進群哈。

相關文章