MBProgressHUD源码解析


简书博客已经暂停更新,想看更多技术博客请到:


听过好多次:“程序员要通过多读好的源码来提升自己”这样类似的话,而且又觉得自己有很多不会的,于是就马上启动了自己的读好源码Project


从哪个框架开始呢?我想到了SDWebImage,但是大致看下来文件很多,代码也不少,不知道从何看起,于是作罢。所以茅塞顿开,还是从最最简单的框架开始吧~因为学习曲线要给自己设定得平缓一点才有利于稳步提升,小步快跑才是王道~

找着找着就找到了MBProgressHUD,这个框架只有两个文件,一个头文件和一个实现文件,很适合我现在的水平(对于一个没怎么读过源码的选手),于是就撸起了袖子开始了。

连查知识点带记笔记一共花了大概3个小时(虽然文件很少,但是里面好多东西都不知道[捂脸])。整体说来,收获还是比较大的,除了一些零碎的语法之外,框架作者对于代码结构的设计和各种情况的考虑还是很出色的,很值得学习,而且我在下文也有介绍。

这篇总结主要分三个部分来介绍这个框架:

  1. 核心Public API
  2. 方法调用流程图
  3. 方法内部实现

不多说了,开始吧~


1. 核心Public API

1.1 属性:


@property (assign, nonatomic) MBProgressHUDMode mode;//HUD的类型
@property (assign, nonatomic) MBProgressHUDAnimation animationType UI_APPEARANCE_SELECTOR;//动画类型

@property (assign, nonatomic) NSTimeInterval graceTime;//show函数触发到显示HUD的时间段
@property (assign, nonatomic) NSTimeInterval minShowTime;//HUD显示的最短时间

1.2 类方法:


/**
 * 在某个view上添加HUD并显示
 *
 * 注意:显示之前,先去掉在当前view上显示的HUD。这个做法很严谨,我们将这个方案抽象出来:如果一个模型是这样的:我们需要将A加入到B中,但是需求上B里面只允许只有一个A。那么每次将A添加到B之前,都要先判断当前的b里面是否有A,如果有,则移除。
 */
+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated;

/**
 * 找到某个view上最上层的HUD并隐藏它。
 * 如果返回值是YES的话,就表明HUD被找到而且被移除了。
 */
+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated;

/**
 * 在某个view上找到最上层的HUD并返回它。
 * 返回值可以是空,所以返回值的关键字为:nullable
 */
+ (nullable MBProgressHUD *)HUDForView:(UIView *)view;

1.3 对象方法:


/**
 * 一个HUD的便利构造函数,用某个view来初始化HUD:这个view的bounds就是HUD的bounds
 */
- (instancetype)initWithView:(UIView *)view;

/** 
 * 显示HUD,有无动画。
 */
- (void)showAnimated:(BOOL)animated;

/** 
 * 隐藏HUD,有无动画。
 */
- (void)hideAnimated:(BOOL)animated;

/** 
 * 在delay的时间过后隐藏HUD,有无动画。
 */
- (void)hideAnimated:(BOOL)animated afterDelay:(NSTimeInterval)delay;

看完了这些比较主要的API,我们看一下方法调用的流程图:

2. 方法调用流程图:

总体来说,这个第三方框架的接口还是比较整齐的,可以大致上分为两类:显示(show)和隐藏(hide)。而且无论是调用显示方法还是隐藏方法,最终都会走到私有方法animateIn:withType: completion:里(前提是附加动画效果)。可以看一下方法调用的流程图:

方法调用流程图

看完方法调用的结构之后,我们来具体看一下方法内部是如何实现的:

3. 方法内部实现:

在讲解API之前,有必要先介绍一下HUD使用的三个Timer。

@property (nonatomic, weak) NSTimer *graceTimer; //执行一次:在show方法触发后到HUD真正显示之前,前提是设定了graceTime,默认为0
@property (nonatomic, weak) NSTimer *minShowTimer;//执行一次:在HUD显示后到HUD被隐藏之前
@property (nonatomic, weak) NSTimer *hideDelayTimer;//执行一次:在HUD被隐藏的方法触发后到真正隐藏之前

  • graceTimer:用来推迟HUD的显示。如果设定了graceTime,那么HUD会在show方法触发后的graceTime时间后显示。它的意义是:如果任务完成所消耗的时间非常短并且短于graceTime,则HUD就不会出现了,避免HUD一闪而过的差体验。
  • minShowTimer:如果设定了minShowTime,就会在hide方法触发后判断任务执行的时间是否短于minShowTime。因此即使任务在minShowTime之前完成了,HUD也不会立即消失,它会在走完minShowTime之后才消失,这应该也是避免HUD一闪而过的情况。
  • hideDelayTimer:用来推迟HUD的隐藏。如果设定了delayTime,那么在触发hide方法后HUD也不会立即隐藏,它会在走完delayTime之后才隐藏。

这三者的关系可以由下面这张图来体现(并没有包含所有的情况):

