Oladipo Planet

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

评测框架简述

###动机

最近的开发需求是写一个评测的功能,根据用户回答一系列的问题,最终生成评测结果。题型包括选择题和输入题,但拥有各样的展示方式。一个答题页会放一道或两道题,根据选择的不同会跳转不同到题目。原先的实现方式是每一种答题页独立实现,相似的答题页和组件实现复用,也能基本满足需求。但是我想做一个拓展性更强的足以面对更多变化的框架,所以尝试性进行重构。

###组件化 组件化的概念就是将一个页面的视图进行拆分,通过布局文件使其能自由组合在一起。如同HTML通过标签铺陈一样,自上而下构建所解析的视图,并且拥有相应的属性。

    [ //JSON格式的Layout文件
     {"view":"TitleLabel","text": "您的性别和生日?"},
     {"view":"ChoiceView","questionid":"0"},
     {"view":"PickView","questionid":"1"},
     {"view":"BiChoiceView"},
    ]

HTML布局的高度是无限制的,可以上下滚动。而我们需要在限定的高度上进行布局,所以为了达到适配效果构建了一个BaseView。它有一个flexibleHeight的属性标明这个View是否固定高度,不固定视图的高度=屏幕高度-固定视图的高度和,最终会填满整个屏幕。这些限定是通过iOS的autoLayout实现的。 lceva screenshot

###组件层次 偏于展示的视图UILabel,UIImageView通过BaseView进行了一层封装,使其可以自适应高度。QuestionView是问题类视图的基类,它在原有增加了loadQuestionroutePage等方法,来处理相关的加载问题和跳转位置等操作。SelectableView对选择题视图的封装,通过KVC和KVO来处理选项之间的变化。 lcevastcut screenshot

###题目结构

起初布局和题目是放在一个文件里,随着布局和题目属性的增加变得相当难以维护。所以将两者独立出来。以questionid来索引到相关的题目。题型的不同导致题目文件没有统一的标准,这部分的解析和加载就交给各自的视图去完成。以下代码中表示,编号为0的题目是一个二选一的题型,选择爷们儿软妹子,并配有相应的图片和不同的跳转项。

    "0":{
        "type":"bichoice",
        "text":["爷们儿","软妹子"],
        "imgs":["male.png","female.png"],
      	  ...
        "nextpage":["page2","page3"],
        "prevpage":"page0"
    },

###总结

在准备初期我已经考虑过组件化的可行性,并花了2天时间成功搭起了框架。随着深入细节会出现一些未曾考虑过或者考虑不完善的状况,这时就需要及时调整和重构。编码量也比预期的要多出许多,之后也一定会有不小的调整。好在一切都在掌控范围内。当框架有血有肉后,就能呈现较好的效果,如图。 screenshot

Swift1.2中的新特性小记

##引入集合概念 集合是一个没有重复的无序元素集,以下是一些基本操作

var set1 = Set(["a", "b", "c", "d"]) //集合创建
set1.insert("e") //集合插入
set1.remove("a") //删除操作
var set2 = Set(["b", "c", "f"])
set1.subtract(set2) //集合差集
//["e", "d"]
set1.union(set2) //集合并集
//["b","e","f","d","c"]

##if let语句的优化 之前由于optional特性的引入,在做判断时需要层层unwrap,使得代码嵌套严重,影响整洁。

if let name = validateName(nameTextField.text) {
    if !name.isEmpty {
        if let age = validateAge(ageTextField.text) {
            if age > 13 {
                
                // 做相应操作
            }
        }
    }
}

在优化if let之后,能够将判断集中处理,代码瞬间精简许多

if let name = validateName(nameTextField.text),
       age = validateAge(ageTextField.text)
    where age > 13 && !name.isEmpty {
    
                // 做相应操作
}

##as!强制类型转换 强制类型转换体现了苹果对类型安全的重视,相比as?转换失败返回nil,as!转换失败会引发运行时错误。这就需要程序员在转换时引起注意。

class A{}
class B:A{}

var a:A? = A()
var b:B? = a as? B //此时b为nil

var c:B? = a as! B //运行时错误,程序崩溃

另外在从objective-C到Swift基本类型转换时需要显示转换,反之不用

var nsstr1:NSString = NSString(string: "hello")
var str1:String = nsstr1 as String

var nsarray:NSArray = NSArray()
var array:[AnyObject] = nsarray as [AnyObject]

var nsdict:NSDictionary = NSDictionary()
var dict:Dictionary = nsdict as Dictionary

Xcode一般会给出类型转换的提示,但是由于beta版的不稳定,导致SourceKit崩溃而无法提示,导致编译过程中出现segment fault问题。以下是出错代码段。

