GitHub源码地址,及时更新:iOS-Source-Code-Analyze

MBProgressHUD是一个为iOS app添加透明浮层 HUD 的第三方框架。作为一个 UI 层面的框架,它的实现很简单,但是其中也有一些非常有意思的代码。

MBProgressHUD

MBProgressHUD是一个 UIView 的子类,它提供了一系列的创建 HUD 的方法。我们在这里会主要介绍三种使用 HUD 的方法。

  • + showHUDAddedTo:animated:

  • - showAnimated:whileExecutingBlock:onQueue:completionBlock:

  • - showWhileExecuting:onTarget:withObject:

+ showHUDAddedTo:animated:

MBProgressHUD 提供了一对类方法 + showHUDAddedTo:animated: 和 + hideHUDForView:animated: 来创建和隐藏 HUD, 这是创建和隐藏 HUD 最简单的一组方法

+ (MB_INSTANCETYPE)showHUDAddedTo:(UIView *)view animated:(BOOL)animated {
    MBProgressHUD *hud = [[self alloc] initWithView:view];
    hud.removeFromSuperViewOnHide = YES;
    [view addSubview:hud];
    [hud show:animated];
    return MB_AUTORELEASE(hud);
}

- initWithView:

首先调用 + alloc - initWithView: 方法返回一个 MBProgressHUD 的实例, - initWithView: 方法会调用当前类的 - initWithFrame: 方法。

通过 - initWithFrame: 方法的执行,会为 MBProgressHUD 的一些属性设置一系列的默认值。

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Set default values for properties
        self.animationType = MBProgressHUDAnimationFade;
        self.mode = MBProgressHUDModeIndeterminate;
        ...
        // Make it invisible for now
        self.alpha = 0.0f;
        [self registerForKVO];
        ...
    }
    return self;
}

在 MBProgressHUD 初始化的过程中, 有一个需要注意的方法 - registerForKVO, 我们会在之后查看该方法的实现。

- show:

在初始化一个 HUD 并添加到 view 上之后, 这时 HUD 并没有显示出来, 因为在初始化时, view.alpha 被设置为 0。所以我们接下来会调用 - show: 方法使 HUD 显示到屏幕上。

- (void)show:(BOOL)animated {
    NSAssert([NSThread isMainThread], @"MBProgressHUD needs to be accessed on the main thread.");
    useAnimation = animated;
    // If the grace time is set postpone the HUD display
    if (self.graceTime > 0.0) {
        NSTimer *newGraceTimer = [NSTimer timerWithTimeInterval:self.graceTime target:self selector:@selector(handleGraceTimer:) userInfo:nil repeats:NO];
        [[NSRunLoop currentRunLoop] addTimer:newGraceTimer forMode:NSRunLoopCommonModes];
        self.graceTimer = newGraceTimer;
    }
    // ... otherwise show the HUD imediately
    else {
        [self showUsingAnimation:useAnimation];
    }
}

因为在 iOS 开发中,对于 UIView 的处理必须在主线程中, 所以在这里我们要先用 [NSThread isMainThread] 来确认当前前程为主线程。

如果 graceTime 为 0,那么直接调用 - showUsingAnimation: 方法, 否则会创建一个 newGraceTimer 当然这个 timer 对应的 selector 最终调用的也是 - showUsingAnimation: 方法。

- showUsingAnimation:

- (void)showUsingAnimation:(BOOL)animated {
    // Cancel any scheduled hideDelayed: calls
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    [self setNeedsDisplay];
    if (animated && animationType == MBProgressHUDAnimationZoomIn) {
        self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(0.5f, 0.5f));
    } else if (animated && animationType == MBProgressHUDAnimationZoomOut) {
        self.transform = CGAffineTransformConcat(rotationTransform, CGAffineTransformMakeScale(1.5f, 1.5f));
    }
    self.showStarted = [NSDate date];
    // Fade in
    if (animated) {
        [UIView beginAnimations:nil context:NULL];
        [UIView setAnimationDuration:0.30];
        self.alpha = 1.0f;
        if (animationType == MBProgressHUDAnimationZoomIn || animationType == MBProgressHUDAnimationZoomOut) {
            self.transform = rotationTransform;
        }
        [UIView commitAnimations];
    }
    else {
        self.alpha = 1.0f;
    }
}

这个方法的核心功能就是根据 animationType 为 HUD 的出现添加合适的动画。

typedef NS_ENUM(NSInteger, MBProgressHUDAnimation) {
    /** Opacity animation */
    MBProgressHUDAnimationFade,
    /** Opacity + scale animation */
    MBProgressHUDAnimationZoom,
    MBProgressHUDAnimationZoomOut = MBProgressHUDAnimationZoom,
    MBProgressHUDAnimationZoomIn
};

它在方法刚调用时会通过 - cancelPreviousPerformRequestsWithTarget: 移除附加在 HUD 上的所有 selector, 这样可以保证该方法不会多次调用。

同时也会保存 HUD 的出现时间。

self.showStarted = [NSDate date]

+ hideHUDForView:animated:

+ (BOOL)hideHUDForView:(UIView *)view animated:(BOOL)animated {
    MBProgressHUD *hud = [self HUDForView:view];
    if (hud != nil) {
        hud.removeFromSuperViewOnHide = YES;
        [hud hide:animated];
        return YES;
    }
    return NO;
}

+ hideHUDForView:animated: 方法的实现和 + showHUDAddedTo:animated: 差不多, + HUDForView: 方法会返回对应 view 最上层的 MBProgressHUD 的实例。

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

然后调用的 - hide: 方法和 - hideUsingAnimation: 方法也没有什么特别的, 只有在 HUD 隐藏之后 - done 负责隐藏执行 completionBlock 和 delegate 回调。

- (void)done {
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    isFinished = YES;
    self.alpha = 0.0f;
    if (removeFromSuperViewOnHide) {
        [self removeFromSuperview];
    }
#if NS_BLOCKS_AVAILABLE
    if (self.completionBlock) {
        self.completionBlock();
        self.completionBlock = NULL;
    }
#endif
    if ([delegate respondsToSelector:@selector(hudWasHidden:)]) {
        [delegate performSelector:@selector(hudWasHidden:) withObject:self];
    }
}

- showAnimated:whileExecutingBlock:onQueue:completionBlock:

当 block 指定的队列执行时, 显示 HUD, 并在 HUD 消失时, 调用 completion。

同时 MBProgressHUD 也提供一些其他的便利方法实现这一功能:

- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block;
- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block completionBlock:(MBProgressHUDCompletionBlock)completion;
- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue;

该方法会异步在指定 queue 上运行 block 并在 block 执行结束调用 - cleanUp。

- (void)showAnimated:(BOOL)animated whileExecutingBlock:(dispatch_block_t)block onQueue:(dispatch_queue_t)queue
     completionBlock:(MBProgressHUDCompletionBlock)completion {
    self.taskInProgress = YES;
    self.completionBlock = completion;
    dispatch_async(queue, ^(void) {
        block();
        dispatch_async(dispatch_get_main_queue(), ^(void) {
            [self cleanUp];
        });
    });
    [self show:animated];
}

关于 - cleanUp 我们会在下一段中介绍。

- showWhileExecuting:onTarget:withObject:

当一个后台任务在新线程中执行时,显示 HUD。

- (void)showWhileExecuting:(SEL)method onTarget:(id)target withObject:(id)object animated:(BOOL)animated {
    methodForExecution = method;
    targetForExecution = MB_RETAIN(target);
    objectForExecution = MB_RETAIN(object);
    // Launch execution in new thread
    self.taskInProgress = YES;
    [NSThread detachNewThreadSelector:@selector(launchExecution) toTarget:self withObject:nil];
    // Show HUD view
    [self show:animated];
}

在保存 methodForExecution targetForExecution 和 objectForExecution 之后, 会在新的线程中调用方法。

- (void)launchExecution {
    @autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        // Start executing the requested task
        [targetForExecution performSelector:methodForExecution withObject:objectForExecution];
#pragma clang diagnostic pop
        // Task completed, update view in main thread (note: view operations should
        // be done only in the main thread)
        [self performSelectorOnMainThread:@selector(cleanUp) withObject:nil waitUntilDone:NO];
    }
}

- launchExecution 会创建一个自动释放池, 然后再这个自动释放池中调用方法, 并在方法调用结束之后在主线程执行 - cleanUp。

Trick

在 MBProgressHUD 中有很多神奇的魔法来解决一些常见的问题。

ARC

MBProgressHUD 使用了一系列神奇的宏定义来兼容 MRC。

#ifndef MB_INSTANCETYPE
#if __has_feature(objc_instancetype)
    #define MB_INSTANCETYPE instancetype
#else
    #define MB_INSTANCETYPE id
#endif
#endif
#ifndef MB_STRONG
#if __has_feature(objc_arc)
    #define MB_STRONG strong
#else
    #define MB_STRONG retain
#endif
#endif
#ifndef MB_WEAK
#if __has_feature(objc_arc_weak)
    #define MB_WEAK weak
#elif __has_feature(objc_arc)
    #define MB_WEAK unsafe_unretained
#else
    #define MB_WEAK assign
#endif
#endif

通过宏定义 __has_feature 来判断当前环境是否启用了 ARC, 使得不同环境下宏不会出错。

KVO

MBProgressHUD 通过 @property 生成了一系列的属性。

- (NSArray *)observableKeypaths {
    return [NSArray arrayWithObjects:@"mode", @"customView", @"labelText", @"labelFont", @"labelColor",
            @"detailsLabelText", @"detailsLabelFont", @"detailsLabelColor", @"progress", @"activityIndicatorColor", nil];
}

这些属性在改变的时候不会, 重新渲染整个 view, 我们在一般情况下覆写 setter 方法, 然后再 setter 方法中刷新对应的属性,在 MBProgressHUD 中使用 KVO 来解决这个问题。

- (void)registerForKVO {
    for (NSString *keyPath in [self observableKeypaths]) {
        [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL];
    }
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(updateUIForKeypath:) withObject:keyPath waitUntilDone:NO];
    } else {
        [self updateUIForKeypath:keyPath];
    }
}
- (void)updateUIForKeypath:(NSString *)keyPath {
    if ([keyPath isEqualToString:@"mode"] || [keyPath isEqualToString:@"customView"] ||
        [keyPath isEqualToString:@"activityIndicatorColor"]) {
        [self updateIndicators];
    } else if ([keyPath isEqualToString:@"labelText"]) {
        label.text = self.labelText;
    } else if ([keyPath isEqualToString:@"labelFont"]) {
        label.font = self.labelFont;
    } else if ([keyPath isEqualToString:@"labelColor"]) {
        label.textColor = self.labelColor;
    } else if ([keyPath isEqualToString:@"detailsLabelText"]) {
        detailsLabel.text = self.detailsLabelText;
    } else if ([keyPath isEqualToString:@"detailsLabelFont"]) {
        detailsLabel.font = self.detailsLabelFont;
    } else if ([keyPath isEqualToString:@"detailsLabelColor"]) {
        detailsLabel.textColor = self.detailsLabelColor;
    } else if ([keyPath isEqualToString:@"progress"]) {
        if ([indicator respondsToSelector:@selector(setProgress:)]) {
            [(id)indicator setValue:@(progress) forKey:@"progress"];
        }
        return;
    }
    [self setNeedsLayout];
    [self setNeedsDisplay];
}

- observeValueForKeyPath:ofObject:change:context: 方法中的代码是为了保证 UI 的更新一定是在主线程中, 而 - updateUIForKeypath: 方法负责 UI 的更新

对MBProgressHUD第三方进行源码分析的更多相关文章

  1. 经典iOS第三方库源码分析 - YYModel

    YYModel介绍 YYModel是一个针对iOS/OSX平台的高性能的Model解析库,是属于YYKit的一个组件,创建是ibireme. 其实在YYModel出现之前,已经有非常多的Model解析 ...

  2. Flask框架 (四)—— 请求上下文源码分析、g对象、第三方插件(flask_session、flask_script、wtforms)、信号

    Flask框架 (四)—— 请求上下文源码分析.g对象.第三方插件(flask_session.flask_script.wtforms).信号 目录 请求上下文源码分析.g对象.第三方插件(flas ...

  3. iOS常用框架源码分析

    SDWebImage NSCache 类似可变字典,线程安全,使用可变字典自定义实现缓存时需要考虑加锁和释放锁 在内存不足时NSCache会自动释放存储的对象,不需要手动干预 NSCache的key不 ...

  4. ABP源码分析九:后台工作任务

    文主要说明ABP中后台工作者模块(BackgroundWorker)的实现方式,和后台工作模块(BackgroundJob).ABP通过BackgroundWorkerManager来管理Backgr ...

  5. jQuery 2.0.3 源码分析 Deferred概念

    JavaScript编程几乎总是伴随着异步操作,传统的异步操作会在操作完成之后,使用回调函数传回结果,而回调函数中则包含了后续的工作.这也是造成异步编程困难的主要原因:我们一直习惯于“线性”地编写代码 ...

  6. dubbo源码分析6-telnet方式的管理实现

    dubbo源码分析1-reference bean创建 dubbo源码分析2-reference bean发起服务方法调用 dubbo源码分析3-service bean的创建与发布 dubbo源码分 ...

  7. Thinkphp源码分析系列(九)–视图view类

    视图类view主要用于页面内容的输出,模板调用等,用在控制器类中,可以使得控制器类把表现和数据结合起来.下面我们来看一下执行流程. 首先,在控制器类中保持着一个view类的对象实例,只要继承自控制器父 ...

  8. Nmap源码分析(脚本引擎)

    Nmap提供了强大的脚本引擎(NSE),以支持通过Lua编程来扩展Nmap的功能.目前脚本库已经包含300多个常用的Lua脚本,辅助完成Nmap的主机发现.端口扫描.服务侦测.操作系统侦测四个基本功能 ...

  9. Orchard源码分析(1):Orchard架构

      本文主要参考官方文档"How Orchard works"以及Orchardch上的翻译.   源码分析应该做到庖丁解牛,而不是以管窥豹或瞎子摸象.所以先对Orchard架构有 ...

随机推荐

  1. JAVA面向对象-----成员内部类访问细节

    JAVA面向对象-–成员内部类访问细节 私有的成员内部类不能在其他类中直接创建内部类对象来访问. 如果内部类中包含有静态成员,那么java规定内部类必须声明为静态的访问静态内部类的形式:Outer.I ...

  2. ORACLE数据库管理常用查询语句

    /*查看表空间的名称及大小*/ SELECT t.tablespace_name, round(SUM(bytes / (1024 * 1024)), 0) ts_size FROM dba_tabl ...

  3. linuxsvn源代码版本库建立

    linuxsvn源代码版本库建立 下面就要建立代码的版本库做描述: 1.     安装svn版本服务器端 yum install subversion 从镜像下载安装svn服务器端,我们服务器已经安装 ...

  4. Linux jar包 后台运行

    Linux 运行jar包命令如下: 方式一: java -jar shareniu.jar 特点:当前ssh窗口被锁定,可按CTRL + C打断程序运行,或直接关闭窗口,程序退出 那如何让窗口不锁定? ...

  5. 关于查找iOS中App路径时所要注意的一个问题

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 免责申明:本博客提供的所有翻译文章原稿均来自互联网,仅供学习交 ...

  6. Dynamics CRM 安装Microsoft Dynamics CRM Reporting Extensions

    在装完CRM Server 后这个组件是必须安装的,但今天由于我的大意在客户安装生产环境时,告诉客户这个组件装在APP服务器上,导致客户安装时 SSRSInstance怎么都是空的,害的人家找了半天原 ...

  7. Android初级教程:单击事件的传递机制初谈

    以上仅是小试牛刀,后续有很多事件传递机制,继续探讨.

  8. SSH网上商城---使用ajax完成用户名是否存在异步校验

    小伙伴在上网的时候,需要下载或者观看某些视频资料,更或者是在逛淘宝的时候,我们都需要注册一个用户,当我们填写好各种信息,点击确定的时候,提示用户名已经存在,小编就想,为什么当我们填写完用户名的时候,她 ...

  9. GDAL书籍中删除数据勘误(C#语言)

    GDAL书籍中关于C#版本删除数据的时候,不能完全删除数据,由于我对C#不了解导致代码有点问题,非常感谢@Bingoyin指出并给出修改方案.此外对于栅格图像的删除.重命名,矢量数据的删除和重命名都有 ...

  10. java中遍历map的几种方法介绍

          喜欢用Java写程序的朋友都知道,我们常用的一种数据结构map中存储的是键值对,我们一般存储的方式是: map.put(key, value); 而提取相应键的值用的方法是: map.ge ...