制作一个类似苹果 VFL(Visual Format Language) 的格式化语言来描述类似 UIStackView 那种布局思路,并解析生成页面

730 阅读28分钟
原文链接: www.starming.com

[文章]制作一个类似苹果VFL(Visual Format Language)的格式化语言来描述类似UIStackView那种布局思路,并解析生成页面

在项目中总是希望页面上各处的文字,颜色,字体大小甚至各个视图控件布局都能够在发版之后能够修改以弥补一些前期考虑不周,或者根据统计数据能够随时进行调整,当然是各个版本都能够统一变化。看到这样的要求后,第一反应是这样的页面只能改成H5,或者尝试使用React Native来应对这种要求。不然仅仅通过下发json这种结构化数据只能够改改文字和颜色而无法满足。

既然UIStackView已经提供了一种既先进又简洁的布局思路,为何不通过制作一个类似VFL这样的DSL语言来处理布局。这样不就能够通过下发一串DSL字符串的方式来进行内容样式甚至布局的更换,不用跟版,还能使多版本统一。同时在端内直接用这样的DSL语言来写界面不光能够减少代码量易于维护,还能够很直观方便的看出整个界面布局结构。

AssembleView(组装视图)和PartView(零件视图)

在设计格式化语言之前需要对布局做个统一思想进行管理,在看了WWDC里关于UIStackView的介绍后感觉任何复杂的布局都能够通过这样一种组合排布再组合排布的思路特别适合用格式化语言来描述。于是我想出两个视图概念。

一个是AssembleView组合视图,专门用于对其PartView子视图进行排列,比如说是水平排列还是垂直排列,PartView是按照居中对齐还是居左等对齐方式,各个PartView之间间隔是多少。

PartView决定自己视图类型,内容,无固定大小的可以设置大小,同时AssembleView可以作为PartView被加入另一个AssembleView里进行排列,这样各种设计图都可以在初期通过拆解分成不同的AssembleView和PartView进行组合套组合布局出来。

格式化语言

接下来是如何通过格式化语言来描述AssembleView和PartView。“{}”符号里包含的是AssembleView的设置,“[]”符号里是PartView的设置,“()”里是他们的属性设置,“<>”可以将对象带入到设置里。下面举几个例子说明下。完整Demo放到了Github上:github.com/ming1016/ST…

三个星星水平对齐居中排列

h表示水平排列horizontal,c表示居中center,“[]”PartView会根据顺序依次添加排列,imageName属性能够指定本地图片 三个星星水平对齐居中排列

{
    hc(padding:30)
    [(imageName:starmingicon)]
    [(imageName:starmingicon)]
    [(imageName:starmingicon)]
}

AssembleView里套作为PartView的AssembleView的复杂情况

color可以指定文字颜色,font指定文字大小 AssembleView里套作为PartView的AssembleView的复杂情况

{
    ht(padding:10)
    [avatarImageView(imageName:avatar)]
    [
        {
            vl(padding:10)
            [(text:戴铭,color:AAA0A3)]
            [(text:Starming站长,color:E3DEE0,font:13)]
            [(text:喜欢画画编程和写小说,color:E3DEE0,font:13)]
        }
        (width:210,backColor:FAF8F9,backPaddingHorizontal:10,backPaddingVertical:10,radius:8)
    ]
}

给PartView设置背景色和按钮

设置背景色使用backColor,背景距离设置的PartView的内容间距通过backPaddingHorizontal属性设置水平间距,backPaddingVertical设置垂直间距,“<>”符号带入的button通过button属性设置。 给PartView设置背景色和按钮

[
    {
        hc(padding:4)
        [(imageName:starmingicon,width:14,height:10)]
        [(text:关注,font:16,color:FFFFFF)]
    }
    (height:36,backColor:AAA0A3,radius:8,backBorderWidth:1,backBorderColor:E3DEE0,backPaddingHorizontal:80,backPaddingVertical:10,button:)
]

AssembleView设置忽略约束的方法

水平排列时,通过ignoreAlignment属性设置忽略left约束,如果是垂直排列设置top忽略。 AssembleView设置忽略约束的方法

{
    hc(padding:5)
    [(text:STMAssembleView演示,color:E3DEE0,font:13)]
    [(imageName:starmingicon,width:14,height:10,ignoreAlignment:left)]
    [(text:Starming星光社,color:E3DEE0,font:13)]
}

将前面的视图组合成一个AssembleView

将前面的视图组合成一个AssembleView

ASS(@"{
    vc(padding:20)
    [%@(height:90)]
    [%@(height:36,backColor:AAA0A3,radius:8,backBorderWidth:1,backBorderColor:E3DEE0,backPaddingHorizontal:80,backPaddingVertical:10,button:)]
    [%@(height:25)]
    [%@(ignoreAlignment:top,isFill:1,height:16)]
}",midStr,followBtStr,centerStr,desStr)