class func changePassword(oldpassword: String, newpassword: String, doneAction:()->()) {
    
    API_USERS_CHANGE_PASSWORD.cancel()
    var api: API_USERS_CHANGE_PASSWORD = API_USERS_CHANGE_PASSWORD()
    
    //以参数形式构造的字典在Xcode6.3 beta2中会导致SourceKit崩溃
    var req = ["old_password": oldpassword, "new_password": newpassword] as NSDictionary
    //api.req是objective-C经bridge转换成代码,原objective-C中api.req接收NSDictionary的赋值,但是经bridge转换后,需要Dictionary值。
    api.req = req //api.req(Dictionary) = req(NSDictionary) 在赋值时没有编辑器错误警告,导致执行编译,最后出现编译不通过情况
    
    //正确的写法如下,是不需要在通过NSDictionary中转,直接赋值即可
    api.req = ["old_password": oldpassword, "new_password": newpassword]
    api.whenSucceed = {
        [unowned api] in
        doneAction()
    }
    api.whenFailed = {
        println("修改密码失败")
    }
    api.send()
}

阅读Chats小记

Chats是一个用Swift语言写的聊天app的一个Demo,初学者能够从中了解如何构建一个实时通信app的雏形。Demo功能本相对简单,没有实现数据储存和网络连接。我之前有用Objective-C写过聊天应用的经验,打算用Swfit重写一遍。希望通过阅读这部代码能比较不同人的实现方式,找到可以借鉴的实现。

####生成气泡

let bubbleImage = bubbleImageMake()

func bubbleImageMake() -> (incoming: UIImage, incomingHighlighed: UIImage, outgoing: UIImage, outgoingHighlighed: UIImage) {
    let maskOutgoing = UIImage(named: "MessageBubble")!
    let maskIncoming = UIImage(CGImage: maskOutgoing.CGImage, scale: 2, orientation: .LeftMirrored)!

    let capInsetsIncoming = UIEdgeInsets(top: 17, left: 26.5, bottom: 17.5, right: 21)
    let capInsetsOutgoing = UIEdgeInsets(top: 17, left: 21, bottom: 17.5, right: 26.5)

    let incoming = coloredImage(maskIncoming, 229/255.0, 229/255.0, 234/255.0, 1).resizableImageWithCapInsets(capInsetsIncoming)
    let incomingHighlighted = coloredImage(maskIncoming, 206/255.0, 206/255.0, 210/255.0, 1).resizableImageWithCapInsets(capInsetsIncoming)
    let outgoing = coloredImage(maskOutgoing, 43/255.0, 119/255.0, 250/255.0, 1).resizableImageWithCapInsets(capInsetsOutgoing)
    let outgoingHighlighted = coloredImage(maskOutgoing, 32/255.0, 96/255.0, 200/255.0, 1).resizableImageWithCapInsets(capInsetsOutgoing)

    return (incoming, incomingHighlighted, outgoing, outgoingHighlighted)
}

func coloredImage(image: UIImage, red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) -> UIImage! {
    let rect = CGRect(origin: CGPointZero, size: image.size)
    UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
    let context = UIGraphicsGetCurrentContext()
    image.drawInRect(rect)
    CGContextSetRGBFillColor(context, red, green, blue, alpha)
    CGContextSetBlendMode(context, kCGBlendModeSourceAtop)
    CGContextFillRect(context, rect)
    let result = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return result
}

####其它

//用类名来作为复用Cell的标识
tableView.registerClass(MessageSentDateCell.self, forCellReuseIdentifier: NSStringFromClass(MessageSentDateCell))

//最简单的实现键盘消失的方法,有.Interactive和.OnDrag两种不同的形式。
//如需点击时消失键盘,还需额外实现touch事件。
tableView.keyboardDismissMode = .Interactive

Swift递归实现的不重复随机序列生成函数

题目来自于《编程珠玑》第一章节的一道题:生成位于0~N-1之间的k个不同的随机序列的随机整数

算法保证在没有生成过数的区间进行随机生成操作,能够满足不重复需求。但是递归原因容易造成随机数的集中。

func createGenerator(count:Int)->(Int,Int)->[Int]{
    //http://stackoverflow.com/questions/24270693/nested-recursive-function-in-swift
    var generator:(Int,Int)->[Int] = {_,_ in return []} // give it a no-op definition
    var total = count
    generator = {min,max in
        if (total <= 0 || min>max) {
            return []
        }else{
            total--;
            var random = Int(arc4random_uniform(UInt32(max-min)))
            var mid = min + random
            return [mid]+generator(min, mid-1)+generator(mid+1, max)
        }
    }
    
    return generator
}

