Oladipo Planet

无知和弱小不是生存的障碍,傲慢才是

薄荷App开发中用到的第三方库

这篇文章简述了我们在重构薄荷App时所采用的第三方库,这些库是一款功能完整的App必要的组成部分。 在这里只是简单讲述各个库的应用场景或实现原理,有些比较复杂的框架还请读者自行了解其设计思想和使用方法,恕不累述。

模块化的网页开发思考

这篇文章用以陈述现有购物功能的弊端,同时展望下移动端网页模块化的实践前景。分析场景为商品详情页。文章的思想和我之前一篇《评测框架简述》类同,只不过把平台从iOS移到了Web前端。

商品详情页是由Native和WebView组合而成的页面,Native部分包括上部分的轮播器,商品名称和单价销量。Web部分用以展示商品详情,包装信息以及可以跳转到Native的评价标签。 screenshot1

轮播器出于性能考虑用原生实现是可以理解的。但是食品名称单价两栏并不是Web页面就让人诧异了。因为原生实现并没有体现任何优势,反而不利于实时更新。比如我们今天想来个促销,把28.0划掉,旁边写上9.9,这样的事情原生就办不到了。

这些都不是最重要的。

最重要的是下面的静态页面(上图蓝框部分)是以很低效的纯手工方式编写的HTML代码。它存在以下缺点

  1. 低效
  2. 灵活性差,维护成本高
  3. 专业性强,不懂HTML的业务人员没有能力修改

为了将来有更大的发展空间,我们需要抛弃历史的包袱,重新构建新的实现方式。

数据源和模板分离

恰巧我朋友所在的堆糖公司实现了一套模块化的页面配置系统。就是将网页元素分割成一个个模块,每个模块有独立的数据源和展现方式,模块之间灵活组合,由模板引擎负责在浏览器端拼合和填充。最重要的是最终页面的生成是由不同技术的市场和运营的人完成,大大降低使用门槛,提高效果。

数据+模板—>前端模板引擎渲染—>HTML页面

这和由服务器渲染页面类似,只是把渲染过程移到了前端。这样做的好处有许多

  1. 模块之间独立灵活易组合
  2. 数据源和模板复用
  3. 维护成本低
  4. 服务器减少了渲染过程,性能提升
  5. 省流量,服务器只负责返回必要数据源,模块相关代码存放在客户端本地
  6. 使用门槛低,非技术人员也可以通过组合模块生成页面

下图是在这个体系中各个角色所承担的职责。 screenshot2

What to do next?

在这个系统中,所有的数据源都是人工输入的。可以理解为是一个静态模块化系统。那么动态模块化系统是什么样子的呢?我们可以由市场和运营的人来设置一些约束或查询条件,由服务器根据这些条件计算得出相应的数据源。现阶段我们更需要静态模块化的功能,之后再根据需求来决定是否跟进动态的版本。

关于产品的一点思考

任务系统

Elevate是一款品质极高的学习App,从听说读写各个角度来提高个人的学习能力。Peak则是侧重从游戏中提高人的思维敏捷性。他们有几个典型的特点。

  1. 循序渐进的任务难度,逐渐探知个体的极限。
  2. 任务不多,相互独立。容易利用碎片化时间。
  3. 形式种类多样,适应不同人群。
  4. 有较科学的评级。

个人认为,凡是脱离舒适区的行为,对大部分人来说不会是好玩的事情,比如学习比如减肥。所以,与其说把任务系统做得好玩,不如说做得较容易获得满足感,荣誉感,对未来能有所期待。就好比打游戏,一味的打怪必然是乏味的,之所以要不断去打,就是希望能够在升级之后能够有新的能力,新的套装,新的副本可以尝试。

任务的循序渐近还有一个好处,就是容易养成习惯。就比如拿我用Elevate来说吧,其实每天学英语心里上是有点抵触的,但是好不容易坚持了一段时间,说中断就中断,于情于理都不忍心,所以还是会时不时进行练习。任务系统是一个可以抽离出来独立的系统。需要综合以上几点,并拥有适当的等级制度,奖励措施,营造良好的竞争氛围。需要做的工作还是很多的。

