iOS 源代码分析 --- MBProgressHUD

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 最简单的一组方法


1

2

3

4

5

6

7

+ (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 的一些属性设置一系列的默认值。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

- (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 显示到屏幕上。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

- (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:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

- (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 的出现添加合适的动画。


1

2

3

4

5

6

7

8

typedef NS_ENUM(NSInteger, MBProgressHUDAnimation) {

    /** Opacity animation */

    MBProgressHUDAnimationFade,

    /** Opacity + scale animation */

    MBProgressHUDAnimationZoom,

    MBProgressHUDAnimationZoomOut = MBProgressHUDAnimationZoom,

    MBProgressHUDAnimationZoomIn

};

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

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


1

self.showStarted = [NSDate date]

+ hideHUDForView:animated:


1

2

3

4

5

6

7

8

9

+ (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 的实例。


1

2

3

4

5

6

7

8

9

+ (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 回调。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

- (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 也提供一些其他的便利方法实现这一功能:


1

2

3

- (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。


1

2

3

4

5

6

7

8

9

10

11

12

- (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。


1

2

3

4

5

6

7

8

9

10

- (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 之后, 会在新的线程中调用方法。


1

2

3

4

5

6

7

8

9

10

11

12

- (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。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

#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 生成了一系列的属性。


1

2

3

4

- (NSArray *)observableKeypaths {

    return [NSArray arrayWithObjects:@"mode", @"customView", @"labelText", @"labelFont", @"labelColor",

            @"detailsLabelText", @"detailsLabelFont", @"detailsLabelColor", @"progress", @"activityIndicatorColor", nil];

}

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


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

- (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 的更新。

End

MBProgressHUD 由于是一个UI的第三方库,所以它的实现还是挺简单的。

时间: 2024-08-02 13:22:00

iOS 源代码分析 --- MBProgressHUD的相关文章

IOS逆向分析——GL脚本的提取

总结:要逆一个程序必须清楚地知道程序的结构和常用的API函数,不清楚一个程序而去逆出结果是不可能滴 首先是glsl脚本运行的全过程,第一步是为shader的运行创建一个容器GLuint glCreateProgram(void),第二步是把编译好的shader附加到程序void glAttachShader(GLuint program, GLuint shader),编译好的shader可以是多个所以第二步可以重复多步把每一个编译好的一一附加到程序,顶点shader和像素shader一一成对,

Android应用Activity、Dialog、PopWindow、Toast窗体加入机制及源代码分析

[工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处.尊重劳动成果] 1 背景 之所以写这一篇博客的原因是由于之前有写过一篇<Android应用setContentView与LayoutInflater载入解析机制源代码分析>.然后有人在文章以下评论和微博私信中问我关于Android应用Activity.Dialog.PopWindow载入显示机制是咋回事,所以我就写一篇文章来分析分析吧(本文以Android5.1.1 (API 22)源代码为基础分析),以

iOS Crash 分析 符号化崩溃日志

参考: http://blog.csdn.net/diyagoanyhacker/article/details/41247367 http://blog.csdn.net/diyagoanyhacker/article/details/41247389 http://blog.csdn.net/diyagoanyhacker/article/details/41247411 http://www.cnblogs.com/smileEvday/p/Crash1.html 未符号化的崩溃日志就象一

iOS Crash 分析(文三)- 符号化崩溃日志

iOS Crash 分析(文三)- 符号化崩溃日志 未符号化的崩溃日志就象一本天书,看不懂,更别谈分析崩溃原因了.所以我们在分析日志之前,要把日志翻译成我们可以看得懂的文字.这一步我们称之为符号化. 在iOS Crash分析(文一)中已经提到过符号化的两种方式: 1.利用Xcode符号化 2.利用symbolicatecrash脚本符号化 其实这两种分析方式都使用了同一个工具符号化:***atos***. atos是苹果提供的符号化工具,在Mac OS系统下默认安装. 使用***atos***符

MonkeyRunner源代码分析之启动

在工作中由于要追求完毕目标的效率,所以很多其它是强调实战.注重招式.关注怎么去用各种框架来实现目的.可是假设一味仅仅是注重招式.缺少对原理这个内功的了解,相信自己非常难对各种框架有更深入的理解. 从几个月前開始接触ios和android的自己主动化測试.原来是本着只为了提高測试团队工作效率的心态先行作浅尝即止式的研究,然后交给測试团队去边实现边自己研究.最后由于各种原因果然是浅尝然后就止步了,而自己终于也离开了上一家公司. 换了工作这段时间抛开全部杂念和曾经的困扰专心去学习研究各个框架的使用,逐

Java中arraylist和linkedlist源代码分析与性能比較

Java中arraylist和linkedlist源代码分析与性能比較 1,简单介绍 在java开发中比較经常使用的数据结构是arraylist和linkedlist,本文主要从源代码角度分析arraylist和linkedlist的性能. 2,arraylist源代码分析 Arraylist底层的数据结构是一个对象数组.有一个size的成员变量标记数组中元素的个数,例如以下图: * The array buffer into which the elements of the ArrayLis

EarthWarrior3D游戏ios源代码

这是一款不错的ios源代码源代码,EarthWarrior3D游戏源代码. 而且游戏源码支持多平台. 适用于cocos v2.1.0.0版本号 源代码下载: http://code.662p.com/view/10968.html <ignore_js_op> <ignore_js_op> 具体说明:http://ios.662p.com/thread-2508-1-1.html

转:RTMPDump源代码分析

0: 主要函数调用分析 rtmpdump 是一个用来处理 RTMP 流媒体的开源工具包,支持 rtmp://, rtmpt://, rtmpe://, rtmpte://, and rtmps://.也提供 Android 版本. 最近研究了一下它内部函数调用的关系. 下面列出几个主要的函数的调用关系. RTMPDump用于下载RTMP流媒体的函数Download: 用于建立网络连接(NetConnect)的函数Connect: 用于建立网络流(NetStream)的函数 rtmpdump源代码

Kafka SocketServer源代码分析

Kafka SocketServer源代码分析 标签: kafka 本文将详细分析Kafka SocketServer的相关源码. 总体设计 Kafka SocketServer是基于Java NIO来开发的,采用了Reactor的模式,其中包含了1个Acceptor负责接受客户端请求,N个Processor负责读写数据,M个Handler来处理业务逻辑.在Acceptor和Processor,Processor和Handler之间都有队列来缓冲请求. kafka.network.Accepto