createGenerator(10)(0,100)

iOS源码阅读之TTTAttributedLabel

TTTAttributedLabel是一个功能更为丰富的UILabel,支持AttributedString,识别特殊文本(如地址,电话,邮箱以及超链接等),并可以自定义这些文本的点击响应事件。为了了解其链接点击的实现方式,我决定对源码一窥究竟。

先看一下官方给出的给文字添加链接的参考代码。步骤很简单,在自己的Delegate实现中处理URL。

label.enabledTextCheckingTypes = NSTextCheckingTypeLink; 
label.delegate = self; 

label.text = @"Fork me on GitHub! (http://github.com/mattt/TTTAttributedLabel/)"; 

NSRange range = [label.text rangeOfString:@"me"];
[label addLinkToURL:[NSURL URLWithString:@"http://github.com/mattt/"] withRange:range]; 

iOS源码阅读之SVPullToRefresh

• iOS

SVPullToRefresh是基于UIScrollView的扩展,动态添加了下拉刷新的视图。下面来简单看一下添加视图的实现过程。

首先在类别中声明一个SVPullToRefreshView的property。

@interface UIScrollView (SVPullToRefresh)
...
@property (nonatomic, strong, readonly) SVPullToRefreshView *pullToRefreshView;
...
@end

然而Category并不支持为类添加属性和成员变量,所以需要通过关联对象的方法来动态添加。我们发现在.m文件中引入了#import <objc/runtime.h>头文件,并用@dynamic来声明pullToRefreshView,@dynamic的作用是告诉编译器手动实现setter和getter方法。关于objective-C的其它保留字可以参考唐巧的博文

#import <objc/runtime.h>

static char UIScrollViewPullToRefreshView;

@implementation UIScrollView (SVPullToRefresh)
...

@dynamic pullToRefreshView;

- (void)setPullToRefreshView:(SVPullToRefreshView *)pullToRefreshView {
[self willChangeValueForKey:@"SVPullToRefreshView"];
objc_setAssociatedObject(self, &UIScrollViewPullToRefreshView,
pullToRefreshView,
OBJC_ASSOCIATION_ASSIGN);
[self didChangeValueForKey:@"SVPullToRefreshView"];
}

- (SVPullToRefreshView *)pullToRefreshView {
return objc_getAssociatedObject(self, &UIScrollViewPullToRefreshView);
}

...
@end

核心部分的setter方法中,使用了objc_setAssociatedObject ,其作用就是为一个对象动态添加没有声明的变量。以下是SO上的详细解释。

objc_setAssociatedObject adds a key value store to each Objective-C object. It lets you store additional state for the object, not reflected in its instance variables.

It’s really convenient when you want to store things belonging to an object outside of the main implementation. One of the main use cases is in categories where you cannot add instance variables. Here you use objc_setAssociatedObject to attach your additional variables to the self object.

When using the right association policy your objects will be released when the main object is deallocated.

这样一个自己实现的下拉刷新视图就可以动态添加到UIScrollView上。之后SVPullToRefreshView 以弱引用保有其所在的scrollView变量,通过KVO侦听UIScrollViewcontentOffset的变化,实现了自己的一套状态逻辑,最终达到我们所见的效果。具体实现细节还请参考源码

“微信三问三答”

• 产品

这是一道知乎上引起广泛讨论的问题,具体查看这里

###朋友圈为什么不调整到首屏,为什么路径这么深? 从定位上来说。微信的核心是即时通讯,朋友圈是其衍生品。层次低一级。

从内容上来说。一个普通人的朋友数量有限,且朋友圈的鸡汤、代购一经过滤,有价值的内容留存很少。 和微博、知乎这种内容主导的平台完全不同,占据第一入口会极其浪费。

对腾讯而言,能为它带来利益具有战略意义的是购物、游戏、钱包。这些没有放到第一入口已算克制了。朋友圈还要啥自行车。

从使用习惯上来说,放得深并没有影响使用热情。所以也就没有必要再放出来了吧。

###朋友圈的基本数据结构设计是怎么样的?既能做到完美阅读权限设置,又能兼顾性能?

自己对数据库认识有限,所以只能说些粗鄙的想法。

每个用户都有自己的朋友圈队列,这里存放的是有权限阅读的内容id。 用户在发一条朋友圈的时候,服务器会筛选出拥有阅读权限的用户(或者客户端可以进行一部分筛选),然后把内容id添加入每一个有权阅读用户的朋友圈队列。这里会存在大量写入,不知道会有何瓶颈,好在每个用户相对独立且实时性要求不是那么高。 用户在刷新朋友圈时只需要从自己的队列中拿到id并索引出相应的内容即可。

