iOS上Delegate的懸垂指針問題


文章有點長,寫的過程很有收獲,但讀的過程不一定有收獲,慎入

【摘要】
 
懸垂指針(dangling pointer)引起的crash問題,是我們在iOS開發過程當中經常會遇到的。其中由delegate引發的此類問題更是常見。本文由一個UIActionSheet引發的delegate懸垂指針問題開始,逐步思索和嘗試解決這類問題的幾種方案並進行比較。
 
【正文】
 
UIActionSheet是一個常用的iOS系統控件,用法很簡單,實現UIActionDelegate協議方法,然后通過showInView:等方法彈出。我們來看一段代碼:(如無特殊說明,本文中的代碼均在ARC條件下書寫)
 
 - (void)popUpActionSheet
{
    UIActionSheet* sheet = [[UIActionSheet alloc] initWithTitle:nil 
                                                       delegate:self 
                                              cancelButtonTitle:NSLocalizedString(@"str_cancel", @"")
                                         destructiveButtonTitle:NSLocalizedString(@"str_delete", @"")
                                               otherButtonTitles:nil];
    [sheet showInView:self.view];
}

 

 
像這樣用一個局部變量彈出actionsheet的代碼喜聞樂見。
 
那么這樣做是否有問題呢?
 
來看某項目中的一個bug:頁面X,按住區域Y點擊按鈕B,再點擊按鈕C(關閉),松開后頁面X退出,但actionsheet A彈出,點擊其中的按鈕,程序crash。
 
從描述中不難看出問題所在:這是個dangling pointer的問題。點擊按鈕B,本應彈出actionsheet A,但由於某些特殊操作(具體原因各不相同,這里是按住區域Y),這個actionsheet的彈出被延遲了,當它彈出的時候,其delegate(通常是一個UIViewController或者一個UIView,這里是頁面的ViewController X)已經被銷毀了,於是delegate成了一個dangling pointer,點擊按鈕,向delegate發送消息的時候,就出現了crash。
 
為了防止retain cycle,iOS中大部分的delegate都是不增加對象(X)的引用計數的(弱引用),因而容易出現dangling pointer的問題。對於此類問題,解決方向通常有兩個:
其一,在向delegate發送消息之前,判斷delegate是否仍然有效;
其二,使對象X在dealloc的時候,主動設置所有指向X的delegate為nil。
 
對於方向一,看上去很美,如果能夠在發消息前判斷一個指針是否是dangling pointer,那么我們就有了最后一道防線,從此再不會發生此類crash問題。但是,當dangling pointer真出現的時候,我們更應反思一下代碼設計上是否出現了不合理的地方,而不是簡單以這種方式捕獲並丟棄。
 
相比之下,方向二“銷毀時置空”這個方案顯得更治本,亦是一種良好的編程習慣。推而廣之,不局限於delegate,所有弱引用指針都可以如此處理。
 
這正是ARC中引入的weak指針的概念,它會在所指對象dealloc的時候自動置為nil。也就是說,只要所有delegate都是weak類型的,此類dangling pointer問題就不復存在了。本文也可以到此結束了。
 
但是,現實總是殘酷的。首先,weak指針只有在iOS 5.0及以上的版本中的ARC條件下才能使用,而目前很多項目依然需要支持iOS4.3。當然,隨着iOS7的發布,這種情況會有所好轉。但即使所有的用戶都是5.0+,問題仍然沒有解決。為何?
 
我們自定義的delegate,可以全部采用weak類型。但是系統控件是什么情況呢?比如UIActionSheet,看看iOS7.0版本SDK下的UIActionSheet.h:
 
 @property(nonatomic,assign) id delegate;    // weak reference

 

 
即使是7.0,這些系統控件的delegate仍然不是weak,而是assign,大約是為了兼容非ARC環境的原因吧。 也就是說,weak指針並不能解決系統控件delegate的dangling pointer問題。這下腫么辦?
 
花開兩朵,各表一支。
 
我們先回過頭來看另外一個問題:為什么actionsheet會出現這個dangling pointer的問題?
 
直接原因是作為delegate的ViewController X被銷毀了,而此時actionsheet A本身還在顯示。但這個A明明是show在self.view上的,為什么self.view都沒了,它還會存在呢?
 