AssembleView的属性

  • 当在“{}”里面第一个字母是v表示垂直排列vertical,是h表示水平排列horizontal
  • 第二个字母是c表示所有PartView居中对齐center,l表示居左对齐left,r表示居右对齐right,t表示居上对齐top,b表示居下对齐bottom。
  • padding:默认各个PartView的间距。

PartView的属性

如果不希望通过属性生成视图,可以通过在[后直接填入带入对象对应的key,然后再在()里设置属性。

PartView布局相关属性

  • width:UILabel和UIImage这样有固定大小的可以不用设置,会按照固定大小的来。
  • height:有固定大小的可以不用设置。
  • isFill:垂直排列时会将宽设置为父AssembleView的宽,水平排列时会将高设置为父AssembleView的高。
  • padding:设置后会忽略父AssembleView里设置的padding,达到自定义间距的效果。
  • partAlignment:可以自定义对齐方向,设置后会忽略父AssembleView里设置的对齐。值可填center,left,right,top,bottom。
  • ignoreAlignment:设置忽略的约束方向,在父AssembleView不需要由子PartView决定大小的情况下,可以通过打断某个方向约束来实现拆开排列的效果。值可填center,left,right,top,bottom。

PartView权重相关属性

  • crp:Compression Resistance Priority的设置,根据权重由低到高值可以设置为fit,low,high,required。对应的UILayoutPriority的分别是UILayoutPriorityFittingSizeLevel,UILayoutPriorityDefaultLow,UILayoutPriorityDefaultHigh,UILayoutPriorityRequired。
  • minWidth:对应NSLayoutRelationGreaterThanOrEqual,设置一个最小的宽
  • maxWidth:对应NSLayoutRelationLessThanOrEqual,设置一个最大的宽

PartView视图控件相关设置

通过以下属性即可生成对应的UILabel,UIImageView或者UIButton等控件视图,而不用特别指出需要生成哪种控件视图

  • text:设置文字内容
  • font:设置字体,可以带入一个UIFont,也可以直接设置一个字体大小,解析时会判断类型。
  • color:设置颜色,可以带入一个UIColor,也可以直接设置一个十六进制颜色,解析时会判断类型。
  • imageName:设置本地图片,值是本地图片名称。
  • image:带入一个UIImage。
  • imageUrl:设置一个网络图片的url地址,ps:目前需要通过<>来带入一个字符串。

PartView的通用设置

可以为PartView创建一个底部视图,并设置其样式。也可以添加一个UIButton设置UIControlStateHighlighted时的样式。

  • backColor:设置底部视图的颜色,可以带入一个UIColor,也可以直接设置一个十六进制颜色,解析时会判断类型。
  • backPaddingHorizontal:设置当前PartView视图距离底部视图top和bottom的间距。
  • backPaddingVertical:设置当前PartView视图距离底部视图left和right的间距。
  • backBorderColor:设置底部视图边框的颜色,可以带入一个UIColor,也可以直接设置一个十六进制颜色,解析时会判断类型。
  • backBorderWidth:设置底部视图边框宽。
  • radius:设置底部视图的圆角半径。
  • button:带入一个button。
  • buttonHighlightColor:设置button在UIControlStateHighlighted时的颜色,默认是透明度0.05的黑色。

解析格式化语言

解析过程的第一步采用扫描scanner程序将字符串按照分析符号表将字符流序列收集到有意义的单元中。

第二步将这些单元逐个归类到对应的类别中。比如解析到“()”里内容时就将其归类到对应的AssembleView的属性或者PartView的属性类别中。在归类过程中会出现PartView是AssembleView,这个Assemble里面又有这样作为PartView的AssembleView这样层层套的情况,所以需要采用类似引用计数方式保证在最后一个“}”符号结束时能将整个Assemble递归进行解析。

第三步将各个类别集合转换成对应原生代码从而生成对应的视图布局。

具体实现可以查看STMAssembleView.m文件。Github地址:github.com/ming1016/ST…

如何生成页面

生成页面需要实现格式化语言对应的原生代码,所有PartView的属性都会存放在STMPartMaker里,包括带入的自定义视图还有用于生成视图控件的属性等。PartView属性设置完成后会在STMPartView这个类中先决定对应的视图控件,并将STMPartMaker里的属性都设置上。实现代码可以查看STMPartView.m里的- (STMPartView *)buildPartView方法。

接下来STMAssembleView会在buildAssembleView时进行布局,具体实现代码可以查看STMAssembleView.m里的- (STMAssembleView *)buildAssembleView方法。

[文章]从ReactiveCocoa中能学到什么?不用此库也能学以致用

从知道ReactiveCocoa开始就发现对这个库有不同的声音,上次参加技术沙龙时唐巧对在项目中已全面使用FRP的代码家提出为什么这种编程模型出现了这么长时间怎么像ReactiveCocoa这种完全按FRP编写的库没能够流行起来这个问题。对这个问题的回答一般都是门槛高,解决方法就是培训和通过熟悉以前的代码来快速入门。其实在我学习的过程中也发现确实会有这个问题,不过就算是有这样那样问题使得ReactiveCocoa这样的库没法大面积使用起来,也不能错失学习这种编程思想的机会。

