‘iOS開發(fā)’幫你全解iOS通知機(jī)制(輕松過面)

簡(jiǎn)述

本文主要是針對(duì)iOS通知機(jī)制的全面解析,從接口到原理面面俱到。同時(shí)也解決了之前寫的文章阿里、字節(jié):一套高效的iOS面試題中關(guān)于通知的問題,相信看完此文再也不怕面試官問我任何通知相關(guān)問題了

由于蘋果沒有對(duì)相關(guān)源碼開放,所以以GNUStep源碼為基礎(chǔ)進(jìn)行研究,GNUStep雖然不是蘋果官方的源碼,但很具有參考意義,根據(jù)實(shí)現(xiàn)原理來猜測(cè)和實(shí)踐,更重要的還可以學(xué)習(xí)觀察者模式的架構(gòu)設(shè)計(jì)

問題列表

先把之前的問題列出來,詳細(xì)讀完本文之后,你會(huì)找到答案

  1. 實(shí)現(xiàn)原理(結(jié)構(gòu)設(shè)計(jì)、通知如何存儲(chǔ)的、name&observer&SEL之間的關(guān)系等)
  2. 通知的發(fā)送時(shí)同步的,還是異步的
  3. NSNotificationCenter接受消息和發(fā)送消息是在一個(gè)線程里嗎?如何異步發(fā)送消息
  4. NSNotificationQueue是異步還是同步發(fā)送?在哪個(gè)線程響應(yīng)
  5. NSNotificationQueuerunloop的關(guān)系
  6. 如何保證通知接收的線程在主線程
  7. 頁面銷毀時(shí)不移除通知會(huì)崩潰嗎
  8. 多次添加同一個(gè)通知會(huì)是什么結(jié)果?多次移除通知呢
  9. 下面的方式能接收到通知嗎?為什么
// 發(fā)送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];
復(fù)制代碼

關(guān)鍵類結(jié)構(gòu)

NSNotification

用于描述通知的類,一個(gè)NSNotification對(duì)象就包含了一條通知的信息,所以當(dāng)創(chuàng)建一個(gè)通知時(shí)通常包含如下屬性:

@interface NSNotification : NSObject <NSCopying, NSCoding>
...
/* Querying a Notification Object */

- (NSString*) name; // 通知的name
- (id) object; // 攜帶的對(duì)象
- (NSDictionary*) userInfo; // 配置信息

@end
復(fù)制代碼

作為一個(gè)開發(fā)者,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,這是一個(gè)我的iOS交流群:761407670 進(jìn)群密碼000,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經(jīng)驗(yàn),討論技術(shù), 大家一起交流學(xué)習(xí)成長!

提供逆向安防、Swift、算法、架構(gòu)設(shè)計(jì)、多線程,網(wǎng)絡(luò)進(jìn)階,還有底層、音視頻、Flutter等資料

image.png

一般用于發(fā)送通知時(shí)使用,常用api如下:

- (void)postNotification:(NSNotification *)notification;
復(fù)制代碼

NSNotificationCenter

這是個(gè)單例類,負(fù)責(zé)管理通知的創(chuàng)建和發(fā)送,屬于最核心的類了。而NSNotificationCenter類主要負(fù)責(zé)三件事

  1. 添加通知
  2. 發(fā)送通知
  3. 移除通知

核心API如下:

// 添加通知
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
// 發(fā)送通知
- (void)postNotification:(NSNotification *)notification;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject;
- (void)postNotificationName:(NSNotificationName)aName object:(nullable id)anObject userInfo:(nullable NSDictionary *)aUserInfo;
// 刪除通知
- (void)removeObserver:(id)observer;

復(fù)制代碼

NSNotificationQueue

功能介紹

通知隊(duì)列,用于異步發(fā)送消息,這個(gè)異步并不是開啟線程,而是把通知存到雙向鏈表實(shí)現(xiàn)的隊(duì)列里面,等待某個(gè)時(shí)機(jī)觸發(fā)時(shí)調(diào)用NSNotificationCenter的發(fā)送接口進(jìn)行發(fā)送通知,這么看NSNotificationQueue最終還是調(diào)用NSNotificationCenter進(jìn)行消息的分發(fā)