另外评论中的查看权限,我觉得可以在客户端本地屏蔽处理,只是会耗费一些额外流量。

###如果你是微信的产品经理,你会最优先做哪一个功能的改进?简述理由。

这一条我很赞同的一个观点,就是我们不能拿自己的喜好去忖度一个产品。所以,还是要让用户、让数据说话。

可以根据访问量调整功能入口的层级。可以深入拓展和开发使用频率高的功能。还有一些用户看不到的,代码层面上的质量或性能问题,产品经理也应当有所重视。

就我个人而言,我比较倾向于完善收藏功能。能够便于自己整理收藏的图文,最好能在电脑端操作,并导出到相应的平台。

iOS App性能提升的技巧

• iOS

25 iOS App Performance Tips & Tricks翻译了部分提高app性能的技巧

##中阶性能提升建议

###9)复用和延迟加载 更多的视图意味着更多的绘制,这些最终意味着更多CPU和内存的开销。在通过UIScrollView展示很多视图时开销尤为明显。

因此,效仿UITableView和UICollectionView的思路,并不在一开始就创建所有视图,在需要的展示时候创建,并不显示的视图加入重用队里里。

通过这个方法我们显示新的视图时,只需要设置复用视图的属性,而避免了新建视图会引起的内存分配和初始化开销。

延迟加载的方法也可以用在其它情景。比如你需要通过点击按钮来展示一个视图,至少有两种实现形式:

  1. 在页面加载时创建并隐藏。需要时再显示出来。
  2. 直到点击按钮时才创建并显示。

这两种方法各有优缺点。第一种会长期占用内存空间,但是显示速度快。第二种刚好相反,不占空间但展示慢。具体采用哪种方式可以根据应用场景权衡。

###10)缓存、缓存、缓存 一项普遍的的开发经验就是”缓存那些有必要缓存的东西”,即缓存那些不太会改变但是经常访问的数据。 具体可以缓存什么数据呢?比如服务器返回的数据,图片,甚至是计算后的值如UITableView的高度。 NSURLConnection已经根据HTTP请求头将资源存储到本地或内存中,你甚至可以人为设置NSURLRequest,让它只访问缓存的数据。

	+ (NSMutableURLRequest *)imageRequestWithURL:(NSURL *)url {
	    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
	 
	    request.cachePolicy = NSURLRequestReturnCacheDataElseLoad; // 这一选项会总是返回缓存的图片
	    request.HTTPShouldHandleCookies = NO;
	    request.HTTPShouldUsePipelining = YES;
	    [request addValue:@"image/*" forHTTPHeaderField:@"Accept"];
	 
	    return request;
	}

注意,你可以用NSURLConnection来进行URL请求,同样AFNetworking也可以。而且你不需要改变什么代码,因为它已经做得足够好。

如果你想了解更多关于HTTP缓存的知识,可以涉猎NSURLCache,NSURLConnection等相关知识,确保浏览了NSHipster网站上的这篇文章 如果你需要缓存不包含在HTTp请求中的数据,可以涉猎NSCache。NSCache和NSDictionary表现的很像,但是当系统需要更多空间资源时,它会被释放。Matt Thompson在NSHipster写了一篇很赞的文章可以阅读一下。 想要了解更多关于HTTP缓存的知识,推荐阅读Google的《 best-practices document on HTTP caching》

###11)考虑图形绘制 在iOS上有好几种方式可以实现漂亮的按钮。可以用全尺寸图片或可伸缩图片显示,再复杂点用CALayer,CoreGraphics甚至OpenGL来绘制。

这些效果的实现难度不同和当然性能也不同。有篇很棒的文章讲述iOS图形性能,很值得阅读。同时,苹果UIKit项目组的成员Andy Matuschak也针对这篇文章给出了深度的分析。

简而言之,使用图片是最快的,因为iOS省去了绘制图形的过程,直接将图片渲染到屏幕上。问题是你需要把所有的图片都放入bundle中,这增加了app的大小。所以使用可伸缩尺寸的图片来绘制按钮更好。iOS会帮你绘制那些重复的纹理。你也不用针对不同尺寸的元素(如按钮)生成不同的图片。

毕竟通过使用图片你可能会丧失图片微调的能力,当你需要对不同的场景对视图进行微调时,你依旧需要处理大量的图片,不能通过代码直接调整。这会是一个繁复的过程。

所以,在绘制性能和app大小上,你需要根据自己的需要进行折中。