如果不用这样的库,能不能将这种库的编程思想融入项目中,发挥出其优势呢?答案是肯定的。

FRP全称Function Reactive Programming,从名称就能够看出来这个模型关键就是Function Programming和Reactive Programming的结合。那么就先从函数式编程说起。说函数式编程前先聊聊链式编程,先看看一个开源Alert控件的头文件里定义的接口方法的写法。

/*
 *  自定义样式的alertView
 *
 */
+ (instancetype)showAlertWithTitle:(NSString *)title
                           message:(NSString *)message
                        completion:(PXAlertViewCompletionBlock)completion
                       cancelTitle:(NSString *)cancelTitle
                       otherTitles:(NSString *)otherTitles, ... NS_REQUIRES_NIL_TERMINATION;

/*
 * @param otherTitles Must be a NSArray containing type NSString, or set to nil for no otherTitles.
 */
+ (instancetype)showAlertWithTitle:(NSString *)title
                       contentView:(UIView *)view
                       secondTitle:(NSString *)secondTitle
                           message:(NSString *)message
                       cancelTitle:(NSString *)cancelTitle
                       otherTitles:(NSArray *)otherTitles
                          btnStyle:(BOOL)btnStyle
                        completion:(PXAlertViewCompletionBlock)completion;

库里还有更多这样的组合,这么写是没有什么问题,无非是为了更方便组合使用而啰嗦了点,但是如果现在要添加一个AttributeString,那么所有组合接口都需要修改,每次调用接口方法如果不需要用Attribuite的地方还要去设置nil,这样会很不易于扩展。下面举个上报日志接口的例子。

@interface SMLogger : NSObject
//初始化
+ (SMLogger *)create;
//可选设置
- (SMLogger *)object:(id)obj;                        //object对象记录
- (SMLogger *)message:(NSString *)msg;               //描述
- (SMLogger *)classify:(SMProjectClassify)classify;  //分类
- (SMLogger *)level:(SMLoggerLevel)level;            //级别

//最后需要执行这个方法进行保存,什么都不设置也会记录文件名,函数名,行数等信息
- (void)save;

@end