我們來看下面一段代碼:
 
 -(void)viewDidAppear:(BOOL)animated
{
    UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:@"abcde" delegate:self cancelButtonTitle:@"cancel" destructiveButtonTitle:nil otherButtonTitles:nil];

    NSLog(@"application windows:%@", [UIApplication sharedApplication].windows);

    [sheet showInView:self.view];

    NSLog(@"self.window:%@", self.view.window);
    NSLog(@"sheet.window:%@", sheet.window);

    NSLog(@"application windows:%@", [UIApplication sharedApplication].windows);
}

 


主要運行結果(為iphone上的,情況在iPad上略有不同)如下:

application windows:(
    UIWindow ...
) // actionsheet彈出前,只有1個UIWindow

self.window: UIWindow ...
sheet.window: _UIAlertOverlayWindow ...

application windows:(
    UIWindow ...
    UITextEffectsWindow ...
    _UIAlertOverlayWindow ...
 ) // actionsheet彈出后,有3個UIWindow

 

原來iOS的application並非只有一個window,actionsheet是彈在另外一個內部的window上的(iPad上情況不同,只有一個window,actionsheet是在showInView的superview的一個叫UIDimmingView的subview上),與showInView:方法中指定的view並沒有持有的關系,所以能在完全不依賴於后者生命周期的情況下存在,於是出現dangling pointer delegate一點也不奇怪了。
 
那么知道了來龍去脈以后,我們可以開始着手解決文章開始時的那個bug了。按照“在一個對象X dealloc的時候,設置所有指向X的delegate為空”這個“方向二”的中心思想,weak指針是派不上用場了,我們只能另想辦法。
 
通過分析,我們知道落實這個“中心思想”的要點就是:
 
怎樣在X dealloc的時候獲取到所有delegate指向X的actionsheet A?
  
由於文章開始時喜聞樂見的代碼中,actionsheet是局部變量彈出的,在ViewController X dealloc的時候,我們已經訪問不到那個局部變量,怎么辦呢?
 
思路1:

改用一個實例變量V保存actionsheet。在X的dealloc方法里置V.delegate = nil。
 
這毫無疑問是最容易想到的方法,無須贅述。只是要注意一個問題:actionsheet是可以同時(或相繼)彈出多個的(我們會看到背景的黑色蒙板隨着彈出actionsheet的數量而疊加,越來越深。)這樣一來,我們要么改用一個數組來保存actionsheet的指針(們),要么就要在每彈出一個新的時候,就把舊的處理掉(或者delegate置空,或者干脆dismiss掉)。
 
這種思路,優點有二:
          一、思路簡單,代碼添加量少;
          二、如果你是在寫一個iPad app,那反正應付轉屏重新布局actionsheet也是需要這個實例變量的,一舉多得。

其缺點也有二:
          一、這種方式通用性差,我們需要針對每一個這樣的X都寫一遍這樣的代碼,如果這是一個已經存在的項目,而這個項目里幾乎所有的actionsheet都是這樣用局部變量彈出的,怎么辦?我們需要修改多少代碼?
          二、actionsheet作為一個系統控件,ViewController多數情況下只是控制彈出和實現delegate方法,並不做其他任何操作,這也就是為什么會出現前述喜聞樂見的代碼,因為其他地方用不着引用這個actionsheet。只為解決dangling pointer的問題而在類中添加一個實例變量保存指針,甚至要保存一個指針數組,並且這部分代碼還和類本身邏輯的代碼耦合在一起,有潔癖的人看起來總覺得刺眼。
 
理想中解決dangling pointer問題的方法,應該是一個通用的基礎方法,與類的業務邏輯無關,代碼相對獨立。 
 
思路2:

不用實例變量,想辦法在delegate dealloc的時候獲得actionsheet的指針。
 