三种timer

下面开始分别讲解show系列的方法和hide系列的方法。

3.1 show系列方法


+ (instancetype)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
    MBProgressHUD *hud = [[self alloc] initWithView:view];// 接着调用 [self initWithFrame:view.bounds]:根据传进来的view的frame来设定自己的frame
    hud.removeFromSuperViewOnHide = YES;//removeFromSuperViewOnHide 应该是一个标记,表明HUD自己处于“应该被移除的状态”
    [view addSubview:hud];//在view上将自己的实例添加上去
    [hud showAnimated:animated];
    return hud;
}

//调用showAnimated:
- (void)showAnimated:(BOOL)animated {
    MBMainThreadAssert();
    [self.minShowTimer invalidate];//取消当前的minShowTimer
     self.useAnimation = animated;//设置animated状态
     self.finished = NO;//添加标记:表明当前任务仍在进行
    // 如果设定了graceTime,就要推迟HUD的显示
    if (self.graceTime > 0.0) {
        NSTimer *timer = [NSTimer timerWithTimeInterval:self.graceTime target:self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        self.graceTimer = timer;
    } 
    // ... otherwise show the HUD immediately
    else {
        [self showUsingAnimation:self.useAnimation];
    }
}

//self.graceTimer触发的方法
- (void)handleGraceTimer:(NSTimer *)theTimer {
    // Show the HUD only if the task is still running
    if (!self.hasFinished) {
        [self showUsingAnimation:self.useAnimation];
    }
}

//所有的show方法最终都会走到这个方法
- (void)showUsingAnimation:(BOOL)animated {
    // Cancel any previous animations : 移走所有的动画
    [self.bezelView.layer removeAllAnimations];
    [self.backgroundView.layer removeAllAnimations];

    // Cancel any scheduled hideDelayed: calls :取消delay的timer
    [self.hideDelayTimer invalidate];

    //记忆开始的时间
    self.showStarted = [NSDate date];
    self.alpha = 1.f;

    // Needed in case we hide and re-show with the same NSProgress object attached.
    [self setNSProgressDisplayLinkEnabled:YES];

    if (animated) {
        
        [self animateIn:YES withType:self.animationType completion:NULL];
   
    } else {

        //方法弃用警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
        self.bezelView.alpha = self.opacity;
#pragma clang diagnostic pop
        self.backgroundView.alpha = 1.f;
    }
}

我们可以看到,无论是类方法的show方法,还是对象方法的show方法,而且无论是触发了graceTimer还是没有触发,最后都会走到showUsingAnimation:方法来让HUD显示出来。

这里补充讲解一下NSProgress的监听方法:


- (void)setNSProgressDisplayLinkEnabled:(BOOL)enabled {
    // 这里使用 CADisplayLink 来刷新progress的变化。因为如果使用kvo机制来监听的话可能会非常消耗主线程(因为频率可能非常快)。
    if (enabled && self.progressObject) {
        // Only create if not already active.
        if (!self.progressObjectDisplayLink) {
            self.progressObjectDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgressFromProgressObject)];
        }
    } else {
        //不刷新
        self.progressObjectDisplayLink = nil;
    }
}

CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。 CADisplayLink以特定模式注册到runloop后, 每当屏幕显示内容刷新结束的时候,runloop就会向 CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。
参考文章:Core Animation系列之CADisplayLink

3.2 hide系列方法


+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated {
    MBProgressHUD *hud = [self HUDForView:view];//获取当前view的最前为止的HUD
    if (hud != nil) {
        hud.removeFromSuperViewOnHide = YES;
        [hud hideAnimated:animated];
        return YES;
    }
    return NO;
}

+ (MBProgressHUD *)HUDForView:(UIView *)view {
   
    NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator]; //倒叙排序
    for (UIView *subview in subviewsEnum) {
        if ([subview isKindOfClass:self]) {
            return (MBProgressHUD *)subview;
        }
    }
    return nil;
}

