Skip to content

Latest commit

 

History

History
122 lines (93 loc) · 5.96 KB

2018-08-20-Animation去哪儿了.md

File metadata and controls

122 lines (93 loc) · 5.96 KB
layout title categories tags
post
Animation去哪儿了
Tech
iOS

前几天遇到一个十分诡异的Bug,有用户在使用App的过程中,遇到了所有动画突然都消失的问题。 没有Log文件,没有固定的重现步骤,一个Random的issue。 刚接到的时候内心是懵逼的,毫无头绪,拿了几个case试了几遍并没有重现问题。又换了几个case,终于有一个case能不稳定的重现了,几率还算比较高。 可是还是不知道从何下手。思考了一下,发现是所有的动画都消失了,肯定不是某个UIView或者某个动画的问题。

尝试Google了一下,找到了个关键的线索,[UIView setAnimateionEnable:]UIView的这个类方法可以控制所有动画的开启或者关闭。 八九不离十,一定是是这个变量被设置错了。全局搜索了工程里调用这个方法的地方,全都打上断点,重新试了case。断点没跑到,还是出现了动画消失的问题。那么一定是哪个系统调用,调用了这个方法,导致变量被设置错了。这就比较难办了,UIKit并不开源,我们没办法找出所有调用setAnimateionEnable:的地方。

没办法,只能祭出Hook大法了,自己动手写个UIViewCategory来Hook这个方法。

#import "UIView+Hook.h"
#import <objc/runtime.h>
@implementation UIView (Hook)

void SwizzleClassMethod(Class c, SEL orig, SEL new) {

    Method origMethod = class_getClassMethod(c, orig);
    Method newMethod = class_getClassMethod(c, new);

    c = object_getClass((id)c);

    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    else
        method_exchangeImplementations(origMethod, newMethod);
}


+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SwizzleClassMethod([self class], @selector(setAnimationsEnabled:), @selector(sw_setAnimationsEnabled:));
    });
}

+ (void)sw_setAnimationsEnabled:(BOOL)enabled {
    [[self class] sw_setAnimationsEnabled:enabled];
}

@end

打个断点在sw_setAnimationsEnabled:里看一看堆栈究竟是谁最后将动画禁止了,又跑了一遍case。 Emmmmm...茫茫多的调用,断点跑了无数遍,一头包。想一想只要保留住最后一次设置为NO的调用堆栈即可。让我们再来加些代码。

#import "UIView+Hook.h"
#import <objc/runtime.h>

@implementation UIView (Hook)

static NSArray * Callstacks;
static BOOL LastStatus = YES;

void SwizzleClassMethod(Class c, SEL orig, SEL new) {

    Method origMethod = class_getClassMethod(c, orig);
    Method newMethod = class_getClassMethod(c, new);

    c = object_getClass((id)c);

    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    else
        method_exchangeImplementations(origMethod, newMethod);
}


+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SwizzleClassMethod([self class], @selector(setAnimationsEnabled:), @selector(sw_setAnimationsEnabled:));
    });
}

+ (void)sw_setAnimationsEnabled:(BOOL)enabled {
    if (!enabled) {
        if (LastStatus) {
            Callstacks = [NSThread callStackSymbols];
        }
    } else {
        Callstacks = nil;
    }

    LastStatus = enabled;
    [[self class] sw_setAnimationsEnabled:enabled];
}

+ (NSString *)printCallstack {
    NSMutableString *ret = [[NSMutableString alloc] init];
    for (id line in Callstacks) {
        [ret appendString:line];
        [ret appendString:@"\n"];
    }
    return ret;
}

@end

用静态变量LastStatus来记录上一次设置的值,用静态变量Callstacks来记录最后一次设置为NO的调用堆栈,让我们来追踪一下是谁设置了NO而没有还原。

又跑了一遍case,成功触发了动画消失,抓紧看一下Callstacks里的堆栈信息,迅速定位到了出问题的代码:工程中有一处调用了[UIView performWithoutAnimation:]方法,希望在做某些操作的时候禁用CoreAnimation的隐式动画。而这个方法会先记录AnimationsEnabled的值,并将AnimationsEnabled设置为NO禁用动画,然后执行block,最后将AnimationsEnabled恢复到之前的状态。 很显然,是最后恢复的时候恢复到了错误的值,那么为什么会恢复错了呢?瞄了一眼堆栈,发现这个操作是在背景线程做的。 一切都清楚了:

  1. 在多线程的环境下调用了[UIView performWithoutAnimation:]方法。这个方法属于UIKit,它并不是线程安全的。在线程A中调用了[UIView performWithoutAnimation:],记录AnimationsEnabledYES,然后全局变量AnimationsEnabled被设置为了NO,接着执行block。
  2. 但是在线程A的block执行完之前,线程B调用了[UIView performWithoutAnimation:]记录了此刻的AnimationsEnabledNO,然后执行block。
  3. 线程A的block先执行完毕,恢复AnimationsEnabledYES,线程B的block后执行完毕,恢复AnimationsEnabledNO。此时AnimationsEnabled的状态就错了,造成了所有的动画消失。

这个issue的根本原因还是在背景线程调用UIKit的API。我们应该在编码的时候就严格控制这种情况的发生,否则会发生一些看起来十分奇怪的问题。Xcode已经为我们提供了实用的工具来帮我们检查背景线程调用UIKitAPI的情况。 勾上Scheme->Diagnostics->Runtime API Checking->Main Thread Checker就能替我们检查这类问题,但是由于工程比较庞大,很多人并不会非常在意这类提示,或者是看见了提示,但是感觉能work就选择忽略它。我个人还是建议把Pause on issues也打开,这样能强制我们去解决这类有潜在风险的调用。