易信年会收获:
1、能不能再多做一点
2、三等奖净水器
不好的:
家里已经安装了净水器
易信年会收获:
1、能不能再多做一点
2、三等奖净水器
不好的:
家里已经安装了净水器
背景:
老板的手机iphone 6 plus有5000+张相片,每次选择相片的时候需要长时间的loading,不能忍受。
排查问题:
打log排查到最耗时的地方是枚举相片的时候,调用的方法是:
- (void)enumerateAssetsUsingBlock:(ALAssetsGroupEnumerationResultsBlock)enumerationBlock;
这个在1000张左右的时候还算OK,但是相片多的时候就耗时了。
然后分析了微信、Lofter、QQ和易信,大概时间是这样的:
环境:iphone6+,相片6000张
微信:1秒
Lofter:1秒
QQ:秒开(无loading)
易信:4秒
注意我说的是大概感受的时间。
这一对比就不淡定了,通过查资料,最后决定使用方案:
- (void)enumerateAssetsAtIndexes:(NSIndexSet *)indexSet options:(NSEnumerationOptions)options usingBlock:(ALAssetsGroupEnumerationResultsBlock)enumerationBlock;
对,其实就是换了一个方法,哈哈,那思路就完全不一样了,之前是枚举出了所有的图片,保存到内存中,后面都使用这个数组,这样,开始的时候就比较耗时,后面使用的时候倒是很爽,之后呢,是通过IndexSet方法配合Table的cellForRow,产看那个加载那个,一张张加载,是不是很爽。
换了新方法后,我也把loading给拿掉了,表现能达到QQ。
然后问题来了,预览大图的时候怎么处理?预览大图还要排除掉视频,怎么保证顺序和currentIndex?
又拿QQ和微信做了分析,发现:
QQ点击相片的时候是[选中]操作,最下面的“预览”按钮是预览所有选中的相片。
微信点击相片是预览大图,在预览大图的时候可以选中该相片。
我个人觉得QQ是很合理,策划说微信的交互比较方便,那挑战就来了,在我们的速度达到QQ以后,交互还要达到微信。
难点:
1、点击其中一个相片预览大图,大图可以左右滑动查看所有相片。
2、预览大图左右滑动的时候需要把视频过滤掉。
分析:
1、在显示相片列表的时候已经枚举了这一屏所显示的20几张相片,点击其中任何一个相片,预览大图都没有问题,那么滑动的时候我们再动态枚举后面的相片,就OK了吧。
2、动态枚举后面的相片时,过滤掉视频,然后再排序,然后再定位当前相片在新的数组中的index就好了,这个地方牵扯出了一个新的难点:动态枚举出新的相片产生新的数组后,什么时候更新PageView呢?
解决新的难题:
滑动预览时,预加载到index==0(PageView只显示三个图片,会预加载提前一个图片)的时候,我们通过IndexSet预加载接下来的20(自己定义)张相片,然后过滤掉视频,并且从新排序,把新取到的数组先放着,等到用户不滑动的时候更新PageView,重新定位currentIndex值。
后续:
1、继续重构、优化;
2、提供Demo
1、ALAssetsLibraryAccessor单例
ALAssetsLibraryAccessor.h
#import <Foundation/Foundation.h>
@class ALAssetsLibrary;
@interface ALAssetsLibraryAccessor : NSObject {
ALAssetsLibrary *assetsLibrary_;
}
+ (ALAssetsLibraryAccessor *)sharedInstance;
- (ALAssetsLibrary *)assetsLibrary;
- (void)refreshAssetsLibrary;
@end
ALAssetsLibraryAccessor.m
#import "ALAssetsLibraryAccessor.h"
#import <AssetsLibrary/AssetsLibrary.h>
@implementation ALAssetsLibraryAccessor
- (id)init {
self = [super init];
if (self) {
assetsLibrary_ = [[ALAssetsLibrary alloc] init];
}
return self;
}
+ (ALAssetsLibraryAccessor *)sharedInstance
{
static ALAssetsLibraryAccessor *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[ALAssetsLibraryAccessor alloc] init];
});
return sharedInstance;
}
- (ALAssetsLibrary *)assetsLibrary {
return assetsLibrary_;
}
- (void)refreshAssetsLibrary {
assetsLibrary_ = nil;
assetsLibrary_ = [[ALAssetsLibrary alloc] init];
}
@end
2、ALAssetHelper获取AlAsset图片
ALAssetHelper.h
#import <Foundation/Foundation.h>
#import <AssetsLibrary/AssetsLibrary.h>
@interface ALAssetHelper : NSObject
+ (UIImage *)getImage:(ALAsset *)asset original:(BOOL)original;
@end
ALAssetHelper.m
#import "ALAssetHelper.h"
@implementation ALAssetHelper
+ (UIImage *)getImage:(ALAsset *)asset original:(BOOL)original
{
UIImage *image = nil;
CGImageRef ref = nil;
ALAssetRepresentation *rep = [asset defaultRepresentation];
if (original) {
// 获取ios剪裁后的图片
NSData *xmpData = rep.metadata[@"AdjustmentXMP"];
if (xmpData != nil) {
ref = [rep fullScreenImage];
image = [UIImage imageWithCGImage:ref];
} else {
ref = [rep fullResolutionImage];
image = [UIImage imageWithCGImage:ref
scale:[rep scale]
orientation:(UIImageOrientation)[rep orientation]];
}
} else {
ref = [rep fullScreenImage];
image = [UIImage imageWithCGImage:ref];
}
return image;
}
@end
3、枚举相册
- (void)enumerateAssetsGroups {
self.assetGroups = [NSMutableArray arrayWithCapacity:0];
ALAssetsLibrary *library = [[ALAssetsLibraryAccessor sharedInstance] assetsLibrary];
// Load Albums into assetGroups
dispatch_async(dispatch_get_main_queue(), ^{
@autoreleasepool {
// Group enumerator Block
void (^assetGroupEnumerator)(ALAssetsGroup *, BOOL *) = ^(ALAssetsGroup *group, BOOL *stop)
{
if (group) {
[self.assetGroups addObject:group];
} else {
// group is nil, so there are no more groups to enumerate
}
};
// Group Enumerator Failure Block
void (^assetGroupEnumberatorFailure)(NSError *) = ^(NSError *error) {
if (error.code == ALAssetsLibraryAccessUserDeniedError ||
error.code == ALAssetsLibraryAccessGloballyDeniedError) {
// Denied Error
}
};
// Enumerate Albums
[library enumerateGroupsWithTypes:ALAssetsGroupAll
usingBlock:assetGroupEnumerator
failureBlock:assetGroupEnumberatorFailure];
}
});
}
4、枚举相片
- (void)enumerateAssets:(ALAssetsGroup *)assetGroup {
NSMutableArray *assets = [NSMutableArray arrayWithCapacity:0];
ALAssetsGroupEnumerationResultsBlock groupEnumeration = ^(ALAsset *result, NSUInteger index, BOOL *stop) {
if (result) {
[assets addObject:result];
} else {
//
}
};
[assetGroup enumerateAssetsUsingBlock:groupEnumeration];
}
枚举相片的三个方法:
// These methods are used to retrieve the assets that match the filter.
// The caller can specify which results are returned using an NSIndexSet. The index set's count or lastIndex cannot exceed -numberOfAssets.
// 'enumerationBlock' is used to pass back results to the caller and provide the opportunity to stop the filter.
// When the enumeration is done, 'enumerationBlock' will be called with result set to nil and index set to NSNotFound.
// If the application has not been granted access to the data, 'enumerationBlock' will be called with result set to nil, index set to NSNotFound, and stop set to YES.
- (void)enumerateAssetsUsingBlock:(ALAssetsGroupEnumerationResultsBlock)enumerationBlock;
- (void)enumerateAssetsWithOptions:(NSEnumerationOptions)options usingBlock:(ALAssetsGroupEnumerationResultsBlock)enumerationBlock;
- (void)enumerateAssetsAtIndexes:(NSIndexSet *)indexSet options:(NSEnumerationOptions)options usingBlock:(ALAssetsGroupEnumerationResultsBlock)enumerationBlock;
提问:
如何发送一个ALAsset对象的视频?
1、Unused
网址:https://github.com/jeffhodnett/Unused
优点:找到相同图片可以预览
缺点:
①没有考虑@3x图片(作者好像不维护了);
②没有排除某个文件夹下面的图片不过滤,导致查找时间较长;
不过这两点都可以自己试着改掉。
使用感受:
需求做完,bug改完,后可以试着跑一下。用处不是很大,该功能下面这个工具也有。主要是以前没有看到过这种用Xcode开发Mac上使用的小工具,比较新奇,玩一下。
2、FauxPas
102 rules: http://fauxpasapp.com/rules/
这个工具不多说了,直接看官网,是我目前发现比较好用的工具,一是可以帮助我找到项目中写的不好的代码,还有一些建议,二是可以通过这些建议对照想要的代码深入学习理解OC。
使用FauxPas后我的思考和困惑:
1、这样的优化意义大不大?
2、怎么才能让真个团队都提高这种写代码的素质?
3、可以有什么新的产出么?比如做一个Web的分析平台?
之前写的时候Mou奔溃了,只截了一张图片
之前有一篇写XCTest异步测试,现在写一下对属性值变化的测试。比如设置某个属性的值,然后调用了某个方法后,值的变化是不是我们预期得到的。下面的代码是作者jtang写的,我修改了一句以适应ARC模式。
1、创建一个NSObject的扩展类(Category)
NSObject+Properties.h
#import <Foundation/Foundation.h>
@interface NSObject (Properties)
// 仅可用于单元测试 ivarName是成员变量名字,不是属性名字
// 对于对象直接返回,如果是原始数据类型.返回NSValue
// 示例:
// NSValue *value = [self getIvarFromString_ONLY_USE_IN_UNIT_TEST:@"_test"];
// int x;
// [value getValue:&x];
- (id)getIvarFromString_ONLY_USE_IN_UNIT_TEST:(NSString *)ivarName;
@end
NSObject+Properties.m
#import "NSObject+Properties.h"
#import <objc/runtime.h>
@implementation NSObject (Properties)
- (id)getIvarFromString_ONLY_USE_IN_UNIT_TEST:(NSString *)ivarName
{
Ivar ivar = class_getInstanceVariable(self.class, [ivarName UTF8String]);
// Ivar ivar = object_getInstanceVariable(self, [ivarName UTF8String], NULL);
if (ivar)
{
id ivarID = object_getIvar(self, ivar);
const char *typeEncoding = ivar_getTypeEncoding(ivar);
if (typeEncoding[0] == '@')
{
return ivarID;
}
else
{
return [NSValue valueWithBytes:&ivarID objCType:typeEncoding];
}
}
return nil;
}
@end
2、用法举例
ViewController.h
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
- (void)test;
@end
ViewController.m
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) NSString *userName;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
_userName = @"abc";
}
- (void)test {
_userName = @"def";
}
@end
测试文件MyAppTests.m
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import "ViewController.h"
#import "NSObject+Properties.h"
@interface MyAppTests : XCTestCase {
// add instance variables to the CalcTests class
ViewController *viewController;
}
@end
@implementation MyAppTests
- (void)setUp {
[super setUp];
// Put setup code here. This method is called before the invocation of each test method in the class.
viewController = [[ViewController alloc] init];
}
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
[super tearDown];
}
- (void)testUserName {
[viewController test];
NSString *userName = (NSString *)[viewController getIvarFromString_ONLY_USE_IN_UNIT_TEST:@"_userName"];
XCTAssertEqualObjects(userName, @"def", @"Sussess");
}
@end
3、测试UI变化
#import "TestSettingPickerCell_UI.h"
#import "SettingPickerCell.h"
#import "NSObject+Properties.h"
@implementation TestSettingPickerCell_UI
{
SettingPickerCell *cell;
}
- (void)testsetCheckMarkYES
{
UIImageView *checkImageView = [cell getIvarFromString_ONLY_USE_IN_UNIT_TEST:@"_mCheckImageView"];
[cell setCheckMark:YES];
STAssertEquals(checkImageView.image, [UIImage imageNamed:@"selected_green.png"],@"");
}
- (void)testsetCheckMarNO
{
UIImageView *checkImageView = [cell getIvarFromString_ONLY_USE_IN_UNIT_TEST:@"_mCheckImageView"];
[cell setCheckMark:NO];
STAssertEquals(checkImageView.image, [UIImage imageNamed:@"CellUnchecked.png"],@"");
}
- (void)setUp
{
[super setUp];
cell = [[SettingPickerCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
}
- (void)tearDown
{
[super tearDown];
}
@end
自己常用的一些脚本,批量查找、替换、删除。
1、复制一个文件夹下的文件到另外一个文件夹下
cp -ri bar/ foo/
2、删除文件夹下的文件
rm -rf /images/image*
3、在文件中查找关键字并替换
grep -rl "2013 - 浙ICP备" * | xargs sed -i -e 's/2013 - 浙ICP备/2013-2014 - 浙ICP备/g'
4、查找文件夹下的文件是否包含某个关键字
find src/ -name *.html |xargs grep -rn "火影"
find classes/ -name '*.xib' -o -name '*.mm' -o -name '*.m' |xargs grep -rl "bg_picker_white_cell"
find src/ -name *.js |xargs grep -rn "__dirty__"
5、批量替换的脚本
#!/bin/sh
#批量替换文案
oldString='<a target="_blank" href="http://yixin.im/contact.html">联系我们</a>'
newString='<a target="_blank" href="http://yixin.im/contact.html">联系我们</a> - <a target="_blank" href="http://yixin.im/contact.html">扫黄打非·净网2014</a>'
grep -rl $oldString *.html | xargs sed -i -e "s#$oldString#$newString#g"
6、删除example文件所有包含test的行
sed '/test/d' example
7、删除文件夹下文件中还有console.log的行,并且删除Sublime Text产生的-e结尾的文件
find html/ -name *.html -o -name *.js | xargs sed -i -e '/console.log/d'
find javascript/ -name *.html -o -name *.js | xargs sed -i -e '/console.log/d'
find src/ -name "*.html-e" -o -name "*.js-e" | xargs rm -f
这个脚本的产生是有个缘由的,一个有奖(大奖有iPhone6 plus)的活动页面,查看页面源代码的时候,虽然js代码经过了压缩并混淆,但是文件中的console.log中写的中文都显示出来了,要命的是,console.log中把每一个步骤及逻辑都写在里面了。幸亏是在公司内测的时候发现的问题。
然后我就想怎么把所有的console.log干掉,然后再压缩混淆,于是就有了这个脚本。但是问题来了,console.log在本地调试的时候还是有用的(Native+Web的方式,weinre调试方便一点),总不能先运行脚本删掉,然后等打包发布完了,再revert回来吧,于是就想,能不能在打包脚本里面加上去掉console.log的功能呢,正好看了一下公司的打包脚本是Uglifyjs2,继续上网查找资料的时候,发现Uglifyjs2有去掉console.log的配置项,drop_console: true,然后联系了NEJ作者genify,大牛百忙中加上了这个配置,工程在这里https://github.com/genify/toolkit,顺利打包。
8、find的时候排除某个文件夹
文件结构:
Demo
| ——— First
| ——— One
| ——— index.html
| ——— Second
| ——— index.html
| ——— index.html
1)、在Demo文件夹下查找index.html,排除Second文件夹
find . -path './Second' -prune -o -name '*.html' -print
2)、在Demo文件夹下查找index.html,排除One文件夹
find . -path './First/One' -prune -o -name '*.html' -print
3)、-prune对前面求值,-print不能少
这个脚本有什么具体的使用呢?这是我后来看到Unused的时候想到的,这个项目有个问题,一是没有处理@3x的图片,作者很久都没有更新过了,另外一个就是跑出来的结果中,有个文件夹下面是我不想让它去检索的,比如存放Emoji表情的文件夹,那我就想,能不能也提供个输入框,用来选择要过滤的某个文件夹呢,然后一番尝试,还真成功了,哈哈,起作用的主要代码如下:
// Create a find task
NSTask *task = [[[NSTask alloc] init] autorelease];
[task setLaunchPath: @"/usr/bin/find"];
// Search for all png files
NSArray *argvals = nil;
if (filterDirectoryPath.length <= 0) {
argvals = [NSArray arrayWithObjects:directoryPath, @"-name", @"*.png", nil];
} else {
argvals = [NSArray arrayWithObjects:directoryPath,
@"-path", filterDirectoryPath, @"-prune", @"-o", @"-name", @"*.png", @"-print", nil];
}
[task setArguments: argvals];
这是过滤了一个文件夹,要是过滤好几个呢,那就是:
find ./ \( -path './dir0*' -o -path './dir1*' \) -a -prune -o -name '*.png' -print
对这个开源项目改造了一下,试跑了一下易信的工程,运行时间大大减少,并且还找出了好几个没有用到的图片资源,减少了好几十个kb呢,也算是安慰了一下这颗折腾的心吧。
QA的同事说帮他们讲一下OC,我自己都是个搓鸟,还给别人讲,就当是玩笑拒绝了,那么多大牛都在,找我讲?后来又说让我讲,是认真的,那我想,反正他们也不会,忽悠一下他们还是OK的了,简单的写个Hello World还是不成问题的,于是就去讲了,讲到后来才明白他们想的最终需求是:单元测试。作为开发我从来没有了解过这一块,我说自己回去先写个Demo。于是上网各种搜索,还是有些收获的,昨天晚上到12:40,对UI和属性的测试有了点思路,完成一个简单的Demo,就当是单元测试的Hello World吧,今天上午又搞定了异步请求的单元测试,结合自己项目(方便写登录的请求)写了一个Demo,这也是列为今天todo list的第一个任务。现在记录一下。
1、新建一个测试的Target
2、设置Search Paths 保持和项目的Target设置一致,不然在引用文件的时候,可能会提示有些文件找不到,比如:LoginManager.h:10:9: fatal error: ‘biz/service/auth/auth_protocol.h’ file not found
3、新建Test Class
见图一。
4、引入需要的文件开始测试 比如要写一个登录的单元测试,那要引入LoginManager.h之类的文件吧,我大概写一下,有些是项目的代码,不知道写在博文里面好不好。
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import "LoginManager.h"
#import "LoginCallBack.h"
@interface MyTests : XCTestCase {
XCTestExpectation *_expectation;
}
@end
@implementation MyTests
- (void)setUp {
[super setUp];
[self addListenEvents];
}
- (void)tearDown {
[self removeListenEvents];
[super tearDown];
}
#pragma mark - 通知
- (void)addListenEvents {
extern NSString *kYIXINNotificationLoginResult;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onGetLoginResult:) name:kYIXINNotificationLoginResult object:nil];
}
- (void)removeListenEvents {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Test Methods
- (void)testUserLogin {
_expectation = [self expectationWithDescription:@"Login request"];
NSString *username = @"userID";
NSString *password = @"111111";
[LoginManager sharedManager].currentLoginData.userName = username;
[LoginManager sharedManager].currentLoginData.userPassword = [XXUtil encytePassword:password];
[LoginManager sharedManager].currentLoginData.type = kAccountYid;
[[LoginManager sharedManager] beginLogin];
[self waitForExpectationsWithTimeout:5
handler:^(NSError *error) {
// handler is called on _either_ success or failure
if (error != nil) {
XCTFail(@"timeout error: %@", error);
}
}];
}
#pragma mark - LoginResultProtocol
- (void)onGetLoginResult:(NSNotification*)aNotification {
extern NSString *kLoginStepKey;
extern NSString *kLoginResultKey;
NSDictionary *data = aNotification.userInfo;
NSInteger step = [[data objectForKey:kLoginStepKey] intValue];
NSInteger errorCode = [[data objectForKey:kLoginResultKey] intValue];
if (step == kLoginStepLogin) {
[_expectation fulfill];
if (errorCode == kResSuccess) {
XCTAssert(YES, @"Pass");
} else {
XCTAssert(NO, @"No Pass");
}
} else {
NSLog(@ "login result: step is %@, code is %@",@(step), @(errorCode));
}
}
@end
5、异步测试要点
_expectation = [self expectationWithDescription:@"Login request”];
[_expectation fulfill];
XCTAssert(YES, @"Pass");
6、其他
其他的异步测试比如Block、Delegate的方式也都如此(采用XCTestExpectation)。
7、参考资料
Octopress听到它有些时间了,并且还配置好Run成功了,甚至有个Hello World的一个博文,我看了提交记录是Aug 14, 2013。从这之后就放着了,因为不知道写什么。再后来,看很多大牛写博客,然后就自己也想写,有个幼稚的想法是:大牛要写博客,写博客会成为大牛。
仔细想想写什么东西呢?写的东西给谁看呢?好累,现在想通了,我的博客定位就是自己记录一下自己学到的知识点,以前不知道的,现在知道了,记录下来,下次再想查的时候,方便一点儿,于是就下定决心想要写了。
这个时候已经换了一台电脑了,又要重新折腾Octopress,在一步一步配置的时候,出现问题了,没有Run成功,于是又放下了,第二天又尝试了一次,又没有成功,就又放下了,如此反复几次后,暗暗的下了一个决心:一定要Run起来,不然会对以后的修行有障碍(凡人修仙)。
花了一整天时间上网查找资料,功夫不负有心人,折腾到下午的时候,突然一个尝试了一个方法,结果成功了。心里特别开心,觉得只要下死功夫没有搞不定的。
环境搞好了,又陷入了脑海(脑子里的一片独特空间,这个地方不断有新想法冒出来)中,第一篇写点什么呢,写的不好怎么办呢?就又放了有一个月。
而后在反思自己为什么总有一些想法(养鱼、养花、打羽毛球,还有很多)冒出来,却都没有实施的时候,突然诊断出自己又一个到了晚期的心理病:拖延症,严重的拖延症。
症状表现是:总是在想,从来没有实施。自己的脑海里面有N个idea,每个idea都是那么的美好(老婆说很多都不切实际),当想实施的时候,发现有许许多多的条件不满足,即使做了,也达不到预期的美好效果,然后就放弃了,开始下一个胡思乱想。可能还有个原因就是,想做到最好,所以没有开始做。
这让我心里有些害怕,我从什么时候变成这样,或者一直是这样,一直没有发现么。
我想改变,哪怕是做一点很小的事情或者举动(比如一直在脑海里做俯卧撑,这个时候身体实施了一个俯卧撑动作),只要不是光想着的就行。
这样有了这一篇博文,我想写,我写了。