- (void)hideAnimated:(BOOL)animated {
    MBMainThreadAssert();
    [self.graceTimer invalidate];
     self.useAnimation = animated;
     self.finished = YES;
     //如果设定了HUD最小显示时间,那就需要判断最小显示时间和已经经过的时间的大小
     if (self.minShowTime > 0.0 && self.showStarted) {
        NSTimeInterval interv = [[NSDate date] timeIntervalSinceDate:self.showStarted];
        
        //如果最小显示时间比较大,则暂时不触发HUD的隐藏,而是启动一个timer,再经过二者的时间差的时间之后再触发隐藏
        if (interv < self.minShowTime) {
            NSTimer *timer = [NSTimer timerWithTimeInterval:(self.minShowTime - interv) target:self selector:@selector(handleMinShowTimer:) userInfo:nil repeats:NO];
            [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
            self.minShowTimer = timer;
            return;
        } 
     }
    //如果最小显示时间比较小,则立即将HUD隐藏
    [self hideUsingAnimation:self.useAnimation];
}

//self.minShowTimer触发的方法
- (void)handleMinShowTimer:(NSTimer *)theTimer {
    [self hideUsingAnimation:self.useAnimation];
}

- (void)hideUsingAnimation:(BOOL)animated {
    if (animated && self.showStarted) {
        //隐藏时,将showStarted设为nil
        self.showStarted = nil;
        [self animateIn:NO withType:self.animationType completion:^(BOOL finished) {
            [self done];
        }];
    } else {
        self.showStarted = nil;
        self.bezelView.alpha = 0.f;
        self.backgroundView.alpha = 1.f;
        [self done];
    }
}

我们可以看到,无论是类方法的hide方法,还是对象方法的hide方法,而且无论是触发还是没有触发minShowTimer,最终都会走到hideUsingAnimation这个方法里。

而无论是show方法,还是hide方法,在设定animated属性为YES的前提下,最终都会走到animateIn: withType: completion:方法:

- (void)animateIn:(BOOL)animatingIn withType:(MBProgressHUDAnimation)type completion:(void(^)(BOOL finished))completion {
    // Automatically determine the correct zoom animation type
    if (type == MBProgressHUDAnimationZoom) {
        type = animatingIn ? MBProgressHUDAnimationZoomIn : MBProgressHUDAnimationZoomOut;
    }

    //()内代表x和y方向缩放倍数
    CGAffineTransform small = CGAffineTransformMakeScale(0.5f, 0.5f);
    CGAffineTransform large = CGAffineTransformMakeScale(1.5f, 1.5f);

    // 设定初始状态
    UIView *bezelView = self.bezelView;
    if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomIn) {
        bezelView.transform = small;
    } else if (animatingIn && bezelView.alpha == 0.f && type == MBProgressHUDAnimationZoomOut) {
        bezelView.transform = large;
    }

    // 创建动画任务
    dispatch_block_t animations = ^{
        if (animatingIn) {
            bezelView.transform = CGAffineTransformIdentity;//重置
        } else if (!animatingIn && type == MBProgressHUDAnimationZoomIn) {
            bezelView.transform = large;
        } else if (!animatingIn && type == MBProgressHUDAnimationZoomOut) {
            bezelView.transform = small;
        }
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
        bezelView.alpha = animatingIn ? self.opacity : 0.f;
#pragma clang diagnostic pop
       //如果animatingIn是true,就是show方法,否则是hide方法
        self.backgroundView.alpha = animatingIn ? 1.f : 0.f;
    };

    // Spring animations are nicer, but only available on iOS 7+
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 || TARGET_OS_TV
    if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0) {
        //执行动画 >= iOS7
        [UIView animateWithDuration:0.3 delay:0. usingSpringWithDamping:1.f initialSpringVelocity:0.f options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completion];
        return;
    }
#endif
    [UIView animateWithDuration:0.3 delay:0. options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completion];
}


除了一些细节上的语法之外,我觉得该框架有几个地方值得我们借鉴:

  1. 暴露出来的API最终都会走到同一个私有方法里,仅已参数来盘吨是show方法还是hide方法。
  2. 将真正显示的时间的前后加上缓冲的时间(graceTimer 和 hideDelayTimer),可以提高可定制性和稳定性。
  3. 如果有两个方法是矛盾的,并且可以同时调用,就需要在全局设置一个属性来判断当前的状态(removeFromSuperViewOnHide属性,finished属性)
  4. 使用CADisplayLink来刷新更新频率可能很高的view。
  5. 使用NSAssert来捕获各种异常。

就这样大致写完了,没有怎么读过第三方框架的源码,所以第一次可能显得稍许不足。有不好的地方还希望多多指点哈~

哦对了,还有一件事,笔者的个人主页正式开放啦,主要将简书里的大部分文章复制到了里面,以后发布博客的话二者会同时发布滴~

因为前段时间学了H5和CSS3,所以觉得博客主题不好的地方就自己花时间调了一下,整体效果还是比较满意的:

博客截图

本文已经同步到我的个人技术博客:传送门,欢迎常来^^


本文已在版权印备案,如需转载请访问版权印。48422928

获取授权

-------------------------------- 2018年7月17日更新 --------------------------------

注意注意!!!

笔者在近期开通了个人公众号,主要分享编程,读书笔记,思考类的文章。

  • 编程类文章:包括笔者以前发布的精选技术文章,以及后续发布的技术文章(以原创为主),并且逐渐脱离 iOS 的内容,将侧重点会转移到提高编程能力的方向上。
  • 读书笔记类文章:分享编程类思考类心理类职场类书籍的读书笔记。
  • 思考类文章:分享笔者平时在技术上生活上的思考。

因为公众号每天发布的消息数有限制,所以到目前为止还没有将所有过去的精选文章都发布在公众号上,后续会逐步发布的。

而且因为各大博客平台的各种限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~

扫下方的公众号二维码并点击关注,期待与您的共同成长~

公众号:程序员维他命
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容