另外NSNotificationQueue是依賴runloop的,所以如果線程的runloop未開啟則無效,至于為什么依賴runloop下面會(huì)解釋

NSNotificationQueue主要做了兩件事:

  1. 添加通知到隊(duì)列
  2. 刪除通知

核心API如下:

// 把通知添加到隊(duì)列中,NSPostingStyle是個(gè)枚舉,下面會(huì)介紹
- (void)enqueueNotification:(NSNotification *)notification postingStyle:(NSPostingStyle)postingStyle;
// 刪除通知,把滿足合并條件的通知從隊(duì)列中刪除
- (void)dequeueNotificationsMatching:(NSNotification *)notification coalesceMask:(NSUInteger)coalesceMask;

復(fù)制代碼

隊(duì)列的合并策略和發(fā)送時(shí)機(jī)

把通知添加到隊(duì)列等待發(fā)送,同時(shí)提供了一些附加條件供開發(fā)者選擇,如:什么時(shí)候發(fā)送通知、如何合并通知等,系統(tǒng)給了如下定義

// 表示通知的發(fā)送時(shí)機(jī)
typedef NS_ENUM(NSUInteger, NSPostingStyle) {
    NSPostWhenIdle = 1, // runloop空閑時(shí)發(fā)送通知
    NSPostASAP = 2, // 盡快發(fā)送,這種情況稍微復(fù)雜,這種時(shí)機(jī)是穿插在每次事件完成期間來做的
    NSPostNow = 3 // 立刻發(fā)送或者合并通知完成之后發(fā)送
};
// 通知合并的策略,有些時(shí)候同名通知只想存在一個(gè),這時(shí)候就可以用到它了
typedef NS_OPTIONS(NSUInteger, NSNotificationCoalescing) {
    NSNotificationNoCoalescing = 0, // 默認(rèn)不合并
    NSNotificationCoalescingOnName = 1, // 只要name相同,就認(rèn)為是相同通知
    NSNotificationCoalescingOnSender = 2  // object相同
};
復(fù)制代碼

GSNotificationObserver

這個(gè)類是GNUStep源碼中定義的,它的作用是代理觀察者,主要用來實(shí)現(xiàn)接口:addObserverForName:object: queue: usingBlock:時(shí)用到,即要實(shí)現(xiàn)在指定隊(duì)列回調(diào)block,那么GSNotificationObserver對(duì)象保存了queueblock信息,并且作為觀察者注冊(cè)到通知中心,等到接收通知時(shí)觸發(fā)了響應(yīng)方法,并在響應(yīng)方法中把block拋到指定queue中執(zhí)行,定義如下:

@implementation GSNotificationObserver
{
    NSOperationQueue *_queue; // 保存?zhèn)魅氲年?duì)列
    GSNotificationBlock _block; // 保存?zhèn)魅氲腷lock
}
- (id) initWithQueue: (NSOperationQueue *)queue 
               block: (GSNotificationBlock)block
{
......初始化操作
}

- (void) dealloc
{
....
}
// 響應(yīng)接收通知的方法,并在指定隊(duì)列中執(zhí)行block
- (void) didReceiveNotification: (NSNotification *)notif
{
    if (_queue != nil)
    {
        GSNotificationBlockOperation *op = [[GSNotificationBlockOperation alloc] 
            initWithNotification: notif block: _block];

        [_queue addOperation: op];
    }
    else
    {
        CALL_BLOCK(_block, notif);
    }
}

@end
復(fù)制代碼

存儲(chǔ)容器

上面介紹了一些類的功能,但是要想實(shí)現(xiàn)通知中心的邏輯必須設(shè)計(jì)一套合理的存儲(chǔ)結(jié)構(gòu),對(duì)于通知的存儲(chǔ)基本上圍繞下面幾個(gè)結(jié)構(gòu)體來做(大致了解下,后面章節(jié)會(huì)用到),后面會(huì)詳細(xì)介紹具體邏輯的

// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
  Observation       *wildcard;  /* 鏈表結(jié)構(gòu),保存既沒有name也沒有object的通知 */
  GSIMapTable       nameless;   /* 存儲(chǔ)沒有name但是有object的通知 */
  GSIMapTable       named;      /* 存儲(chǔ)帶有name的通知,不管有沒有object  */
    ...
} NCTable;

// Observation 存儲(chǔ)觀察者和響應(yīng)結(jié)構(gòu)體,基本的存儲(chǔ)單元
typedef struct  Obs {
  id        observer;   /* 觀察者,接收通知的對(duì)象  */
  SEL       selector;   /* 響應(yīng)方法     */
  struct Obs    *next;      /* Next item in linked list.    */
  ...
} Observation;

復(fù)制代碼

注冊(cè)通知

正式開始“注冊(cè)通知”的深入研究,注冊(cè)通知有幾個(gè)常用方法,但只需要研究典型的一兩個(gè)就夠了,原理都是一樣的

目前只介紹NSNotificationCenter的注冊(cè)流程,NSNotificationQueue的方式在下面章節(jié)單獨(dú)拎出來解釋

接口1

直接看源碼(精簡(jiǎn)版便于理解)

/*
observer:觀察者,即通知的接收者
selector:接收到通知時(shí)的響應(yīng)方法
name: 通知name
object:攜帶對(duì)象
*/
- (void) addObserver: (id)observer
            selector: (SEL)selector
                name: (NSString*)name 
                object: (id)object {
  // 前置條件判斷
  ......

  // 創(chuàng)建一個(gè)observation對(duì)象,持有觀察者和SEL,下面進(jìn)行的所有邏輯就是為了存儲(chǔ)它
  o = obsNew(TABLE, selector, observer);

/*======= case1: 如果name存在 =======*/
  if (name) {
    //-------- NAMED是個(gè)宏,表示名為named字典。以name為key,從named表中獲取對(duì)應(yīng)的mapTable
      n = GSIMapNodeForKey(NAMED, (GSIMapKey)(id)name);
      if (n == 0) { // 不存在,則創(chuàng)建 
          m = mapNew(TABLE); // 先取緩存,如果緩存沒有則新建一個(gè)map
          GSIMapAddPair(NAMED, (GSIMapKey)(id)name, (GSIMapVal)(void*)m);
          ...
      }
      else { // 存在則把值取出來 賦值給m
          m = (GSIMapTable)n->value.ptr;
      }
    //-------- 以object為key,從字典m中取出對(duì)應(yīng)的value,其實(shí)value被MapNode的結(jié)構(gòu)包裝了一層,這里不追究細(xì)節(jié)
      n = GSIMapNodeForSimpleKey(m, (GSIMapKey)object);
      if (n == 0) {// 不存在,則創(chuàng)建 
          o->next = ENDOBS;
          GSIMapAddPair(m, (GSIMapKey)object, (GSIMapVal)o);
      }
      else {
          list = (Observation*)n->value.ptr;
          o->next = list->next;
          list->next = o;
      }
    }
/*======= case2:如果name為空,但object不為空 =======*/
  else if (object) {
    // 以object為key,從nameless字典中取出對(duì)應(yīng)的value,value是個(gè)鏈表結(jié)構(gòu)
      n = GSIMapNodeForSimpleKey(NAMELESS, (GSIMapKey)object);
      // 不存在則新建鏈表,并存到map中
      if (n == 0) { 
          o->next = ENDOBS;
          GSIMapAddPair(NAMELESS, (GSIMapKey)object, (GSIMapVal)o);
      }
      else { // 存在 則把值接到鏈表的節(jié)點(diǎn)上
        ...
      }
    }
/*======= case3:name 和 object 都為空 則存儲(chǔ)到wildcard鏈表中 =======*/
  else {
      o->next = WILDCARD;
      WILDCARD = o;
  }
}
復(fù)制代碼

邏輯說明

從上面介紹的存儲(chǔ)容器中我們了解到NCTable結(jié)構(gòu)體中核心的三個(gè)變量以及功能:wildcard、named、nameless,在源碼中直接用宏定義表示了:WILDCARD、NAMELESS、NAMED,下面邏輯會(huì)用到

建議如果看文字說明覺得復(fù)雜不好理解,就看看下節(jié)介紹的存儲(chǔ)關(guān)系圖

case1: 存在name(無論object是否存在)

  1. 注冊(cè)通知,如果通知的name存在,則以name為key從named字典中取出值n(這個(gè)n其實(shí)被MapNode包裝了一層,便于理解這里直接認(rèn)為沒有包裝),這個(gè)n還是個(gè)字典,各種判空新建邏輯不討論
  2. 然后以object為key,從字典n中取出對(duì)應(yīng)的值,這個(gè)值就是Observation類型(后面簡(jiǎn)稱obs)的鏈表,然后把剛開始創(chuàng)建的obs對(duì)象o存儲(chǔ)進(jìn)去

數(shù)據(jù)結(jié)構(gòu)關(guān)系圖

這里就回答了上述問題列表的問題1的一部分,現(xiàn)在梳理下存儲(chǔ)關(guān)系

如果注冊(cè)通知時(shí)傳入name,那么會(huì)是一個(gè)雙層的存儲(chǔ)結(jié)構(gòu)

  1. 找到NCTable中的named表,這個(gè)表存儲(chǔ)了還有name的通知
  2. name作為key,找到value,這個(gè)value依然是一個(gè)map
  3. map的結(jié)構(gòu)是以object作為key,obs對(duì)象為value,這個(gè)obs對(duì)象的結(jié)構(gòu)上面已經(jīng)解釋,主要存儲(chǔ)了observer & SEL

case2: 只存在object

  1. object為key,從nameless字典中取出value,此value是個(gè)obs類型的鏈表
  2. 把創(chuàng)建的obs類型的對(duì)象o存儲(chǔ)到鏈表中

數(shù)據(jù)結(jié)構(gòu)關(guān)系圖

只存在object時(shí)存儲(chǔ)只有一層,那就是objectobs對(duì)象之間的映射

case3: 沒有name和object

這種情況直接把obs對(duì)象存放在了Observation *wildcard 鏈表結(jié)構(gòu)中

接口2

源碼

接口功能: 此接口實(shí)現(xiàn)的功能是在接收到通知時(shí),在指定隊(duì)列queue執(zhí)行block

// 這個(gè)api使用頻率較低,怎么實(shí)現(xiàn)在指定隊(duì)列回調(diào)block的,值得研究
- (id) addObserverForName: (NSString *)name 
                   object: (id)object 
                    queue: (NSOperationQueue *)queue 
               usingBlock: (GSNotificationBlock)block
{
    // 創(chuàng)建一個(gè)臨時(shí)觀察者
    GSNotificationObserver *observer = 
        [[GSNotificationObserver alloc] initWithQueue: queue block: block];
    // 調(diào)用了接口1的注冊(cè)方法
    [self addObserver: observer 
             selector: @selector(didReceiveNotification:) 
                 name: name 
               object: object];

    return observer;
}
復(fù)制代碼

邏輯說明

這個(gè)接口依賴于接口1,只是多了一層代理觀察者GSNotificationObserver,在關(guān)鍵類結(jié)構(gòu)中已經(jīng)介紹了它,設(shè)計(jì)思路值得學(xué)習(xí)

  1. 創(chuàng)建一個(gè)GSNotificationObserver類型的對(duì)象observer,并把queueblock保存下來
  2. 調(diào)用接口1進(jìn)行通知的注冊(cè)
  3. 接收到通知時(shí)會(huì)響應(yīng)observerdidReceiveNotification:方法,然后在didReceiveNotification:中把block拋給指定的queue去執(zhí)行

小結(jié)

  1. 從上述介紹可以總結(jié),存儲(chǔ)是以nameobject為維度的,即判定是不是同一個(gè)通知要從nameobject區(qū)分,如果他們都相同則認(rèn)為是同一個(gè)通知,后面包括查找邏輯、刪除邏輯都是以這兩個(gè)為維度的,問題列表中的第九題也迎刃而解了
  2. 理解數(shù)據(jù)結(jié)構(gòu)的設(shè)計(jì)是整個(gè)通知機(jī)制的核心,其他功能只是在此基礎(chǔ)上擴(kuò)展了一些邏輯
  3. 存儲(chǔ)過程并沒有做去重操作,這也解釋了為什么同一個(gè)通知注冊(cè)多次則響應(yīng)多次

發(fā)送通知

源碼

發(fā)送通知的核心邏輯比較簡(jiǎn)單,基本上就是查找和調(diào)用響應(yīng)方法,核心函數(shù)如下