//宏
FOUNDATION_EXPORT void SMLoggerDebugFunc(DCProjectClassify classify, DCLoggerLevel level, NSString *format, ...) NS_FORMAT_FUNCTION(3,4);
//debug方式打印日志,不会上报
#define SMLoggerDebug(frmt, ...) 
do { SMLoggerDebugFunc(SMProjectClassifyNormal,DCLoggerLevelDebug,frmt, ##__VA_ARGS__);} while(0)
//简单的上报日志
#define SMLoggerSimple(frmt, ...) 
do { SMLoggerDebugFunc(SMProjectClassifyNormal,SMLoggerLevelDebug,frmt, ##__VA_ARGS__);} while(0)
//自定义classify和level的日志,可上报
#define SMLoggerCustom(classify,level,frmt, ...) 
do { SMLoggerDebugFunc(classify,level,frmt, ##__VA_ARGS__);} while(0)

从这个头文件可以看出,对接口所需的参数不用将各种组合一一定义,只需要按照需要组合即可,而且做这个日志接口时发现后续维护过程中会增加越来越多的功能和需要更多的input数据。比如每条日志添加应用生命周期唯一编号,产品线每次切换唯一编号这样需要在特定场景需要添加的input支持。采用这种方式会更加易于扩展。写的时候会是[[[[DCLogger create] message:@"此处必改"] classify:DCProjectClassifyTradeHome] save]; 这样,对于不是特定场所较通用的场景可以使用宏来定义,内部实现还是按照前者的来实现,看起来是[DCLogger loggerWithMessage:@"此处必改"];,这样就能够同时满足常用场景和特殊场景的调用需求。

有了链式编程这种易于扩展方式的编程方式再来构造函数式编程,函数编程主要思路就是用有输入输出的函数作为参数将运算过程尽量写成一系列嵌套的函数调用,下面我构造一个需求来看看函数式编程的例子。

typedef NS_ENUM(NSUInteger, SMStudentGender) {
    SMStudentGenderMale,
    SMStudentGenderFemale
};

typedef BOOL(^SatisfyActionBlock)(NSUInteger credit);

@interface SMStudent : NSObject

@property (nonatomic, strong) SMCreditSubject *creditSubject;

@property (nonatomic, assign) BOOL isSatisfyCredit;

+ (SMStudent *)create;
- (SMStudent *)name:(NSString *)name;
- (SMStudent *)gender:(SMStudentGender)gender;
- (SMStudent *)studentNumber:(NSUInteger)number;

//积分相关
- (SMStudent *)sendCredit:(NSUInteger(^)(NSUInteger credit))updateCreditBlock;
- (SMStudent *)filterIsASatisfyCredit:(SatisfyActionBlock)satisfyBlock;

@end

这个例子中,sendCredit的block函数参数会处理当前的积分这个数据然后返回给SMStudent记录下来,filterIsASatisfyCredit的block函数参数会处理是否达到合格的积分判断返回是或否的BOOL值给SMStudent记录下来。实现代码如下

    //present
    self.student = [[[[[SMStudent create]
                       name:@"ming"]
                      gender:SMStudentGenderMale]
                     studentNumber:345]
                    filterIsASatisfyCredit:^BOOL(NSUInteger credit){
                        if (credit >= 70) {
                            self.isSatisfyLabel.text = @"合格";
                            self.isSatisfyLabel.textColor = [UIColor redColor];
                            return YES;
                        } else {
                            self.isSatisfyLabel.text = @"不合格";
                            return NO;
                        }

                    }];

    @weakify(self);
    [[self.testButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
        @strongify(self);

        [self.student sendCredit:^NSUInteger(NSUInteger credit) {
            credit += 5;
            NSLog(@"current credit %lu",credit);
            [self.student.creditSubject sendNext:credit];
            return credit;
        }];
    }];

    [self.student.creditSubject subscribeNext:^(NSUInteger credit) {
        NSLog(@"第一个订阅的credit处理积分%lu",credit);
        self.currentCreditLabel.text = [NSString stringWithFormat:@"%lu",credit];
        if (credit < 30) {
            self.currentCreditLabel.textColor = [UIColor lightGrayColor];
        } else if(credit < 70) {
            self.currentCreditLabel.textColor = [UIColor purpleColor];
        } else {
            self.currentCreditLabel.textColor = [UIColor redColor];
        }
    }];

    [self.student.creditSubject subscribeNext:^(NSUInteger credit) {
        NSLog(@"第二个订阅的credit处理积分%lu",credit);
        if (!(credit > 0)) {
            self.currentCreditLabel.text = @"0";
            self.isSatisfyLabel.text = @"未设置";
        }
    }];

每次按钮点击都会增加5个积分,达到70个积分就算合格了。上面的例子里可以看到一个对每次积分变化有不同的观察者处理的操作代码,这里并没有使用ReactiveCocoa里的信号,而是自己实现了一个特定的积分的类似信号的对象,方法名也用的是一样的。实现这个对象也是用的函数式编程方式。下面我的具体的实现代码

@interface SMCreditSubject : NSObject

typedef void(^SubscribeNextActionBlock)(NSUInteger credit);

+ (SMCreditSubject *)create;

- (SMCreditSubject *)sendNext:(NSUInteger)credit;
- (SMCreditSubject *)subscribeNext:(SubscribeNextActionBlock)block;

@end

@interface SMCreditSubject()

@property (nonatomic, assign) NSUInteger credit;
@property (nonatomic, strong) SubscribeNextActionBlock subscribeNextBlock;
@property (nonatomic, strong) NSMutableArray *blockArray;

@end

@implementation SMCreditSubject

+ (SMCreditSubject *)create {
    SMCreditSubject *subject = [[self alloc] init];
    return subject;
}

- (SMCreditSubject *)sendNext:(NSUInteger)credit {
    self.credit = credit;
    if (self.blockArray.count > 0) {
        for (SubscribeNextActionBlock block in self.blockArray) {
            block(self.credit);
        }
    }
    return self;
}

- (SMCreditSubject *)subscribeNext:(SubscribeNextActionBlock)block {
    if (block) {
        block(self.credit);
    }
    [self.blockArray addObject:block];
    return self;
}

#pragma mark - Getter
- (NSMutableArray *)blockArray {
    if (!_blockArray) {
        _blockArray = [NSMutableArray array];
    }
    return _blockArray;
}

Demo地址:github.com/ming1016/RA…

主要思路就是subscribeNext时将参数block的实现输入添加到一个数组中,sendNext时记录输入的积分,同时遍历那个记录subscribeNext的block的数组使那些block再按照新积分再实现一次输入,达到更新积分通知多个subscriber来实现新值的效果。

除了block还可以将每次sendNext的积分放入一个数组记录每次的积分变化,在RAC中的Signal就是这样处理的,如下图,这样新加入的subscirber能够读取到积分变化历史记录。

所以不用ReactiveCocoa库也能够按照函数式编程方式改造现有项目达到同样的效果。

上面的例子也能够看出FRP的另一个响应式编程的特性。说响应式编程之前可以先看看我之前关于解耦的那篇文章里的Demogithub.com/ming1016/De…,里面使用了Model作为连接视图,请求存储和控制器之间的纽带,通过KVO使它们能够通过Model的属性来相互监听来避免它们之间的相互依赖达到解耦的效果。

像上面的例子那样其实也能够达到同样的效果,创建一个Model然后通过各个Subject来贯穿视图层和数据层进行send值和多subscribe值的处理。

了解了这种编程模型,再去了解下ReactiveCocoa使用的三种设计模式就能够更容易的将它学以致用了,下面配上这三种贯穿ReactiveCocoa的设计模式,看这些图里的方法名是不是很眼熟。

ReactiveCocoa里面还有很多可以学习的地方,比如宏的运用,可以看看sunnyxx的那篇《Reactive Cocoa Tutorial [1] = 神奇的Macros》blog.sunnyxx.com/2014/03/06/…

[文章]检测iOS的APP性能的一些方法

首先如果遇到应用卡顿或者因为内存占用过多时一般使用Instruments里的来进行检测。但对于复杂情况可能就需要用到子线程监控主线程的方式来了,下面我对这些方法做些介绍:

Time Profiler

可以查看多个线程里那些方法费时过多的方法。先将右侧Hide System Libraries打上勾,这样能够过滤信息。然后在Call Tree上会默认按照费时的线程进行排序,单个线程中会也会按照对应的费时方法排序,选择方法后能够通过右侧Heaviest Stack Trace里双击查看到具体的费时操作代码,从而能够有针对性的优化,而不需要在一些本来就不会怎么影响性能的地方过度优化。

Allocations

这里可以对每个动作的前后进行Generations,对比内存的增加,查看使内存增加的具体的方法和代码所在位置。具体操作是在右侧Generation Analysis里点击Mark Generation,这样会产生一个Generation,切换到其他页面或一段时间产生了另外一个事件时再点Mark Generation来产生一个新的Generation,这样反复,生成多个Generation,查看这几个Generation会看到Growth的大小,如果太大可以点进去查看相应占用较大的线程里右侧Heaviest Stack Trace里查看对应的代码块,然后进行相应的处理。

Leak

可以在上面区域的Leaks部分看到对应的时间点产生的溢出,选择后在下面区域的Statistics>Allocation Summary能够看到泄漏的对象,同样可以通过Stack Trace查看到具体对应的代码区域。

开发时需要注意如何避免一些性能问题

NSDateFormatter

通过Instruments的检测会发现创建NSDateFormatter或者设置NSDateFormatter的属性的耗时总是排在前面,如何处理这个问题呢,比较推荐的是添加属性或者创建静态变量,这样能够使得创建初始化这个次数降到最低。还有就是可以直接用C,或者这个NSData的Category来解决github.com/samsoffes/s…

UIImage

这里要主要是会影响内存的开销,需要权衡下imagedNamed和imageWithContentsOfFile,了解两者特性后,在只需要显示一次的图片用后者,这样会减少内存的消耗,但是页面显示会增加Image IO的消耗,这个需要注意下。由于imageWithContentsOfFile不缓存,所以需要在每次页面显示前加载一次,这个IO的操作也是需要考虑权衡的一个点。

页面加载

如果一个页面内容过多,view过多,这样将长页面中的需要滚动才能看到的那个部分视图内容通过开启新的线程同步的加载。

优化首次加载时间

通过Time Profier可以查看到启动所占用的时间,如果太长可以通过Heaviest Stack Trace找到费时的方法进行改造。

监控卡顿的方法

还有种方法是在程序里去监控性能问题。可以先看看这个Demo,地址github.com/ming1016/De…。 这样在上线后可以通过这个程序将用户的卡顿操作记录下来,定时发到自己的服务器上,这样能够更大范围的收集性能问题。众所周知,用户层面感知的卡顿都是来自处理所有UI的主线程上,包括在主线程上进行的大计算,大量的IO操作,或者比较重的绘制工作。如何监控主线程呢,首先需要知道的是主线程和其它线程一样都是靠NSRunLoop来驱动的。可以先看看CFRunLoopRun的大概的逻辑

int32_t __CFRunLoopRun()
{
    __CFRunLoopDoObservers(KCFRunLoopEntry);
    do
    {
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources); //这里开始到kCFRunLoopBeforeWaiting之间处理时间是感知卡顿的关键地方

        __CFRunLoopDoBlocks();
        __CFRunLoopDoSource0(); //处理UI事件

        //GCD dispatch main queue
        CheckIfExistMessagesInMainDispatchQueue();

        //休眠前
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);

        //等待msg
        mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();

        //等待中

        //休眠后,唤醒
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);

        //定时器唤醒
        if (wakeUpPort == timerPort)
            __CFRunLoopDoTimers();

        //异步处理
        else if (wakeUpPort == mainDispatchQueuePort)
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()

        //UI,动画
        else
            __CFRunLoopDoSource1();

        //确保同步
        __CFRunLoopDoBlocks();

    } while (!stop && !timeout);

    //退出RunLoop
    __CFRunLoopDoObservers(CFRunLoopExit);
}

根据这个RunLoop我们能够通过CFRunLoopObserverRef来度量。用GCD里的dispatch_semaphore_t开启一个新线程,设置一个极限值和出现次数的值,然后获取主线程上在kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting两个状态之间的超过了极限值和出现次数的场景,将堆栈dump下来,最后发到服务器做收集,通过堆栈能够找到对应出问题的那个方法。

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    MyClass *object = (__bridge MyClass*)info;
    object->activity = activity;
}

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;
    lagMonitor->runLoopActivity = activity;

    dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
    dispatch_semaphore_signal(semaphore);
}

- (void)endMonitor {
    if (!runLoopObserver) {
        return;
    }
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
    CFRelease(runLoopObserver);
    runLoopObserver = NULL;
}

- (void)beginMonitor {
    if (runLoopObserver) {
        return;
    }
    dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore保证同步
    //创建一个观察者
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopAllActivities,
                                              YES,
                                              0,
                                              &runLoopObserverCallBack,
                                              &context);
    //将观察者添加到主线程runloop的common模式下的观察中
    CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);

    //创建子线程监控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //子线程开启一个持续的loop用来进行监控
        while (YES) {
            long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 30*NSEC_PER_MSEC));
            if (semaphoreWait != 0) {
                if (!runLoopObserver) {
                    timeoutCount = 0;
                    dispatchSemaphore = 0;
                    runLoopActivity = 0;
                    return;
                }
                //两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
                if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
                    //出现三次出结果
                    if (++timeoutCount < 3) {
                        continue;
                    }

                    //将堆栈信息上报服务器的代码放到这里

                } //end activity
            }// end semaphore wait
            timeoutCount = 0;
        }// end while
    });

}