系統的view樹一定是保存了actionsheet的指針的,第一反應是想在actionsheet上打tag,然后利用viewWithTag:方法來獲取。或者,在dealloc的時候遍歷整個view樹來尋找當前存在的actionsheet,這兩種方法本質上是相同的。我們暫且不討論遍歷view樹的開銷是否值得,只討論方法可行性。剛才我們說過,iphone上的actionsheet是從屬於一個內部window的,並不在我們程序可控的window中,所以上述方法根結點的選取是關鍵。
 
 UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:@"sheet" delegate:self cancelButtonTitle:@"cancel" destructiveButtonTitle:nil otherButtonTitles:nil];
    [sheet showInView:self.view];

    [sheet setTag:kSheetTag];

    NSLog(@"root(self.view.window):%@", [self.view.window viewWithTag:kSheetTag]);  // null
    NSLog(@"root(internal window):%@", [[UIApplication sharedApplication].windows[2] viewWithTag:kSheetTag]); // actionsheet found!

 


結果情理之中,我們在當前的window上是遍歷不到這個actionsheet的,需要在之前說的_UIAlertOverlayWindow上遍歷才行。於是我們可以先在actionsheet創建時打個tag,然后在X dealloc方法里這樣寫:(不能應付多個actionsheet彈出的情況)
 
    
 [[UIApplication sharedApplication].windows enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        if (strcmp(class_getName([obj class]),"_UIAlertOverlayWindow") == 0)
        {
            UIActionSheet *theSheet = (UIActionSheet *)[obj viewWithTag:kSheetTag];
            [theSheet setDelegate:nil];
        }
    }];

 

 
也可以不打tag,直接采用遍歷view樹的方式。(如果是在ipad上,不用使用內部window,直接遍歷自己的self.view.superview的subviews就行了,可自行實驗)

 [[UIApplication sharedApplication].windows enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
         [self traverseView:obj];
    }];
 
// 遍歷view樹
- (void)traverseView:(UIView *)root
{
    [[root subviews] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        if ([obj isKindOfClass:[UIActionSheet class]])
        {
            if (((UIActionSheet *)obj).delegate == self)
            {
                ((UIActionSheet *)obj).delegate = nil;
                NSLog(@"enemy spotted!");
            }
        }
        else
        {
            [self traverseView:obj];
        }
    }];
}

 


這樣也解決了問題,其優點有一:
          一、不用改動類X的業務邏輯部分的代碼,修改范圍縮小到X的dealloc方法中,相對來講便於移植到其他類中,也可以通過一些runtime的手段實現自動化。
          二、遍歷的方法,可以輕易應對彈出多個actionsheet的情況。
 
其缺點有二:
          一、提起遍歷view樹,開銷問題是肯定要考慮的。以某項目為例,一vc在dealloc的時候,view樹中共有320個view,遍歷尋找UIAcionSheet並置delegate空需要約0.002s,而正常的dealloc方法只需要不到0.0001s,時間提升20倍。實話說,這個開銷並不是大到無法忍受,是否划算視具體情況而定。
          二、如果想節省這個開銷,那么就需要利用一些“潛規則”,例如像上面viewWithTag的方法代碼那樣,利用_UIAlertOverlayWindow這個類名來縮小遍歷范圍。潛規則這個東西,如果用,請慎用。它們是毫無文檔保障的,可能下一代iOS,這個實現就被改掉了,那時我們的代碼就會出問題。譬如通過hack系統imagePicker的方式實現多圖選擇框,算是一個比較常見且“合理”的利用“潛規則”的例子(因為常規用assetslibrary實現的多圖選擇框在iOS5上會有定位權限的提示問題,這是很多產品不願意接受的事情),但是iOS7中,imagePicker的內部ViewController的名字就被改掉了,原來用這種方式實現多圖選擇框的代碼,就需要跟進修改。
 
思路3:

從思路1和2中我們可以得到這樣的啟發,如果有一個集合,里面存放了所有delegate指向X的actionsheet A(甚至其它對象實例),那么,我們就能在dealloc時遍歷這個集合來置A.delegate = nil。
 
上述這種集合S有如下特征:
 
1、S能夠與一個對象X實現1對1的綁定或對應,並在X dealloc的時候能被訪問到。
2、在合適的時機(比如設置delegate時),能夠對S添加或刪除元素
 
我們先按1和2抽象出一個通用的包含集合S的類結構,取名為parasite:

 @interface DelegateBaseParasite : NSObject
{
    NSMutableSet *sanctuarySet_; // 集合S
}
 