// 發(fā)送通知
- (void) postNotificationName: (NSString*)name
               object: (id)object
             userInfo: (NSDictionary*)info
{
// 構(gòu)造一個(gè)GSNotification對(duì)象, GSNotification繼承了NSNotification
  GSNotification    *notification;
  notification = (id)NSAllocateObject(concrete, 0, NSDefaultMallocZone());
  notification->_name = [name copyWithZone: [self zone]];
  notification->_object = [object retain];
  notification->_info = [info retain];

  // 進(jìn)行發(fā)送操作
  [self _postAndRelease: notification];
}
//發(fā)送通知的核心函數(shù),主要做了三件事:查找通知、發(fā)送、釋放資源
- (void) _postAndRelease: (NSNotification*)notification {
    //step1: 從named、nameless、wildcard表中查找對(duì)應(yīng)的通知
    ...
    //step2:執(zhí)行發(fā)送,即調(diào)用performSelector執(zhí)行響應(yīng)方法,從這里可以看出是同步的
    [o->observer performSelector: o->selector
                    withObject: notification];
    //step3: 釋放資源
    RELEASE(notification);
}

復(fù)制代碼

邏輯說明

其實(shí)上述代碼注釋說的很清晰了,主要做了三件事

  1. 通過name & object 查找到所有的obs對(duì)象(保存了observersel),放到數(shù)組中
  2. 通過performSelector:逐一調(diào)用sel,這是個(gè)同步操作
  3. 釋放notification對(duì)象

小結(jié)

從源碼邏輯可以看出發(fā)送過程的概述:從三個(gè)存儲(chǔ)容器中:namednameless、wildcard去查找對(duì)應(yīng)的obs對(duì)象,然后通過performSelector:逐一調(diào)用響應(yīng)方法,這就完成了發(fā)送流程

核心點(diǎn):

  1. 同步發(fā)送
  2. 遍歷所有列表,即注冊(cè)多次通知就會(huì)響應(yīng)多次

刪除通知

這里源碼太長而且基本上都是查找刪除邏輯,不一一列舉,感興趣的去下載源碼看下吧
要注意的點(diǎn):

  1. 查找時(shí)仍然以nameobject為維度的,再加上observer做區(qū)分
  2. 因?yàn)椴檎視r(shí)做了這個(gè)鏈表的遍歷,所以刪除時(shí)會(huì)把重復(fù)的通知全都刪除掉
// 刪除已經(jīng)注冊(cè)的通知
- (void) removeObserver: (id)observer
           name: (NSString*)name
                 object: (id)object {
  if (name == nil && object == nil && observer == nil)
      return;
      ...
}

- (void) removeObserver: (id)observer
{
  if (observer == nil)
    return;

  [self removeObserver: observer name: nil object: nil];
}
復(fù)制代碼

異步通知

上面介紹的NSNotificationCenter都是同步發(fā)送的,而這里介紹關(guān)于NSNotificationQueue的異步發(fā)送,從線程的角度看并不是真正的異步發(fā)送,或可稱為延時(shí)發(fā)送,它是利用了runloop的時(shí)機(jī)來觸發(fā)的

入隊(duì)

下面為精簡(jiǎn)版的源碼,看源碼的注釋,基本上能明白大致邏輯

  1. 根據(jù)coalesceMask參數(shù)判斷是否合并通知
  2. 接著根據(jù)postingStyle參數(shù),判斷通知發(fā)送的時(shí)機(jī),如果不是立即發(fā)送則把通知加入到隊(duì)列中:_asapQueue、_idleQueue

核心點(diǎn):

  1. 隊(duì)列是雙向鏈表實(shí)現(xiàn)
  2. 當(dāng)postingStyle值是立即發(fā)送時(shí),調(diào)用的是NSNotificationCenter進(jìn)行發(fā)送的,所以NSNotificationQueue還是依賴NSNotificationCenter進(jìn)行發(fā)送