有时候造成卡顿是因为数据异常,过多,或者过大造成的,亦或者是操作的异常出现的,这样的情况可能在平时日常开发测试中难以遇到,但是在真实的特别是用户受众广的情况下会有人出现,这样这种收集卡顿的方式还是有价值的。

堆栈dump的方法

第一种是直接调用系统函数获取栈信息,这种方法只能够获得简单的信息,没法配合dSYM获得具体哪行代码出了问题,类型也有限。这种方法的主要思路是signal进行错误信号的获取。代码如下

static int s_fatal_signals[] = {
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGSEGV,
    SIGTRAP,
    SIGTERM,
    SIGKILL,
};

static int s_fatal_signal_num = sizeof(s_fatal_signals) / sizeof(s_fatal_signals[0]);

void UncaughtExceptionHandler(NSException *exception) {
    NSArray *exceptionArray = [exception callStackSymbols]; //得到当前调用栈信息
    NSString *exceptionReason = [exception reason];       //非常重要,就是崩溃的原因
    NSString *exceptionName = [exception name];           //异常类型
}

void SignalHandler(int code)
{
    NSLog(@"signal handler = %d",code);
}

void InitCrashReport()
{
    //系统错误信号捕获
    for (int i = 0; i < s_fatal_signal_num; ++i) {
        signal(s_fatal_signals[i], SignalHandler);
    }

    //oc未捕获异常的捕获
    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}