// 創建並將自己(parasite)綁定(對應)到hostObj X 上
+ (DelegateBaseParasite *)parasitizeIn:(id)hostObj;
 
// 返回已經綁定(對應)到hostObj X上的parasite對象(或nil若未綁定)
+ (DelegateBaseParasite *)getParasiteFromHost:(id)hostObj;

// 添加一個對象object到此parasite的集合S中,當object.delegate = hostObj X的時候
- (void)addToSanctuary:(id)object;
 
// 從此parasite的集合S中移除object,當object.delegate不再=X的時候
- (void)removeFromSanctuary:(id)object;

// 將所有sanctuary中對象的delegate(此時都指向hostObj)置為nil
- (void)redemptionAll;
@end

 

大意是:如果每一個X都與一個這樣的DelegateBaseParasite P綁定(對應),在設置A.delegate = X的時候,調用addToSanctuary將A添加到P的集合S中(同時通過removeFromSanctuary方法將A從舊delegate綁定parasite的集合S中移除),並且在X dealloc的時候執行redemptionAll方法來清空集合S里的所有對象的delegate屬性,那么問題就解決了。
 
對集合S操作的方法沒有什么復雜的。重點關注的是如何實現對象X與parasite P一對一的綁定。
我們發現這個parasite對象有如下特點:
1、與宿主的類型和實現完全無關,沒有調用宿主的任何方法或訪問任何實例變量。
2、只需要在宿主dealloc的時候調用自己的一個方法,並且自己也被銷毀。
 
這讓我們不禁想到了一個叫做associate object( 關聯對象文檔)的東西!不妨將DelegateBaseParasite作為一個associate object,綁定到X上。按這個思路派生一個DelegateAssociativeParasite類,實現一下綁定相關方法:

 #define kDelegateAssociativeParasiteSanctuaryKey "kDelegateAssociativeParasiteSanctuaryKey"

@implementation DelegateAssociativeParasite