混合开发

  1. 开发效率高
  2. 跨平台代码复用
  3. 验证想法,快速迭代
  4. 易于维护,线上实时修改

案例1:H5开发一个展示丰富的引导页只需要一个小时,然后就可以应用于iOS和安卓平台。而在iOS或安卓上实现此功能花费不止一小时,且重复开发的时间人力成本不止两倍。

案例2:产品经理在构想新的功能时候,没有经过市场验证之前往往过度设计,逻辑功能过于复杂。上线反馈不佳后不再跟进该功能开发,既造消耗大量人力物力浪费资源,同时代码的复杂度增加维护变得更困难。利用H5进行高效开发,一步一步迭代验证功能,监测用户使用情况,实时线上调整。功能反响好就可以原生迅速跟进。

混合开发框架需要具备的几点功能

  1. 线上更新机制。增量更新,安全验证。
  2. 完善的本地控制,充分利用Native优势,实现页面跳转等基本操作。
  3. 灵活的线上控制,方便进行A/B测试,线上配置调控。

weakify和strongify探究

@weakify和@strongify是一组非常简洁搭配使用的宏,用来避免因循环引用而导致内存泄露。由开源项目libextobjc提供,被ReactiveCocoa广泛应用而进一步被熟知。

由于之前对宏不甚了解,看这两个宏的实现时非常头大。好在猫神的宏定义的黑魔法 - 宏菜鸟起飞手册给了非常大的帮助,是篇非常好的入门文章,这里就不再累述了。优雅的宏能够帮我们节省工作量,实乃居家旅行之必备神器。

@weakiy和@strongify本质上是对传统方法的简化和强化

传统的写法,获取self类型,声明weakSelf的变量并在最前面设置weak属性。

创建你的CocoaPod库简明教程

pod lib create [pod name]

基于模板创建Pod库,会询问一些基本问题,比如是否提供Example,是否提供测试等。执行后会生成一系列文件,其中Pod文件夹中放入你的代码和资源。Example中提供相应的样例代码。

在Example文件夹下pod install或pod update来引入或更新你的代码库。之后就是漫长的编码测试阶段。

coding—->debuging—>testing-
^                         |
|——————<——————<——————————-|

完成代码后,开始准备提交信息。在.podspec中完善你的库信息,包括项目描述以及存放代码的github地址。添加Travis CI集成化测试(调用代码Xcode的测试框架)。测试通过的代码在github页面会显示build passing

将代码发布到github上,打Tag并推送到远程仓库。Podspec文件中的version对应git中的tag,在更新时候需要记得统一。

git add -A && git commit -m “Release 0.0.1.”
git tag ‘0.0.1’
git push —tags

通过pod lib lint检测你的Podspec信息是否正常。使用—verbose查看详细。pod spec lint会联网检测你的版本库和tag状态。

最后注册并发布到CocoaPod,稍等片刻就可以在上面查到你的开源库了。

pod trunk register
pod trunk push

CATransition初探

本周的工作中有一个替换UINavigationBar标题字符串的小需求。为了使字符串的替换更自然,我找了利用CATransition实现动画自然过渡的方法,新字符串淡出旧字符串淡出。

薄荷TimeLine的优化

##简介

薄荷APP是国内最受欢迎的健康减肥APP,是由上海薄荷信息科技有限公司创立,是中国领先的体重管营商。薄荷科技建立了中国最大最活跃的在线减肥平台已服务上百万的减肥用户。

Timeline是薄荷app主要的展示页面,由于历史原因,一直卡顿不理想。我从一开始就希望能改进这块性能及UI,给用户提供极致的体验。对于性能的优化也一直是我感兴趣的方向之一。现在终于有机会接触这块功能,便饶有兴致得进行一番研究并归纳总结,以便为之后的版本打好基础。

###工具 Xcode,Instruments-Time Profiler,KMCGeigerCounter,My Eye

##案例

###1.无意义的IO消耗 这个问题的发现一度让我非常震惊。代码中充斥着类似这样的调用:

[[SQUtils getValueFromSettingPlist:@“leftSpace.width”] floatValue];

这个方法的作用是读取配置文件中的属性信息,包括Timeline和Cell的背景色,字体大小和颜色,间距等信息。方法本身没有做任何缓存,每次取值都需要经过读取、解析、拆分和取值的过程。

+ (id) getValueFromSettingPlist:(NSString*)key{
		NSString *localizedPath = [[NSBundle mainBundle] pathForResource:kSQScrollSettingsFilename ofType:@“plist”];
		NSData *plistData = [NSData dataWithContentsOfFile:localizedPath];
		id plist = [NSPropertyListSerialization propertyListFromData:plistData mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&plistError];
		NSArray* keys = [key componentsSeparatedByString:@“.”];
		NSDictionary* dict = [NRSimplePlist valuePlist:kSQScrollSettingsFilename withKey:[keys objectAtIndex:0]];
		return dict[keys.lastObject];
}

这一方法本身没有什么问题,但是在Timeline的场景下,频繁创建Cell时IO操作的频率简直令人发质。更甚者颜色的ARGB属性都分次读取,我的下巴都要掉下来了。

SQRGBACOLOR(
[[SQUtils getValueFromSettingPlist:@“CellBackground.color-red”] floatValue],
[[SQUtils getValueFromSettingPlist:@“CellBackground.color-green”] floatValue],
[[SQUtils getValueFromSettingPlist:@“CellBackground.color-blue”] floatValue],
[[SQUtils getValueFromSettingPlist:@“CellBackground.color-alpha”] floatValue]);

如果没有特殊的需求,这些属性完全可以硬编码到源文件里。如果需求方期望保留配置文件以便将来在线动态更改,那么应当进行缓存plist对象,避免频繁IO操作。再进一步,可以使用单例,使各UIColor、UIFont的对象只构造一次,降低了构造的频率。

###2.异步处理耗时操作

在TimeLine的正文中,往往会有可跳转的超链接或标签。传统的UILabel并不支持这些属性,可以通过NIAttributedLabel、TTTAttributedLabel等第三方库实现。这些库都是用官方名为NSDataDetector的类实现链接解析操作。解析是个比较耗时的操作,因此如NIAttributedLabel提供了异步处理的操作,解析完成后更新相关试图。

- (void)_deferLinkDetection {
  if (!self.detectingLinks) {
    self.detectingLinks = YES;

    NSString* string = [self.mutableAttributedString.string copy];
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      NSArray* matches = [self _matchesFromAttributedString:string];
      self.detectingLinks = NO;

      dispatch_async(dispatch_get_main_queue(), ^{
        self.detectedlinkLocations = matches;
        self.linksHaveBeenDetected = YES;

        [self attributedTextDidChange];
      });
    });
  }
}

我们的代码被没有利用好这样的异步功能,同步解析为降低滑动的流畅性又作出了应有的“贡献”。

###3.缓存中无谓的性能消耗 缓存本来是性能提升的主要手段,但是不当的实现不能充分发挥缓存的性能优势。

//消耗占比 118*0.81/2352 = 4% 
- (void)cacheCells:(UITableViewCell*)cell{
    if (!_cellCaches) {
        _cellCaches = [[NSMutableArray alloc] init];
    }
    
    if ([_cellCaches indexOfObject:cell] == NSNotFound) {
        [_cellCaches addObject:cell];
    }

    if ([self.cellCaches count] > 20) {
        [self.cellCaches removeObjectAtIndex:0];
    }
}

此处缓存的容量为20,超出之后就会删除最前排的元素。这样的实现在TimeLine下拉的场景中,必然会频繁创建新元素,每缓存一次都会调用removeObjectAtIndex方法。而该方法的弊端如官方文档所述To fill the gap, all elements beyond index are moved by subtracting 1 from their index.不停移动数组中元素,从而导致无谓的消耗。

//消耗占比 129*0.81/3494 = 3%
- (void)cacheCells:(UITableViewCell*)cell {
    static NSInteger index = 0;
    if (!_cellCaches) {
        _cellCaches = [[NSMutableArray alloc] initWithCapacity:20];
//Edited on 2015-4-18,之前没有加初始化
		for (int i=0;i<20;i++){
				cellCaches[i] = [NSNull null];
			}
    }

    if ([_cellCaches indexOfObject:cell] == NSNotFound) {
        _cellCaches[index] = cell;
        index = index == 19 ? 0 : ++index;
    }
}

我使用了一个会循环移动的索引,指向当前存储得位置,来避免removeObjectAtIndex带来的的开销。由对比结果可以看出,尽管降低1%,但仍有3%的时间被消耗。从分析工具中可以看到,是被踢出缓存的对象在调用cxx_destruct方法进行内存释放。相关知识点可以参考这篇文章

###4.cell的复用

在iOS开发实践中通常对UITableView的cell采取复用机制。因为对象的创建通常伴随着一定的开销,当cell过于复杂时尤甚。在原来的实现中,我们可以看到复用代码的使用。但是由于同时使用自定义的cache(第三点所讲),导致实际上dequeueReusableCellWithIdentifier是没有任何意义的。自定义的cell的配置是在初始时进行,并不支持复用。如果不是在cache中就得重新创建。因而,在不断下拉的过程中,实际上是都是在创建新的cell。

    ONESQScrollCellViewMixVotesWithDestroyButton *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    ONESQCellModelExtra* model = [self.cells objectAtIndex:indexPath.row];
    if (model) {
        cell = (ONESQScrollCellViewMixVotesWithDestroyButton*)[self loadCellFromCache:model.index];
    }

所以,为了实现cell的复用,我必须重构cell使其可配置。在初始化时通过buildContentView创建Cell中的视图元素。新增的configure方法针对model实现视图的配置,包括UILabel高度的计算和ImageView宽高的设置和相关内容的更新等。cell可配置之后,就可以很自然地运用系统的复用机制。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  static NSString *CellIdentifier = @“SQTimelineCell”;
	ONESQScrollCellViewMixVotesWithDestroyButton *cell = (ONESQScrollCellViewMixVotesWithDestroyButton *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
  ONESQCellModelExtra* model = [self.cells objectAtIndex:indexPath.row];
	if (cell == nil) {
        cell = [[ONESQScrollCellViewMixVotesWithDestroyButton alloc] init:model reuseIdentifier:CellIdentifier];
        cell.delegate = self;
	}
  [cell configure:model];
	return cell;
}

在实现了真正的cell复用后,第三点中的cache重要性就大大降低。最终被我弃用。

####复用所引发的问题 复用后TimeLine的流畅性有了显著的提升。但是在快速滑动时,出现了莫名的崩溃。经过分析得出是由第二条中的detectingLinks引起的。detectingLinks针对富文本字符串进行分析,是一个异步过程。由于Timeline的快速滑动,复用的cell被新的字符串替代,而分析完成后需要根据分析结果对字符串颜色字体等进行渲染。由于前后字符串的不一致,最终出现了outOfRange的溢出异常。

###5.弃用autoLayout 为了使cell能够适配不同尺寸屏幕,在重构过程中我使用了自己比较熟悉和依赖的autoLayout布局框架Masonry。然后自己没有认识到autoLayout的性能为题。autoLayout的本质是计算各视图之间的一元二次方程,而当view嵌套过多时,计算的时间是成指数级增长,文章给出了相关的测试数据。

在遇到autoLayout的瓶颈后,立即转向最传统也最高效的setFrame布局方式。最后用KMCGeigerCounter对比两种实现。使用autoLayout的情形,丢帧率达到20%左右。而直接通过计算和setFrame的方式,丢帧率很少超过10%。性能的提升可见一斑。AutoLayout是把双刃剑,我们需要根据不同的场景来进行适当取舍。

###总结 经过一周左右的重构后,TimeLine终于可以流畅地呈现。这其中遇到的性能问题都可以尝试用缓存、异步处理、cell复用以及弃用低效的实现,这些都是很基础的方案。性能优化是一个权衡的过程,比如复用了cell就不再适用detectingLinks方法。比如为了性能弃用简单的autoLayout实现。

尝试使用优秀的高性能框架比如Facebook的AsyncDisplayKit,其异步渲染工作,能更容易应对复杂的场景,是开发者的福音。

优化是一个永不止步,除了性能还有UI优化、业务逻辑优化等多方面。有些能看出来,有些则不那么明显。希望在之后的编码过程中能进一步优化自己的代码。开发优质的产品。

GVUserDefaults的源码研究

###简介 GVUserDefaults是一个封装NSUserDefaults以达到仅仅通过使用property的set和get方法就可以实现本地存储。GVUserDefaults可以通过category对property进行分类,非常方便管理和使用。比如在GVUserDefaults+user.h下声明的property是用户本地数据,而GVUserDefaults+event.h下的就是事件相关的本地数据。

###概念梳理 GVUserDefaults的代码不多,核心功能用C语言和runtime相关的函数实现。因为对这些概念和函数不太了解,所以先梳理下。

####C语言函数

  1. char *strdup(const char *s)复制字符串到新开辟的内存空间并返回新位置的指针。
  2. char *strsep(char **stringp, const char *delim)作用类似NSString的componentsSeparatedByString,但是只进行一个分割,返回值指向分隔符之前的字符串位置,参数stringp更新至分隔符后的字符串位置。
  3. char *strstr(const char *haystack, const char *needle)找到haystack中第一次出现needle的字符串的指针位置。

####objc/runtime相关概念 1. SEL表示函数Selector的一个字符串指针,指代函数名。唯有通过sel_registerName注册的方法才能拥有Selector。编译器会根据源码编译时自动生成相应函数的Selector。在运行时,我们可以手动调用生成。 2. IMP是指向函数的指针,该函数接收self和Selector参数。 3. class_copyPropertyListproperty_getNameproperty_getAttributes分别为获取类的property列表,名称和属性。属性由字符串组成,描述了property的声明时所有的特性。官方文档给出了相关的格式和实例。 4. sel_registerName用C的字符串注册一个方法,返回该方法的Selector。 5. class_addMethod为一个类增加方法,提供Selector,IMP以及参数类型字符串作为参数。参数类型字符串标示着返回值和参数的数据类型。

###实现流程

GVUserDefaults的核心方法是generateAccessorMethods,在初始化时调用。通过遍历当前类的property列表,与事先实现的存取方法(见下)匹配,动态添加property的setter和getter方法。为了能够知道每个Selector对应存储的Key,通过一个叫mapping的字典以setter和getter的字符串作为Key,property的名称作为Value来进行索引。

如下为一个整型的setter和getter的方法实现。同样类型的property指向相同的IMP,通过SEL名称找到相关的Key以实现存取。

static void integerSetter(GVUserDefaults *self, SEL _cmd, int value) {
NSString *key = [self defaultsKeyForSelector:_cmd];
[self.userDefaults setInteger:value forKey:key];
}

static int integerGetter(GVUserDefaults *self, SEL _cmd) {
NSString *key = [self defaultsKeyForSelector:_cmd];
return (int)[self.userDefaults integerForKey:key];
}

###小结 GVUserDefaults的源码非常简短,却实现了一个令人眼前一亮的功能。自此我们便可以方便存取,再也不用多写琐碎的代码和记那些搞不灵清的KeyValue。这篇文章对源码进行了简短的剖析。当然读万卷书不如看源码,希望通过这篇文章的索引可以为对runtime陌生的同志提供帮助。

Swift中的String

在Swift的项目中涉及String操作时,会发现一些让人无所适从的变化[NSString length]方法不见了,substringWithRange的参数变成了Range<String.Index>等。

书写规范的Git提交说明

一直以来我在使用Git进行提交时,书写信息都过于随意。这对于个人来说可能影响不大,但在团队合作中让别人不通过阅读代码就能理解你的意图,对提高工作效率是非常重要的。因此我希望在这方面可以做一些改进,翻阅了相关文章,进行了一些总结。