北武灵尊

记录一些事情,方便查阅。

易信年会

易信年会收获:

1、能不能再多做一点

2、三等奖净水器

不好的:

家里已经安装了净水器

ALAssetsLibrary选择图片优化

背景:

老板的手机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

AssetsLibrary自定义图片选择器

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

网址:http://fauxpasapp.com

102 rules: http://fauxpasapp.com/rules/

这个工具不多说了,直接看官网,是我目前发现比较好用的工具,一是可以帮助我找到项目中写的不好的代码,还有一些建议,二是可以通过这些建议对照想要的代码深入学习理解OC。

使用FauxPas后我的思考和困惑:

1、这样的优化意义大不大?

2、怎么才能让真个团队都提高这种写代码的素质?

3、可以有什么新的产出么?比如做一个Web的分析平台?

之前写的时候Mou奔溃了,只截了一张图片

How

XCTest测试属性变化

之前有一篇写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

然后我就想怎么把所有的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呢,也算是安慰了一下这颗折腾的心吧。

XCTest单元测试

QA的同事说帮他们讲一下OC,我自己都是个搓鸟,还给别人讲,就当是玩笑拒绝了,那么多大牛都在,找我讲?后来又说让我讲,是认真的,那我想,反正他们也不会,忽悠一下他们还是OK的了,简单的写个Hello World还是不成问题的,于是就去讲了,讲到后来才明白他们想的最终需求是:单元测试。作为开发我从来没有了解过这一块,我说自己回去先写个Demo。于是上网各种搜索,还是有些收获的,昨天晚上到12:40,对UI和属性的测试有了点思路,完成一个简单的Demo,就当是单元测试的Hello World吧,今天上午又搞定了异步请求的单元测试,结合自己项目(方便写登录的请求)写了一个Demo,这也是列为今天todo list的第一个任务。现在记录一下。

1、新建一个测试的Target

Test Target

2、设置Search Paths 保持和项目的Target设置一致,不然在引用文件的时候,可能会提示有些文件找不到,比如:LoginManager.h:10:9: fatal error: ‘biz/service/auth/auth_protocol.h’ file not found

Search Paths

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、参考资料

Hello to Myself

Octopress听到它有些时间了,并且还配置好Run成功了,甚至有个Hello World的一个博文,我看了提交记录是Aug 14, 2013。从这之后就放着了,因为不知道写什么。再后来,看很多大牛写博客,然后就自己也想写,有个幼稚的想法是:大牛要写博客,写博客会成为大牛。

仔细想想写什么东西呢?写的东西给谁看呢?好累,现在想通了,我的博客定位就是自己记录一下自己学到的知识点,以前不知道的,现在知道了,记录下来,下次再想查的时候,方便一点儿,于是就下定决心想要写了。

这个时候已经换了一台电脑了,又要重新折腾Octopress,在一步一步配置的时候,出现问题了,没有Run成功,于是又放下了,第二天又尝试了一次,又没有成功,就又放下了,如此反复几次后,暗暗的下了一个决心:一定要Run起来,不然会对以后的修行有障碍(凡人修仙)。

花了一整天时间上网查找资料,功夫不负有心人,折腾到下午的时候,突然一个尝试了一个方法,结果成功了。心里特别开心,觉得只要下死功夫没有搞不定的。

环境搞好了,又陷入了脑海(脑子里的一片独特空间,这个地方不断有新想法冒出来)中,第一篇写点什么呢,写的不好怎么办呢?就又放了有一个月。

而后在反思自己为什么总有一些想法(养鱼、养花、打羽毛球,还有很多)冒出来,却都没有实施的时候,突然诊断出自己又一个到了晚期的心理病:拖延症,严重的拖延症。

症状表现是:总是在想,从来没有实施。自己的脑海里面有N个idea,每个idea都是那么的美好(老婆说很多都不切实际),当想实施的时候,发现有许许多多的条件不满足,即使做了,也达不到预期的美好效果,然后就放弃了,开始下一个胡思乱想。可能还有个原因就是,想做到最好,所以没有开始做。

这让我心里有些害怕,我从什么时候变成这样,或者一直是这样,一直没有发现么。

我想改变,哪怕是做一点很小的事情或者举动(比如一直在脑海里做俯卧撑,这个时候身体实施了一个俯卧撑动作),只要不是光想着的就行。

这样有了这一篇博文,我想写,我写了。