/*
* 把要發(fā)送的通知添加到隊(duì)列,等待發(fā)送
* NSPostingStyle 和 coalesceMask在上面的類結(jié)構(gòu)中有介紹
* modes這個(gè)就和runloop有關(guān)了,指的是runloop的mode
*/ 
- (void) enqueueNotification: (NSNotification*)notification
        postingStyle: (NSPostingStyle)postingStyle
        coalesceMask: (NSUInteger)coalesceMask
            forModes: (NSArray*)modes
{
    ......
  // 判斷是否需要合并通知
  if (coalesceMask != NSNotificationNoCoalescing) {
      [self dequeueNotificationsMatching: notification
                coalesceMask: coalesceMask];
  }
  switch (postingStyle) {
      case NSPostNow: {
        ...
        // 如果是立馬發(fā)送,則調(diào)用NSNotificationCenter進(jìn)行發(fā)送
         [_center postNotification: notification];
         break;
      }
      case NSPostASAP:
        // 添加到_asapQueue隊(duì)列,等待發(fā)送
        add_to_queue(_asapQueue, notification, modes, _zone);
        break;

      case NSPostWhenIdle:
        // 添加到_idleQueue隊(duì)列,等待發(fā)送
        add_to_queue(_idleQueue, notification, modes, _zone);
        break;
    }
}
復(fù)制代碼

發(fā)送通知

這里截取了發(fā)送通知的核心代碼,這個(gè)發(fā)送通知邏輯如下:

  1. runloop觸發(fā)某個(gè)時(shí)機(jī),調(diào)用GSPrivateNotifyASAP()GSPrivateNotifyIdle()方法,這兩個(gè)方法最終都調(diào)用了notify()方法
  2. notify()所做的事情就是調(diào)用NSNotificationCenterpostNotification:進(jìn)行發(fā)送通知
static void notify(NSNotificationCenter *center, 
                   NSNotificationQueueList *list,
                   NSString *mode, NSZone *zone)
{
    ......
    // 循環(huán)遍歷發(fā)送通知
    for (pos = 0; pos < len; pos++)
    {
      NSNotification    *n = (NSNotification*)ptr[pos];

      [center postNotification: n];
      RELEASE(n);
    }
    ......  
}
// 發(fā)送_asapQueue中的通知
void GSPrivateNotifyASAP(NSString *mode)
{
    notify(item->queue->_center,
        item->queue->_asapQueue,
        mode,
        item->queue->_zone);
}
// 發(fā)送_idleQueue中的通知
void GSPrivateNotifyIdle(NSString *mode)
{
    notify(item->queue->_center,
        item->queue->_idleQueue,
        mode,
        item->queue->_zone);
}

復(fù)制代碼

小結(jié)

對(duì)于NSNotificationQueue總結(jié)如下

  1. 依賴runloop,所以如果在其他子線程使用NSNotificationQueue,需要開啟runloop
  2. 最終還是通過NSNotificationCenter進(jìn)行發(fā)送通知,所以這個(gè)角度講它還是同步的
  3. 所謂異步,指的是非實(shí)時(shí)發(fā)送而是在合適的時(shí)機(jī)發(fā)送,并沒有開啟異步線程

主線程響應(yīng)通知

異步線程發(fā)送通知?jiǎng)t響應(yīng)函數(shù)也是在異步線程,如果執(zhí)行UI刷新相關(guān)的話就會(huì)出問題,那么如何保證在主線程響應(yīng)通知呢?

其實(shí)也是比較常見的問題了,基本上解決方式如下幾種:

  1. 使用addObserverForName: object: queue: usingBlock方法注冊(cè)通知,指定在mainqueue上響應(yīng)block
  2. 在主線程注冊(cè)一個(gè)machPort,它是用來做線程通信的,當(dāng)在異步線程收到通知,然后給machPort發(fā)送消息,這樣肯定是在主線程處理的,具體用法去網(wǎng)上資料很多,蘋果官網(wǎng)也有

總結(jié)

本文寫的內(nèi)容比較多,以GNUStep源碼為基礎(chǔ)進(jìn)行研究,全面闡述了通知的存儲(chǔ)、發(fā)送、異步發(fā)送等原理,對(duì)研究學(xué)習(xí)有很大幫助

最后推薦個(gè)我的高級(jí)iOS交流群:761407670 進(jìn)群密碼000,有一個(gè)共同的圈子很重要,結(jié)識(shí)人脈!里面都是iOS開發(fā),全棧發(fā)展,歡迎入駐,共同進(jìn)步!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容