#pragma mark -
#pragma public interface
+ (DelegateAssociativeParasite *)parasitizeIn:(id)hostObj
{
    DelegateAssociativeParasite *parasite = [[DelegateAssociativeParasite alloc] init];
    
    objc_setAssociatedObject(hostObj, &kDelegateAssociativeParasiteSanctuaryKey, parasite, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    return parasite;
}

+ (DelegateAssociativeParasite *)getParasiteFromHost:(id)hostObj
{
    return objc_getAssociatedObject(hostObj, &kDelegateAssociativeParasiteSanctuaryKey);
}

- (void)Dealloc
{
    [self redemptionAll];
}

@end

 

不知不覺,我們已經成功了一半。也就是說,還有另一半的問題需要我們解決:
 
即我們需要在actionsheet A的setDelegate:方法中刪除綁定於舊delegate的集合S中的元素A,並添加A到綁定於新delegate X的集合S中。還要在A的dealloc方法中調用[self setDelegate:nil]
 
每次調用setDelegate的時候添加代碼手動修改么?這顯然不是一個好辦法,並且,actionsheet的dealloc時機,並不由我們控制,想手動添加代碼都辦不到。那么有沒有辦法能修改這兩個方法的實現,並且這種修改還能夠調用到其原有的方法實現呢?
 
繼承一個DelegateAutoUnregisteredUIActionSheet出來當然可以辦到,但是將所有UIActionSheet替換掉,仍然要做不少工程,而且,功能上只是在UIActionSheet上打個自動注銷delegate的補丁,沒必要也不應該采用繼承的方式。
 
能不能用category呢?category復寫主類同名方法會產生warning,屬於apple強烈不推薦的方式,而且就算強行復寫了主類同名方法,也無法調用原來的實現。
 
那么怎么辦呢?可以用objc的runtime提供的一些方法。先用class_addMethod為類添加一個新方法,在新方法中調用原有實現,再用method_exchangeImplementation將其與原有實現做交換。( objc runtime文檔
 
按這個思路我們可以寫一個輔助類DelegateAutoUnregisterHelper類(代碼見附件示例工程)。

 
這樣一來,另一半問題也解決了,現在只需在main.m里簡單調用:
 
 [DelegateAutoUnregisterHelper registerDelegateAutoUnregisterTo:[UIActionSheet class]];

 

就可以實現actionsheet的delegate自動置空功能了。
 
這個利用associate object和runtime相結合的解法,也有其優缺點。其優點有二:
     一、向工程中添加的DelegateAssociativeParasite和DelegateAutoUnregisterHelper兩個類是完全與其它類獨立的代碼,與業務邏輯無關,邏輯清晰。
     二、使用簡單,只需要在main.m中調用一個方法對目標類(UIActionSheet)進行注冊。項目之前“喜聞樂見”的代碼完全不用做任何修改。
其缺點有一:
     一、廣義的dangling pointer delegate出現最多的場景其實是多線程。一個線程釋放了delegate對象,而另外一個線程恰好在使用它。反觀我們剛才寫的代碼,卻完全沒有考慮任何線程安全的問題。
     
我們不禁要問兩個問題:
     1. 解決UIActionSheet的delegate問題為什么可以不考慮線程安全?
     2. 這種利用associate object的思路,能否通過鎖/信號量等方式解決線程安全的問題?
 
問題1是由UIActionSheet的使用場景決定的,作為一個系統的UI控件,在大多數情況下,其setDelegate、dealloc、showInView等方法,都是在UI線程中調用的。而其delegate一般都是一個UIView或者UIViewController,這兩種對象的銷毀通常也是發生在UI線程里(實際上,假如我們發現我們的某些View或者ViewController的最后一次釋放以致銷毀跑到了非UI線程,我們應該停下來思考一下是不是設計上出了問題,因為View和VC的釋放很有可能會涉及到一些在UI線程才能進行的操作。)當然,我說的是大多數情況,而並非絕對。因而通常正常使用actionsheet並不會涉及線程安全問題。
 
那么來到問題2,這種以associate object為核心的綁定方式,究竟有沒有可能解決線程安全問題呢?
 
一推敲,天然的缺陷就暴露出來了。
 
之前我們一直刻意模糊了一個概念,即“當X dealloc的時候”。dealloc的時候是什么時候?是dealloc前還是dealloc后?
 
對於associate object,其dealloc方法,是在其宿主X的dealloc方法調用完畢以后,也就是宿主X已經被銷毀之后,才調用的。也就是說,delegate的置空是在delegate被銷毀之后。無論之間間隔多么短,總是有那么一瞬間,X已經被銷毀了,delegate還沒有被置空,dangling pointer出現,如果是在多線程的場景下,就有可能有另外的線程在此時訪問到了這個dangling pointer,程序依然會crash。
 
所以,基於associate object的解決方案,歸根結底是無法解決線程安全的問題的。
 
那么怎樣才能做出一個線程安全的dangling pointer delegate問題的解決方案呢?
 
思路4:

既然問題出在associate object上,那我們就不用它,想想有沒有其它實現X與P一對一綁定(對應)的方法。這時我們又想起了weak指針。系統是怎么做到將object與指向其的weak指針集合綁定(對應)在一起的呢?
 
關於weak指針的實現,我們可以在 llvm.org上看到相關的文檔內容( http://clang.llvm.org/docs/AutomaticReferenceCounting.html),但是不夠詳細。更直接的方式是閱讀 http://www.opensource.apple.com/source/objc4/里面的NSObject和runtime實現的源碼。
 
簡而言之,編譯器實現的weak指針與我們的中心思想是一致的,即用一種方法綁定對象X和一個指向X的需要監視的指針集合,並在X dealloc之時自動將集合內元素置空。只不過與associate object的方法相比,有兩點不同:
 
1. 綁定對象,用的是一個全局的hash table(SideTable),而非associate object。hash table的key對應一個對象X,value為指針集合。
2. dealloc之時,指的是X的dealloc方法調用過程之中,而非最終銷毀以后,這樣就不存在天然的缺陷,其線程安全問題是可以通過在hash table上加鎖來解決的。
 
按照這個思路,我們來派生一個新的DelegateDictParasite類,實現另一種利用CFDictionary的綁定(對應)的方法:

 @implementation DelegateDictParasite

+ (DelegateDictParasite *)parasitizeIn:(id)hostObj
{
    if (!class_getInstanceMethod([hostObj class], @selector(myHostObjDealloc)))
    {
        [DelegateDictParasite addNewMethodToHost:[hostObj class]];
        [DelegateAutoUnregisterHelper mergeOldSEL:[DelegateAutoUnregisterHelper deallocSelector] NewSEL:@selector(myHostObjDealloc) ForClass:[hostObj class]];
        [DelegateAutoUnregisterHelper mergeOldSEL:[DelegateAutoUnregisterHelper releaseSelector] NewSEL:@selector(myHostObjRelease) ForClass:[hostObj class]];
    }
    
    DelegateDictParasite *parasite;
    
    @synchronized(kDelegateAssociativeParasiteLock)
    {
        if (!delegateHostParasiteHashTable)
        {
            delegateHostParasiteHashTable = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        }
    
        parasite = [[DelegateDictParasite alloc] init];

        CFDictionarySetValue(delegateHostParasiteHashTable, (__bridge const void *)(hostObj), (__bridge const void *)(parasite));
    }
    
    return parasite;
}

+ (DelegateDictParasite *)getParasiteFromHost:(id)hostObj
{
    DelegateDictParasite *parasite;
    
    @synchronized(kDelegateAssociativeParasiteLock)
    {
        if (!delegateHostParasiteHashTable)
        {
            return nil;
        }
        
        parasite = CFDictionaryGetValue(delegateHostParasiteHashTable, (__bridge const void *)(hostObj));
    }
    
    return parasite;
}

@end

 

這里,由於沒有了associate object的幫助,X dealloc與parasite dealloc的聯動需要我們自己觸發,同樣利用runtime,我們可以改寫每一個X的dealloc方法來完成這種聯動,解除hash table中對X的綁定,從而引發自動置空。
 
另外,通過鎖,我們可以解決線程安全問題。從而解決多線程下delegate的dangling pointer問題。(完整代碼見附錄)
 
這種思路,其優點有二:
     一、沒有了先天缺陷,解決了線程安全問題,從而可以推廣到廣義的dangling pointer delegate問題上。
     二、用法與思路三一樣,比較簡單。

其缺點有二:
     一、用了全局的一個hash table。一般有潔癖的人看到全局變量會不舒服。
     二、對每一個成為delegate的對象X的類,都會修改其dealloc方法,不像associate object的聯動那么自然,有點不干凈。
 
思路5:
 
GitHub上有一個mikeash寫的開源項目MAZeroingWeakRef,目的是在不支持weak的情況下提供一個weak指針的實現,其實現思想也是與系統weak指針類似,即利用全局hash table來做。與思路4不同的是,它修改X的dealloc方法,是通過動態繼承出X的一個子類,然后在子類上addMethod的方式,而不是利用method_exchangeImplementation。
 
這個項目考慮了更多的情況,比如說對於KVO的支持以及toll-free的CF對象的處理(不過用到了私有API)等等,大家有興趣和時間的話可以研究一下,不再贅述。
 
其優點有二:
     一、考慮了KVO/CF等情況的支持,更加嚴謹。
     二、動態繼承的方式把dealloc方法修改的范圍縮小到只是使用weak的實例而不是此類的所有實例,解決了思路4的缺點二。
其缺點有二:
     一、動態繼承的方式修改了類的名字。
     二、只是用來在weak不能使用的條件下實現weak指針,可以解決自定義的delegate的dangling pointer問題,並不能解決文中已經被指定為assign類型的系統控件delegate的問題。
 
注:本文由於篇幅所限,實現過程中一些坑和有意思的地方並未一一提及。例如修改方法實現的時候,需要注意修改的是父類方法還是子類方法;一些方法實現只能放在非ARC(添加-fno-objc-arc標志)文件中;等等。
 
 
【總結】
 
本文逐步思考並總結的幾種解決dangling pointer問題的思路各有優缺點,並不存在哪種一定最好,要具體情況具體分析。相比之下,思路4是解決多線程delegate的dangling pointer的較為完整的解決方案。思考和實現過程當中還有很多不成熟的地方,歡迎大家一起討論、不正確的地方也歡迎批評指正。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM