zoukankan      html  css  js  c++  java
  • 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的较为完整的解决方案。思考和实现过程当中还有很多不成熟的地方,欢迎大家一起讨论、不正确的地方也欢迎批评指正。
  • 相关阅读:
    面向对象的继承关系体现在数据结构上时,如何表示
    codeforces 584C Marina and Vasya
    codeforces 602A Two Bases
    LA 4329 PingPong
    codeforces 584B Kolya and Tanya
    codeforces 584A Olesya and Rodion
    codeforces 583B Robot's Task
    codeforces 583A Asphalting Roads
    codeforces 581C Developing Skills
    codeforces 581A Vasya the Hipster
  • 原文地址:https://www.cnblogs.com/max5945/p/4226995.html
Copyright © 2011-2022 走看看