int main(int argc, char * argv[]) {
    @autoreleasepool {
        InitCrashReport();
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

使用PLCrashReporter的话出的报告看起来能够定位到问题代码的具体位置了。

NSData *lagData = [[[PLCrashReporter alloc]
                                          initWithConfiguration:[[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll]] generateLiveReport];
PLCrashReport *lagReport = [[PLCrashReport alloc] initWithData:lagData error:NULL];
NSString *lagReportString = [PLCrashReportTextFormatter stringValueForCrashReport:lagReport withTextFormat:PLCrashReportTextFormatiOS];
//将字符串上传服务器
NSLog(@"lag happen, detail below:  %@",lagReportString);

下面是测试Demo里堆栈里的内容

2016-03-28 14:59:26.922 HomePageTest[4803:201212]  INFO: Reveal Server started (Protocol Version 25).
2016-03-28 14:59:27.134 HomePageTest[4803:201212] 费时测试
2016-03-28 14:59:29.262 HomePageTest[4803:201212] 费时测试
2016-03-28 14:59:30.865 HomePageTest[4803:201212] 费时测试
2016-03-28 14:59:32.115 HomePageTest[4803:201212] 费时测试
2016-03-28 14:59:33.369 HomePageTest[4803:201212] 费时测试
2016-03-28 14:59:34.832 HomePageTest[4803:201212] 费时测试
2016-03-28 14:59:34.836 HomePageTest[4803:201615] lag happen, detail below: 
 Incident Identifier: 73BEF9D2-EBE3-49DF-B95B-7392635631A3
CrashReporter Key:   TODO
Hardware Model:      x86_64
Process:         HomePageTest [4803]
Path:            /Users/daiming/Library/Developer/CoreSimulator/Devices/444AAB95-C393-45CC-B5DC-0FB8611068F9/data/Containers/Bundle/Application/9CEE3A3A-9469-44F5-8112-FF0550ED8009/HomePageTest.app/HomePageTest
Identifier:      com.xiaojukeji.HomePageTest
Version:         1.0 (1)
Code Type:       X86-64
Parent Process:  debugserver [4805]

Date/Time:       2016-03-28 06:59:34 +0000
OS Version:      Mac OS X 9.2 (15D21)
Report Version:  104

Exception Type:  SIGTRAP
Exception Codes: TRAP_TRACE at 0x10aee6f79
Crashed Thread:  2

Thread 0:
0   libsystem_kernel.dylib              0x000000010ec6b206 __semwait_signal + 10
1   libsystem_c.dylib                   0x000000010e9f2b9e usleep + 54
2   HomePageTest                        0x000000010aedf934 -[TestTableView configCell] + 820
3   HomePageTest                        0x000000010aee5c89 -[testTableViewController observeValueForKeyPath:ofObject:change:context:] + 601
4   Foundation                          0x000000010b9c2564 NSKeyValueNotifyObserver + 347
5   Foundation                          0x000000010b9c178f NSKeyValueDidChange + 466
6   Foundation                          0x000000010b9bf003 -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] + 1176
7   Foundation                          0x000000010ba1d35f _NSSetObjectValueAndNotify + 261
8   HomePageTest                        0x000000010aec9c26 -[DCTableView tableView:cellForRowAtIndexPath:] + 262
9   UIKit                               0x000000010c872e43 -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] + 766
10  UIKit                               0x000000010c872f7b -[UITableView _createPreparedCellForGlobalRow:willDisplay:] + 74
11  UIKit                               0x000000010c847a39 -[UITableView _updateVisibleCellsNow:isRecursive:] + 2996
12  UIKit                               0x000000010c87c01c -[UITableView _performWithCachedTraitCollection:] + 92
13  UIKit                               0x000000010c862edc -[UITableView layoutSubviews] + 224
14  UIKit                               0x000000010c7d04a3 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 703
15  QuartzCore                          0x000000010c49a59a -[CALayer layoutSublayers] + 146
16  QuartzCore                          0x000000010c48ee70 _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 366
17  QuartzCore                          0x000000010c48ecee _ZN2CA5Layer28layout_and_display_if_neededEPNS_11TransactionE + 24
18  QuartzCore                          0x000000010c483475 _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 277
19  QuartzCore                          0x000000010c4b0c0a _ZN2CA11Transaction6commitEv + 486
20  QuartzCore                          0x000000010c4bf9f4 _ZN2CA7Display11DisplayLink14dispatch_itemsEyyy + 576
21  CoreFoundation                      0x000000010e123c84 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 20
22  CoreFoundation                      0x000000010e123831 __CFRunLoopDoTimer + 1089
23  CoreFoundation                      0x000000010e0e5241 __CFRunLoopRun + 1937
24  CoreFoundation                      0x000000010e0e4828 CFRunLoopRunSpecific + 488
25  GraphicsServices                    0x0000000110479ad2 GSEventRunModal + 161
26  UIKit                               0x000000010c719610 UIApplicationMain + 171
27  HomePageTest                        0x000000010aee0fdf main + 111
28  libdyld.dylib                       0x000000010e92b92d start + 1

Thread 1:
0   libsystem_kernel.dylib              0x000000010ec6bfde kevent64 + 10
1   libdispatch.dylib                   0x000000010e8e6262 _dispatch_mgr_init + 0

Thread 2 Crashed:
0   HomePageTest                        0x000000010b04a445 -[PLCrashReporter generateLiveReportWithThread:error:] + 632
1   HomePageTest                        0x000000010aee6f79 __28-[SMLagMonitor beginMonitor]_block_invoke + 425
2   libdispatch.dylib                   0x000000010e8d6e5d _dispatch_call_block_and_release + 12
3   libdispatch.dylib                   0x000000010e8f749b _dispatch_client_callout + 8
4   libdispatch.dylib                   0x000000010e8dfbef _dispatch_root_queue_drain + 1829
5   libdispatch.dylib                   0x000000010e8df4c5 _dispatch_worker_thread3 + 111
6   libsystem_pthread.dylib             0x000000010ec2f68f _pthread_wqthread + 1129
7   libsystem_pthread.dylib             0x000000010ec2d365 start_wqthread + 13

Thread 3:
0   libsystem_kernel.dylib              0x000000010ec6b6de __workq_kernreturn + 10
1   libsystem_pthread.dylib             0x000000010ec2d365 start_wqthread + 13

Thread 4:
0   libsystem_kernel.dylib              0x000000010ec65386 mach_msg_trap + 10
1   CoreFoundation                      0x000000010e0e5b64 __CFRunLoopServiceMachPort + 212
2   CoreFoundation                      0x000000010e0e4fbf __CFRunLoopRun + 1295
3   CoreFoundation                      0x000000010e0e4828 CFRunLoopRunSpecific + 488
4   WebCore                             0x0000000113408f65 _ZL12RunWebThreadPv + 469
5   libsystem_pthread.dylib             0x000000010ec2fc13 _pthread_body + 131
6   libsystem_pthread.dylib             0x000000010ec2fb90 _pthread_body + 0
7   libsystem_pthread.dylib             0x000000010ec2d375 thread_start + 13

Thread 5:
0   libsystem_kernel.dylib              0x000000010ec6b6de __workq_kernreturn + 10
1   libsystem_pthread.dylib             0x000000010ec2d365 start_wqthread + 13

Thread 6:
0   libsystem_kernel.dylib              0x000000010ec6b6de __workq_kernreturn + 10
1   libsystem_pthread.dylib             0x000000010ec2d365 start_wqthread + 13

Thread 7:
0   libsystem_kernel.dylib              0x000000010ec6b6de __workq_kernreturn + 10
1   libsystem_pthread.dylib             0x000000010ec2d365 start_wqthread + 13

Thread 2 crashed with X86-64 Thread State:
   rip: 0x000000010b04a445    rbp: 0x0000700000093da0    rsp: 0x0000700000093b10    rax: 0x0000700000093b70 
   rbx: 0x0000700000093cb0    rcx: 0x0000000000001003    rdx: 0x0000000000000000    rdi: 0x000000010b04a5ca 
   rsi: 0x0000700000093b40     r8: 0x0000000000000014     r9: 0x0000000000000000    r10: 0x000000010ec65362 
   r11: 0x0000000000000246    r12: 0x00007fdaf5800940    r13: 0x0000000000000000    r14: 0x0000000000000009 
   r15: 0x0000700000093b90 rflags: 0x0000000000000202     cs: 0x000000000000002b     fs: 0x0000000000000000 
    gs: 0x0000000000000000 

[文章]竭尽全力的去解耦的一次实践,封装一个TableView和一些功能组合的控件

可以先看看这个Demo:github.com/ming1016/De…。从这个Demo里可以看到Controller和View还有Store的头文件里没有任何Delegate,Block回调,只有初始化和更新ViewModel的方法。所有这些控件,请求,ViewController和视图之间的联系都是通过ViewModel来进行的,而viewModel也不进行任何逻辑处理,只是简单的起到描述和默认值设置的作用。ViewController也被减轻的小得不能再小了,只需要初始化视图和Store即可。这也是我的一次尝试,看看如何利用KVO能够做到最大限度的解耦,和最大限度的减少代码和接口。

可以先看看以前代码最臃肿的地方在使用了新的思路后会变成怎么样,首先是ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self addKVO];
    [self buildConstraints];
    self.tbStore = [[TestTableStore alloc] initWithViewModel:self.tbView.viewModel];
}

可以看到里面仅仅做了添加KVO,布局控件和初始化Store的工作。

封装的TableView作为一个通用控件是不会去设置管理不同的Cell的,可以看看不用Delegate和Block是如何处理的。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    self.viewModel.tableViewIdentifier = smTableViewIdentifier;
    self.viewModel.tableView = tableView;
    self.viewModel.cellIndexPath = indexPath;
    return self.viewModel.cell;
}

我觉得这样应该很简化了。当触发到UITableView这个配置Cell的回调时,通过对ViewModel的键值的监听就能够在任何地方对Cell进行配置了,而不用通过繁琐的Delegate和Block来层层回调了。

除了这里外,其它地方也用同样的方法进行了处理,比如说对新出现消息提示点击使其消失只需要设置ViewModel里的isHideHintView的值的处理,还有对请求不同状态显示不同引导页,只要是以前需要通过接口和回调的全部干掉,用ViewModel去控制,下面可以看看我写的ViewModel中,我将KVO分成了View Side和Data Side,前者主要是响应视图方面的逻辑变化,后者Data Side是响应不同的动作来产生对数据不同的处理,其它就都是些关于样式和数据配置相关的了。

//---------------------------
//           KVO View Side
//---------------------------
@property (nonatomic, assign) BOOL isHideGuideView;             //是否显示guide view
@property (nonatomic, assign) BOOL isHideHintView;              //是否显示hint view
//下拉刷新上拉加载更多
@property (nonatomic, assign) SMTableRequestStatus requestStatus; //刷新状态
//TableView Delegate
//通用
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSString *tableViewIdentifier;
//Cell
@property (nonatomic, strong) NSIndexPath *cellIndexPath;
@property (nonatomic, strong) UITableViewCell *cell;
//CellHeight
@property (nonatomic, strong) NSIndexPath *cellHeightIndexPath;
@property (nonatomic, assign) CGFloat cellHeight;

//---------------------------
//          KVO Data Side
//---------------------------
@property (nonatomic, assign) SMTableRefreshingStatus dataSourceRefreshingStatus; //请求状态

纵观整个项目,头文件都很干净,唯一有方法需要参数的也就是ViewModel。这种完全面向对象思路的编程方式在需求经常变更的情况下优势就会慢慢显露出来,对吧。

[文章]为文章“深入剖析Auto Layout”做的slides

里面提到的Demo在:github.com/ming1016/Sh…