更新: 现在已不需要再按照下面方法安装, 按官方文档操作即可

—————————————————-

nuclide 是一款由 facebook 研发, 基于 atom 的, 针对 web/mobile 开发定制的 IDE, 目前(2015.10.06) 还处于初级阶段, 相应的支持很少. 但难得的是这是一款 IDE 而不是编辑器, 也是现阶段能对 react/react-native 提供主要支持的少有的几个工具之一.

本文主要针对 react-native 开发, 不涉及其他功能.

直接使用 atom 安装可能会发生 atom helper 长时间占用 100% cpu 进而导致 atom 无法使用的问题 github issue, 下面是解决方法.

正确的安装方法(之一):

环境: El Capitan 10.11.0, atom 1.0.19, Nuclide 截止至10月6日代码

安装方法:

  1. 清空 ~/.atom/packages 下面所有 nuclide 相关的包, 注意不仅仅是 nuclide 开头的, 还有个 hyperclick 也是 nuclide 的组件
  2. 删除 ~/.atom/config.cson 里面 disabledPackages 中禁用的 nuclide 相关 package
  3. 按照 nuclide readme 下面 Building from Source 的方法, 用 ./scripts/dev/setup 安装, 包实际上安装到了 nuclide 项目目录里, 以软连接的形式放到了 ~/.atom/packages 下面, 注意脚本运行过程中有没有错误信息, 如果有要解决.
  4. 安装后第一次启动 atom 会没响应, 问你要不要 wait 选 wait, 可以看到 atom helper 一直占用 100% cpu, 这是 because of the large number of Babel files that need to be transpiled, 等不到 10 分钟就正常了. 再次重开包括进 settings 也没有问题, 不需要禁用任何 package

PS1.可能要注意的:

  1. 在安装前保证 atom 没有任何打开的项目, 标签等, 包括 settings, timecop 这些东西

PS2. 安装后, 一定要注意的:

  1. 仔细阅读 nuclide-flow 的 readme, 会告诉你如何让这个插件正常运作:
    • 插件 settings 添加 flow 的路径,
    • .flowconfig (react-native 默认就会自动生成),
    • /* @flow */
  2. react-native 自动生成的 .flowconfig 可能会让 flow 无法运行, 原因是其中的 [version] 会限制 flow 的版本, 不匹配就不能运行, 删掉那部分即可
  3. flow 执行需要几秒钟, 过了这几秒就可以像 readme 展示的那样支持 cmd-click 等功能了
  4. 仔细观察 nuclide-flow 包的设置选项, 你会发现默认需要通过 save 来触发 flow 的错误检测, 并且提供了选项来让 flow 随时检测错误.

PS3.

  • 仔细读其他 nuclide 插件的 readme (如果有), 目前 nuclide 的相关东西特少, 有问题都搜不到, 所以仔细读说明才是正确的做法
Tagged with:  

英文标题: How to implement a user interactivable UIView implicit animation

这一篇教程将结合一个小 demo 讲解, 最终代码在 github 上可以下载到. demo 的运行效果如图所示, 通过手势拖动顶端红色矩形到虚线位置, 拖动距离超过 50% 松手, 矩形会自动移动到目标位置, 少于 50% 松手, 矩形自动归位:

How to implement a user interactive UIView implicit animation

实现一个可交互的动画大概有这么几种思路:

  1. 在响应 GestureRecognizer 的方法中实时更新 view 的状态, 在 GestureRecognizer 结束的时候创建动画完成后续的部分.
  2. 在 GestureRecognizer 开始的时候创建显式动画, 随着 Gesture 变化, 改变动画的 progress, 在 GestureRecognizer 结束之后完成动画.
  3. 在 GestureRecognizer 开始的时候创建隐式动画, 随着 Gesture 变化, 改变动画的 progress, 在 GestureRecognizer 结束之后完成动画.

注意 2 和 3 只有显式和隐式的区别.

对于简单的 view, 单一的动画 (比如 demo 中的这种, 线性移动一个简单的 view), 三种方法差别不大. 而对于复杂的 view (view 中嵌套多个 subView), 多个动画 (view 和 subview 都有各自的动画), 如果在不考虑可交互的前提下, 隐式的动画已经可以满足需求, 那么方法 3 可能会比方法 1 和 2 简洁得多, 本文将对方法 3 进行讲解.

要实现方法 3, 首先要了解 CALayer 的几个属性: speed, timeOffset, beginTime. 对于这几个属性, Controlling Animation Timing 这篇教程图文并茂地给出了直观的解释, 必读, 这里只做简单介绍:

  • speed: 控制动画的速度, 比如设置为 1 (默认), 动画正常运行, 设置为 0 动画暂停, 设置为 -1 动画反向运行.
  • timeOffset: 控制动画在整个动画时间轴上的位置, 比如一个 3 秒的动画, timeOffset 为 1.5, layer 就会处于整个动画过程中间的那个状态.
  • beginTime: 动画的开始时间.

有了这些基础, 我们就可以着手实现了, 首先我们需要建立一个 CALayer 的 Category, 添加一些动画辅助代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)gs_pauseAnimation {
    self.speed = 0;
    self.timeOffset = 0;
}
 
- (void)gs_recoverToDefaultState {
    self.speed = 1;
    self.timeOffset = 0;
    self.beginTime = 0;
}
 
- (void)gs_setTimeProgress:(CFTimeInterval)timeProgress {
    self.timeOffset = timeProgress;
}
 
- (void)gs_continueAnimationWithTimeProgress:(CFTimeInterval)timeProgress {
    [self gs_recoverToDefaultState];
 
    self.beginTime = [self convertTime:CACurrentMediaTime() fromLayer:nil] - timeProgress;
}
 
- (void)gs_continueReverseAnimationWithTimeProgress:(CFTimeInterval)timeProgress animationDuration:(CFTimeInterval)duration {
    [self gs_recoverToDefaultState];
 
    self.beginTime = [self convertTime:CACurrentMediaTime() fromLayer:nil] - (duration - timeProgress);
    self.timeOffset = duration;
    self.speed = -1;
}
  • gs_pauseAnimation: 通过将 speed 设置为 0, 来暂停动画.
  • gs_recoverToDefaultState: 将 layer 的各个属性恢复到默认状态
  • gs_setTimeProgress: 在 gs_pauseAnimation 的基础上, 设置动画的时间进度.
  • gs_continueAnimationWithTimeProgress: 从 timeProgress 时间进度开始, 继续播放之前暂停了的动画
    • 这里首先调用了 gs_recoverToDefaultState, 是为了让 convertTime:fromLayer: 得出正确的时间. 
    • [self convertTime:CACurrentMediaTime() fromLayer:nil] 将当前系统时间转换到 layer 的时间上, 减去 timeProgress, 假设这是一个 3 秒的动画, timeProgress 为 1 秒, 则这条代码就表示 layer 的动画在一秒之前就已经开始, 所以动画将从运行到第 1 秒的状态开始继续播放.
    • 如果不能理解, 可以结合一下文档中这篇: How do I pause all animations in a layer tree?
  • gs_continueReverseAnimationWithTimeProgress: 从 timeProgress 时间进度开始, 反向播放之前暂停了的动画.
    • 这里 beginTime 设置为 当前时间 - (整个动画时间 - timeProgress), 再假设这是一个 3 秒的动画, timeProgress 为 1 秒, 因为要让动画反向播放, 所以要把动画反过来看, 由终点到起点运动, timeProgress 这 1 秒, 是动画最后的部分, 前 3 – 1 = 2 秒是已经播放完的, 所以动画要从 第 2 秒开始.
    • 通过将 speed 设置为 -1 来反向播放动画

完成了动画控制的实现, 接下来是控制 view 状态的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)switchToState1 {
    [self.moveableView setCenter:CGPointMake(self.view.bounds.size.width / 2, 50)];
    [self.moveableView setTag:1];
}
 
- (void)switchToState2 {
    [self.moveableView setCenter:CGPointMake(self.view.bounds.size.width / 2, 500)];
    [self.moveableView setTag:2];
}
 
- (void)switchState {
    if (self.moveableView.tag == 1) {
        [self switchToState2];
    }
    else {
        [self switchToState1];
    }
}

很简单, 不用过多解释, view 的起始状态是 state1.

然后是手势控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGesture {
    if (panGesture.state == UIGestureRecognizerStateBegan) {
        [self.moveableView.layer removeAllAnimations];
 
        [UIView animateWithDuration:kAnimationDuration animations:^{
            if (self.moveableView.tag == 1) {
                [self switchToState2];
            }
            else {
                [self switchToState1];
            }
        } completion:^(BOOL finished) {
            [self.moveableView.layer gs_recoverToDefaultState];
 
            if (self.animationProgress <= 0.5) {
                if (self.moveableView.tag == 1) {
                    [self switchToState2];
                }
                else {
                    [self switchToState1];
                }
            }
        }];
 
        [self.moveableView.layer gs_pauseAnimation];
    }
    else if (panGesture.state == UIGestureRecognizerStateChanged) {
        CGPoint translation = [panGesture translationInView:self.view];
        CGFloat length = ABS(translation.y);
 
        self.animationProgress = MAX(0, MIN(length / 500, 1));
 
        [self.moveableView.layer gs_setTimeProgress:self.animationProgress *  kAnimationDuration];
    }
    else if (panGesture.state == UIGestureRecognizerStateEnded || panGesture.state == UIGestureRecognizerStateFailed || panGesture.state == UIGestureRecognizerStateCancelled) {
 
        if (self.animationProgress > 0.5) {
            [self.moveableView.layer gs_continueAnimationWithTimeProgress:self.animationProgress * kAnimationDuration];
        }
        else {
            [self.moveableView.layer gs_continueReverseAnimationWithTimeProgress:self.animationProgress * kAnimationDuration animationDuration:kAnimationDuration];
        }
    }
}
  1. 在手势开始时, 创建 UIView 隐式动画并暂停;
  2. 随着手势移动, 调整动画的进度
  3. 在手势结束后, 根据当前手势位移来判断是将 view 归位, 还是移动到目标位置.
  4. 在动画结束后, 也就是在手势开始时隐式动画的 completion 中, 恢复 layer 状态, 并在 view 归位的情况下, 设置 view 到初始的状态.

运行一下 (这部分代码在 branch v1), 看起来得到了想要的效果, 但是如果细心观察, 会发现在手势拖动距离不够长, view 移动到原位的时候, view 会闪烁一下. 因为 demo 中 view 太简单了, 所以可能要多试几次才能观察到, 不过也可以在 completion block 里面第一行设置个断点, 可以看到, 执行到断点处时, view 消失了.

这里的原因我没有深入研究, 猜测是因为 speed 设置为 -1, 导致动画运行完成的时候 view 处于一个不存在的状态, 也就无法将这个状态的 view 绘制到屏幕上. 虽然通过将 speed 设置为 -1 来反向运行动画看起来很机智, 但很显然, 这不是一个完美的解决方法.

既然不能让动画自动反向运行, 那就稍微绕个远, 手动反向运行. CADisplayLink 是一个不错的选择, 它与 NSTimer 类似, 但是是在屏幕每一帧刷新之前调用 selector. 新的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGesture {
    if (panGesture.state == UIGestureRecognizerStateBegan) {
        [self.moveableView.layer removeAllAnimations];
 
        [self.displayLink invalidate];
 
        [UIView animateWithDuration:kAnimationDuration animations:^{
            [self switchState];
        } completion:^(BOOL finished) {
 
        }];
 
        [self.moveableView.layer gs_pauseAnimation];
    }
    else if (panGesture.state == UIGestureRecognizerStateChanged) {
        CGPoint translation = [panGesture translationInView:self.view];
        CGFloat length = ABS(translation.y);
 
        self.animationProgress = MAX(0, MIN(length / 500, 1));
 
        [self.moveableView.layer gs_setTimeProgress:self.animationProgress *  kAnimationDuration];
    }
    else if (panGesture.state == UIGestureRecognizerStateEnded || panGesture.state == UIGestureRecognizerStateFailed || panGesture.state == UIGestureRecognizerStateCancelled) {
 
        if (self.animationProgress > 0.5) {
            [self.moveableView.layer gs_continueAnimationWithTimeProgress:self.animationProgress * kAnimationDuration];
        }
        else {
            [self.displayLink invalidate];
 
            self.displayLinkStarTime = CACurrentMediaTime();
            self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateAnimation:)];
            [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        }
    }
}
 
- (void)updateAnimation:(id)sender {
    CFTimeInterval timeInterval = CACurrentMediaTime() - self.displayLinkStarTime;
    CFTimeInterval animationTimeProgress = self.animationProgress * kAnimationDuration - timeInterval;
 
    if (animationTimeProgress >= 0) {
        [self.moveableView.layer gs_setTimeProgress:animationTimeProgress];
    }
    else {
        [self.displayLink invalidate];
 
        [self.moveableView.layer gs_continueAnimationWithTimeProgress:kAnimationDuration];
        [self switchState];
    }
}

可以看到, 在手势介素后, 我们创建了一个 CADisplayLink 来手动反向运行动画, 并在动画结束后, 重新设置 view 的状态.

运行, 闪烁消失, Cheers.

–以上–

Tagged with:  

UIScrollView及其子类UITableView, UICollectionView为iOS开发带来了极大的方便, 其分页(pagingEnabled)功能也很常用, 但是功能却有些局限, 页只能按UIScrollviewbounds尺寸划分, 如果要实现自定义分页宽度或高度就需要一些技巧.

方法一

类似iOS 6中MobileSafari, 如图所示的分页方法:

HGPageScrollView_Screen_shot_2

 

StackOverflow上提供的实现思路如下:

 

  1. UIScrollviewbounds限制再屏幕中间那一页的位置
  2. 禁用UIScrollviewclipsToBouds, 从而能显示超出bounds的内容

此时已经可以在借助UIScrollview原有分页功能的基础上实现了"视觉上"与MobileSafari类似的效果, 但因为UIScrollview的尺寸限制, 超出范围的触摸事件不会被UIScrollview收到, 影响体验, 要把触摸事件传递给UIScrollview, 方法如下:

  1. 用一个全屏的(或者其他你想要的尺寸)UIView子类, 这里我们命名为CustomUIView来做UIScrollviewsuperView
  2. 重写CustomUIViewhitTest:withEvent方法:

     - (UIView *) hitTest:(CGPoint) point withEvent:(UIEvent *)event {
       if ([self pointInside:point withEvent:event]) {
         return scrollView;
       }
       return nil;
     }
    

    通过重写hitTest, 就可以将CustomUIView接受到的触摸事件传递给UIScrollView

方法二

前一种方法保留了UIScrollView原有的分页功能, 而下面这种更加灵活的自定义分页方法则完全自制了一个分页功能:

DMPagingScrollView就是一个很好的实现, 其自定义分页的思路大致如下(需要参考DMPagingScrollView.m代码):

  1. 用实例变量pageWidth记录自定义分页的宽度
  2. 通过实现UIScrollView的delegate方法, 来记录每次拖动UIScrollView动作的相关参数(位移, 加速度…)
  3. scrollViewDidEndDecelerating:等一些标志拖动结束的delegate方法中调用snapToPage方法, 在snapToPage方法中, 借助之前收集的动作参数计算出contentOffset, 并通过setContentOffset:offset animated:YES来移动.

这种方式的关键就在contentOffset的计算中(DMPagingScrollView.m pageOffsetForComponent:方法), 如果计算的不正确, 就会严重影响拖动UIScrollView的手感.

DMPagingScrollView通过subclassing UIScrollview将自定义分页相关的过程操作封装在DMPagingScrollView里面, 同时支持横向纵向的自定义分页, 十分方便. 类似地, 我们也可以对UITableView, UICollectionView进行同样的处理.

此外, 通过参考pageOffsetForComponent:中的计算方法, 我们甚至可以让每一页的宽度都不同, 从而大大提升了分页的灵活性, 比如我实现的这种:

- (CGFloat)pageOffset {
    CGFloat totalWidth = self.collectionView.contentSize.width;
    CGFloat visibleWidth = self.collectionView.bounds.size.width;

    CGFloat currentOffset = self.collectionView.contentOffset.x;
    CGFloat dragVelocity = _dragVelocity.x;
    CGFloat dragDisplacement = _dragDisplacement.x;

    NSInteger nearestIndex = 0;
    CGFloat minDis = CGFLOAT_MAX;
    CGFloat nearestOffset = 0;
    CGFloat prevOffset = 0;
    CGFloat nextOffset = 0;
    CGFloat totalOffset = 0;

    // self.pagesWidths is an NSArray that contains each page's width
    for (NSInteger i = 0; i < self.pagesWidths.count; i++) {
        CGFloat dis = ABS(totalOffset - currentOffset);

        if (dis < minDis) {
            minDis = dis;

            prevOffset = nearestOffset;
            
            nearestOffset = totalOffset;

            if (i == self.pagesWidths.count - 1) {
                nextOffset = totalOffset;
            }
            else {
                nextOffset = totalOffset + [(NSNumber *)self.pagesWidths[i] floatValue];
            }
        }

        totalOffset += [(NSNumber *)self.pagesWidths[i] floatValue];
    }

    NSInteger lowerIndex;
    NSInteger upperIndex;
    CGFloat lowerOffset;
    CGFloat upperOffset;
    if (currentOffset - nearestOffset < 0) {
        lowerIndex = nearestIndex - 1;
        upperIndex = nearestIndex;

        lowerOffset = prevOffset;
        upperOffset = nearestOffset;
    }
    else {
        lowerIndex = nearestIndex;
        upperIndex = nearestIndex + 1;

        lowerOffset = nearestOffset;
        upperOffset = nextOffset;
    }

    CGFloat newOffset;
    if (ABS(dragDisplacement) < DRAG_DISPLACEMENT_THRESHOLD || dragDisplacement * dragVelocity < 0) {
        if (currentOffset - lowerOffset > upperOffset - currentOffset) {
            newOffset = upperOffset;
        } else {
            newOffset = lowerOffset;
        }
    } else {
        if (dragVelocity > 0) {
            newOffset = upperOffset;
        } else {
            newOffset = lowerOffset;
        }
    }

    if (newOffset > totalWidth - visibleWidth)
        newOffset = totalWidth - visibleWidth;

    if (newOffset < 0)
        newOffset = 0;

    return newOffset;
}

–以上–

Tagged with:  

最近做的一个项目中涉及这样一个需求: 响应用户点击html元素的事件.这其中包含响应单击(single tap/click)和双击(double tap/dblclick).

单击事件

通常单击的解决方案是用javascript实现, 为html元素绑定onclick事件, javascript接收事件后跳转url, 在CocoaTouch中用 webView:shouldStartLoadWithRequest:navigationType: 拦截url跳转, 从而实现单击事件的响应, 大致的代码如下:

Javascript:

function myClick() {
    var e = window.event
    var url = "click:" + e.target.id;
    document.location = url;
}

var ele = document.getElementById(ELEMENT_ID);
ele.onclick = myClick;

CocoaTouch:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {

    NSString *requestString = [[request URL] absoluteString];
    NSArray *components = [requestString componentsSeparatedByString:@":"];
    if (components.count == 2 && [(NSString *)[components objectAtIndex:0] isEqualToString:@"click"]) {
        NSString *eleIdStr = (NSString *)[components objectAtIndex:1];
        NSInteger eleId = [eleIdStr integerValue];

        // Do something

        return NO;   // Do not load request, just receive the event
    }
    return YES;
}

双击事件

Failed 1

使用与上面同样的方法为html元素添加ondblclick来响应双击事件就无法实现, 现象为响应函数从不被调用. 用setTimeout等方法尝试用单击事件来模拟双击事件也无法实现, 无法接收到double tap的第二次点击.

Failed 2

根据之前的现象来看, 显然双击事件在html之外就被拦截了, 要从CocoaTouch层下手才行.

系统的UIWebView中包含了一个UIScrollView, 绑定了UIScrollViewDelayedTouchesBeganGestureRecognizerUIScrollViewPanGestureRecognizer这两个事件, 后者响应拖动事件, 前者相应点击事件.

尝试添给UIScrollView加UITapGestureRecognizer:

UITapGestureRecognizer *dblTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(fakeTapGestureHandler:)];

经测试, fakeTapGestureHandler:从未被调用过.

Success

尝试曲线救国的方式, 用UIGestureRecognizer的delegate方法获取事件:

Javascript:

function eleIdFromPoint(x, y) {
    var ele = document.elementFromPoint(x, y)
    return "" + ele.id;
}

CocoaTouch:

// Add gesture
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(fakeTapGestureHandler:)];

[tapGestureRecognizer setDelegate:self];
[_webView.scrollView addGestureRecognizer:tapGestureRecognizer];

// Delegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    CGPoint tapPoint = [touch locationInView:_webView];
    NSString *script = [NSString stringWithFormat:@"eleIdFromPoint(%f, %f)", tapPoint.x, tapPoint.y];
    NSString *eleIdStr = [self stringByEvaluatingJavaScriptFromString:script];
    if (touch.tapCount == 2) {
        if (eleIdStr.length > 0) {
            NSInteger eleId = [eleIdStr integerValue];

            // Do somthing

        }
    }
    return YES; // Return NO to prevent html document from receiving the touch event.
}

上面代码通过UIGestureRecognizer的delegate方法成功获取到点击事件, 需要说明的几点:

  • locationInView:的参数为UIWebView, 后面的document.elementFromPoint传递的也是UIWebView中对应的坐标, 不需要算上UIScrollView的Offset.
  • delegate方法中单击事件也可以被拦截到, 这样单击双击就可以在这块儿一起实现, 让代码看起来比较统一.
  • delegate方法返回NO会拦截事件, html文档就不能接受到任何点击事件了.

–以上–

Tagged with:  

Fusion Drive组建及Bootcamp安装

On 04/09/2013, in 折腾, 硬件, by ultragtx

本教程将涉及

  • 硬件安装
  • 系统安装,Fusion Drive创建,及保留Recovery分区
  • Fusion Drive以后如何Bootcamp装Windows

安装硬件

硬件选购参考(购于Taobao)

FDnBI m5s

Plextor m5s 256G:这个不用说吧,1.0.3固件,东芝颗粒,速度快,便宜。

FDnBI optbay

光驱位硬盘托架 佳翼H125 :做工一般般,不过这个价格也找不出做工好的,需要高质量的去买OWC Data Doubler

FDnBI usbsata

光驱盒替代品:这个小玩意跟上面那个光驱托架一起买只要9.9,特便宜,相对来讲那个光驱盒目测质量不敢恭维,买个这个凑合一下,反正光驱也不常用。另外有一点要注意,拆下来的光驱在读盘的时候会从各种缝隙漏出激光,直射眼睛可能会造成伤害,尤其是刻录的时候,要知道DVD刻录光驱的激光是可以点烟的,所以如果常用光驱还是建议买一下光驱盒

关于SSD,HDD分别放在哪些位置

注意事项:

(其中没有实际测试过的,不能保证100%的正确性)

速度方面:
  • 2011年以后的机器硬盘位支持SATA III 6Gb/s
  • 2012年以后的机器光驱位支持SATA III 6Gb/s
  • 2011年春季的机器光驱位有部分“看起来”支持SATA III 6Gb/s 部分支持SATA II 3Gb/s
  • 2011年秋季的机器光驱位全部“看起来”支持SATA III 6Gb/s
  • 可以在"系统报告/串行ATA/链接速度" 查看是否“看起来”支持SATA III 6Gb/s
  • 2011年光驱位看起来支持SATA III 6Gb/s的机器,将支持SATA III的SSD放入光驱位会有问题,传说Intel的SSD没有问题,具体见这里最后一楼这里的讨论。再次强调,Intel的SSD没有经过测试,其他厂商SSD放在光驱位几乎可以肯定会出问题。
  • SATA III的设备安装在
  • 一些机械硬盘是SATA III 6Gb/s的,这种情况下将机械硬盘放到有问题的光驱位也可能有问题。
  • SATA III的设备理论上是兼容SATA II的,所以只要不是2011年有问题的机器,
安全方面:
  • HDD位有移动感应,据说可以在检测出电脑移动的时候让硬盘磁头跟盘面分离。
  • 视不同的硬盘,有些硬盘内建重力感应,也就是说自带一定减震效果

根据以上条件:

  • 如果你的设备为2011年,光驱位“看起来”支持SATA III的机器,又购买了非Intel的SSD,那么SSD只能放到HDD位,光驱位放原来的硬盘。
  • 如果没有上述问题,尽量将SSD放到光驱位,硬盘放在硬盘位,既保证硬盘的安全,同时硬盘噪音会小很多。

实测“伪”SATA III装SATA III SSD后的问题:

  • 向SSD写数据,会间歇性暂停传输,传输速度为0,如果系统安装在上面就会出现系统间歇性卡顿,难以使用
  • 写入数据速度最高仅200MB/s,正常应达到360MB以上
  • 有时会直接无法写入

拆机

根据上一部分的判断,选择性拆光驱、硬盘。建议先看一下王自如拆机组RAID的视频,做到心里有数,拆的时候不要心急。

以2011 late Macbook Pro(MD318/MD322)为例:

拆光驱(如图所示):

FDnBI breakout

  1. 背盖
  2. 拆绿色部分,顺序无所谓
  3. 紫色部分,撕开与光驱相连的黑色胶布
  4. 蓝色部分,顺序无所谓

一些说明:

  • 红色部分的线跟视频中位置不同,如图所示情况下不需要拆,实际只要不会影响取出光驱就没必要拆这根线,而且这根也较难拆
  • 蓝色部分除7之外,所有的都是6角螺丝,需要特殊的螺丝刀
  • 绿色部分除1之外,所有的都是十字螺丝
  • 蓝4,在红色部分线下面,需要将红线移开一些才能接触到
  • 蓝5,需要拿开上面的黑色塑料模块才能看到

拆硬盘

  • 跟视频中的方法一样,注意排线。

安装OSX

本文创作时,OSX最新版本为10.8.3,一切内容均以此版本为基准。

安装OSX 10.8.3 Mountain Lion

安装流程:

WARNING

以下操作会清空所有数据,切记在执行前备份重要数据!

  1. 制作安装光盘/U盘

  2. 重启,安装盘引导,查看磁盘列表

     diskutil list 
  3. 创建逻辑卷组(Logical volume group)。其中,cs为coreStorage简写;FusionDrive为卷组名字,随便起;disk0为SSD,disk1为HDD,根据上一步的结果自行修改为合适的值

     diskutil cs create FusionDrive disk0 disk1 
  4. 查看创建结果,复制逻辑卷组的UUID(eg. "+– Logical Volume Group 2B4EDB74-5842-40E4-8398-1567CD879127",这一行最后一串数字字母)

     diskutil cs list 
  5. 创建卷。后面2B4…127为之前复制的UUID,FusionDrive为名字,100%为容量

     diskutil coreStorage createVolume 2B4EDB74-5842-40E4-8398-1567CD879127 jhfs+ FusionDrive 100% 
  6. 在Fusion Drive上安装系统

  7. 装好系统想着开启Trim:方法(适用于10.8.3,其他未测试)

参考链接:

关于Recovery HD问题

虽说按上面教程安装后用开机按option看不到Recovery分区,但开机按Command + R还是可以进入Recovery的。

这个链接 DIY Fusion Drive: Adding Recovery HD to a CoreStorage Volume Group里有创建Recovery分区的方法,实测@robmathers的方案在开机Option中还是看不到Recovery,也可能是我操作有问题。

Bootcamp

几点说明:

  • Macbook Pro通过Bootcamp安装Windows需要内置光驱,外置以及USB引导安装盘都无解
  • Fusion Drive后,Bootcamp分区是从HDD上分割出来的,不会用到SSD
  • 在Bootcamp 5下,光驱位HDD上的Windows是可以引导的(有传言不能引导)

准备工作:

  • Virtualbox,虚拟机,用于制作Windows磁盘镜像。
  • Windows 7 64bit,或Win 8 64bit 安装镜像。32位Bootcamp说不支持,未测试。
  • Winclone,用于将磁盘镜像写入Bootcamp分区

安装

创建虚拟磁盘

  1. VirtualBox新建虚拟机,命名为“Windows”,磁盘文件类型VDI,动态分配,大小10G(不要给大,影响后面转换的速度)。
  2. 虚拟机设置里面设置好Windows安装光盘镜像
  3. 启动进入安装界面,Shift+F12打开CMD,输入下面命令,手动创建分区,避免Windows自动创建100MB分区

     diskpart select disk 0 create partition primary exit exit 
  4. 开始安装,选择刚才创建的分区,继续

  5. 第一次重启的时候关闭虚拟机,注意是关闭不是暂停
  6. VirtualBox菜单/管理/虚拟介质管理,选择“Windows.vdi”,复制,文件格式选择vdi,固定大小,命名为“Windows_copy”
  7. 上一步复制完成后,打开终端,进入复制后文件所在目录,输入下面内容,转换虚拟磁盘:

     sudo VBoxManage internalcommands converttoraw Windows_copy.vdi Windows.raw 
  8. 转换结束后,终端接着输入下面内容,挂载虚拟磁盘:

     sudo hdiutil attach -imagekey diskimage-class=CRawDiskImage Windows.raw 

创建Bootcamp分区

  • 使用Bootcamp助理创建Bootcamp分区(需要Windows安装光盘,据说加载个镜像就可以,未测试,我用的是Bootcamp助理制作的USB安装盘)
  • 关于如何通过Bootcamp助理制作USB Windos安装盘(适用于Bootcamp 5):

    • Finder 找到 /应用程序/实用工具/Boot Camp 助理,显示包内容
    • 复制Contens/Info.plist到桌面,修改“PreUSBBootSupportedModels”下面的“MacBookPro8,3”为“MacBookPro7,3”,移动回原来的位置,替换。
    • 重启Bootcamp助理,可以看到制作USB Windos安装盘的选项
  • 创建Bootcamp分区后记得重启,你可以尝试不重启,但是可能会有个小问题,导致下面的步骤报错,重启能简单解决。

将虚拟磁盘镜像写入Bootcamp分区

  • 打开Winclone,应该可以看到一个Untitled,一个Bootcamp
  • 选择Untitled,写映像…,一切操作选择默认选项
  • 上一步完成后,选择创建好的winclone文件,选择Bootcamp分区为目的磁盘,恢复…,一切操作选择默认选项
  • 系统偏好设置,选择启动磁盘为windows
  • 重启进入Windows,继续安装,完成

参考链接:

Tagged with:  

继之前翻译的两篇[iOS]如何避免图像解压缩的时间开销iPad 3 图片解压缩测试,Cocoanetics又在iPhone 5发布后测试了一下iPhone 5的图片解压缩能力:iPhone 5 Image Decompression Benchmarked。这次还是挑重点翻译一下,以后的文章如果没什么技术因素只是单纯跑分的话就不继续翻译了,大家自行到Cocoanetics上看数据就行了,这些数据对我们评估应用的性能还是很有帮助的。


以下为译文:

我们希望在水果每个新的CPU的重复我们的benchmark,这次测试中,我们对benchmark进行了一些改进,将之前打印log的NSLog替换成了CFAbsoluteTime,NSLog本身每次都要消耗多大50ms的时间,新的方法更加精确,不会包含打印log本身的时间。

测试结果:iPhone 5 是上代处理器性能的2倍

我们之前在较早的iOS设备上运行benchmark的结果已经表明GPU性能的提升并不影响我们的测试结果,同时,渲染速度的提升看起来完全是CPU的功劳,这一点也也被一个在WWDC的苹果工程师证实,这位工程师告诉我所有的UIKit图片解压缩都依靠CPU完成。

唯一一个让图片解压缩由GPU完成的方法是使用CoreImage,但问题时虽然解压缩更快了,但将解压缩侯的图片传递到CALayer上并进入渲染树(render tree,这个概念文档和WWDC里都讲过,CALayer部分,不清楚的可以看看,基本就是字面意思)仍是一个瓶颈。所以通过CoreImage解压图片仅当你能够将它们保持在GPU的时候才有效果,比如视频处理以及使用作为3D纹理使用。

虽然我们的benchmark只关注CPU,但是我们仍认为这是一个评估每代CPU性能的一个有效方法。

iPhone 5 测试结果:

128x96

 

256x192

 

512x384

 

1024x768

 

2048x1536

 

iPhone 5对PNG的解压缩能力提升明显,优化过(crushed)的处理时间仅仅比未优化的(uncrushed)快一点,差距很小。但对于较早的设备来说,二者差距很明显。

总结:

A6处理器支持VFPv4,一个特殊的并行浮点操作指令集,VFP是“Vector Floating Point”的缩写,就是这个指令集的出现让Anandtech得出结论:A6是水果第一个完全自家设计的SoC(The iPhone 5's A6 SoC: Not A15 or A9, a Custom Apple Core Instead),虽然基于ARM 7。

如果我理解正确,那么VFP性能提升来自其中的32个寄存器(比VFPv3多一倍),这意味着可以同时处理两倍的浮点数,从而揭示了A6两倍浮点性能的提升,这同时也对降低电量消耗有帮助。

水果为iPhone 5自主设计的硅片让它以2倍的性能轻松战胜了前代设备。

–以上–

Tagged with:  

事先声明:

  1. 能用傻瓜式的API还是去用傻瓜式吧,MediaPlayerFramework基本都能满足正常的音频播放需求。本文所涉及的内容用这些是实现不了的。

  2. 如果你指望本文是一个关于标题中提到的东西的教程,那我建议你还是关掉这个标签去翻文档吧,我看过的几个市面上容量仅有一篇博文那么多的关于这些方面的教程都没有什么实际意义,真的想学就踏踏实实去看文档,没有比文档对这方面讲的更清楚的了。除了文档建议补一下WWDC 2010 2011的相关Session,此外就是几个官方Sample。遇到问题了尤其是设计底层的API时,基本只有一个地方回答的比较靠谱,就是Apple Mailing List,另外StackOverflow也不错。本文仅仅回提供一些思路和链接,给后来的人指一个大概的方向。

以下进入正文:

记得小时候mp3刚开始流行那阵,那时候WINAMP称霸一时,虽然当时对音乐还没有什么感觉,但对WINAMP自带的众多可视化效果甚是着迷,直到今天终于有能力自己实现一个了,做了个Demo录了个视频发出来,效果较挫,嫌弃不好的就当听歌了,原曲来自niconico,sm18028378。youku视频如下:(也有u2b新浪

想实现视频中的效果有一种简单的方法和一种困难的方法

先说简单的:首先你要有音频的源文件(这不是废话么),压缩非压缩均可,格式要在iOS设备支持的范围内,另外就是对AudioQueue工作方式比较熟悉,如果你能成功用AudioQueue播放音频的话基本就成功一半了,其实从一定程度上来说AudioQueue也算是半傻瓜式的API,接下来就是获取level meter,这里提示点关键东西,自己看下就明白了:

  • AudioQueueGetProperty, kAudioQueueProperty_CurrentLevelMeterDB
  • Sample Code, Speak Here,MeterTable.h

这样就可以在每次OpenGL刷新的时候获取level meter值,
关于最后的OpenGL展示集中在后面讲

比较困难的方法就是用Audio Unit了,说困难其实不是API多么难用,而是越到底层我们做的工作就越多,真的要实现一个播放器的话我们就要人工处理音乐播放器的各种逻辑,时间控制,播放列表控制等等,实现功能以后还有一段很长的路要走。

进入正题,要求音频文件最好是liner PCM,也就是无损无压缩的,因为Audio Unit只能接受liner PCM的数据,如果是压缩格式还需要转换,转换格式可能就需要用到ExtAudioFile,或者AudioFile+AudioConverter,前者比较方便,后者相当复杂,尤其是要考虑对各种格式的兼容问题,需要很多判断,很多分支,对音频格式的知识要求也相当高,参考Sample Code,ConvertFile,这是一个控制台应用,代码很多,耐心看。如果格式的问题回避或解决了,就可以进入AudioUnit部分了,这部分其实也不难,基本就是设置一大堆属性,然后在回调函数里面提供数据就可以了,看官方文档会对你有很大帮助。至于获取level meter,可以让音频数据通过一次MultiChannleMixer Unit(这个AudioUnit的使用可以参考下Sample Code,Mixer Host),不过我们只用一个输入口,不真正mix,只是为了借用其kMultiChannelMixerParam_PostAveragePower,获取和kAudioQueueProperty_CurrentLevelMeterDB相同的东西,然后沿用MeterTable.h,转换成更直观的数据,最后显示出来。

如果做更复杂显示效果,可能就需要fft了,也就是将时域转换成频域,根据频率数据来实现比如做那种频谱图(Spectrometer),可参考的资料:

FFT部分:

  • 理论基础,随便找个教程看吧,这方面有很多
  • 实际一点的看下Create a FFT Analyzer这个系列的前半部分,这个系列从头到尾讲了下Mac上一个显示频谱的AudioUnit的实现,但是iOS目前(5.x.x)并不支持自制的Audio Unit,所以即使你照着这个教程做出来东西也不能用到iOS平台上,不过对实现过程还是能有一个比较清晰的认识的。另外该教程也对学习路线有一明确指引。

AudioUnit部分:

  • Sample Code, AurioTouch2, Aurio Touch;前者用了AccelerateFramework,这个Framework里面有个硬件加速版的FFT,以及一些iOS 5才出现的API,但仅仅FFT部分还是能支持到iOS 4的,至于后者是纯用CPU算FFT,可以兼容iOS 4之前的系统。

接下来是显示部分:

这部分实在没什么可说的,对OpenGL ES我还是个小白,现在只是用了一个现成的Particle System,在每次draw view之前根据level meter改变了下各点的尺寸,然后就没有然手了。我也跪求高手指导如何才能做出漂亮的效果。

最后说下我的学习路线:

  1. WWDC 2010 2011关于Audio的所有Session,对Audio部分有一个直观的认识,
  2. 官方文档,从Multimedia Programming Guide开始,所有关于Audio的分支文档,从表层到底层都有个了解,明确实现某种功能需要从哪一层入手,这部分内容很多,拖拖拉拉看至少4天吧。
  3. 期间参杂之前提到的所有Sample Code,文章,以及StackOverflow,Mailing list中的各种问题。

其实有一些关于CoreAudio的书也不错,但是太大部头了,短期纯为了实现功能看书太慢了。

本文Demo的代码我就不放出来了,没有太大价值,只能起到急功近利的效果,对学习没有什么帮助,另外就是代码都是拼凑了,结构混乱,还是不要拿出来让人喷了。

PS.题外话,关于Mac向Youku,新浪传视频,Mac下的转码软件基本都是个悲剧,前面用的视频是用Video Converter-Clone2Go这个转换的,这个软件虽然UI非常悲剧,但至少能设置下音频,视频的bit率,这对于要保证音频质量来说是必不可少的(youku没爆音真是很幸运)。另外就是对比新浪和youku,新浪的审核转码都比youku快不少,音质也明显比youku高,至于u2b各方面都非常好,也就是对于我们这些国内的人上传超费劲。

祝各位玩得愉快。

–以上–

Tagged with:  

哦… 首先希望闲来无事发现此文的人,可以回复报下你们一天中XCode Crash的次数,附上版本,大家比比谁受气受得多。(我:4.3.1 10多次crash)

目前测试Bug的环境:XCode 4.3.1,用svn作为版本控制
PS. 4.3可能也有这个Bug,4.2.1貌似没有,但不排除svn下的其他毛病

Bug产生过程:

  1. Project处于svn版本控制之下
  2. (可有可无)向Project中添加文件,并加入版本控制(A)
  3. 从项目中Delete某个文件,并选择Move to trash
  4. 编译的时候会有Wraning提示缺少了之前删除的那个文件(Missing File xxxx)

虽然这是一个误报,对编译没有任何影响,但是有Warning看着就心烦,而且如果删除了很多文件就会造成满屏Warning,既影响心情,又影响工作。

下面是几种解决方法

  1. 如果你对XCode 4.3+移植抱有敌视态度,那么你的/Developer或者废纸篓里面可能还有XCode 4.2.1,用4.2.1进行删除的步骤,那些Warning就不会出现。
  2. 完全用XCode 4.3.1 + Terminal解决
    1. 对那些你要删除的并且有"A"(版本控制)标记的文件,右键 >> Source Control >> Discard Changes…
    2. 右键 >> Source Control >> Ignore
    3. Delete并Move to trash

这样编译就不会有warning,如果你删除的是一个文件夹,那么还会提示Missing这个文件夹,再或者上一步删除文件之前忘记设置ignore了,warning就会出现,用终端解决:
1. 打开终端,cd到项目主目录下
2. svn status,可以看到那个Warning的文件(夹)
3. svn rm xxxxx 从版本控制里去掉那个文件(夹)

这时再编译就不会有那个warning了,世界终于清净了

总结:XCode 4+对svn支持非常废,经常有莫名其妙错误,要手动解决,所以一个更方便的方法就是不用svn,现在git这么火,不怕开源的项目就直接放到github上得了,保密的就放到Bitbucket上,或者找个服务器自己搭一个,总比折腾svn这些破事好,不过广大开发者最终还是应该鄙视苹果的,照这么下去苹果还是回到几年前的小众状态比较好,那时无论是SL,还是Xcode 3都是精品中的精品。现在苹果到了风口浪尖上,做东西都很浮躁,各种神奇,傻X的Bug层出不穷,老乔也不托梦骂他们几句。

懒得喷了,话说中午写的这篇,晚上发的,发现XCode 4.3.2出了,不知道老乔显灵没,如果还Crash记得上App Store评一星啊!link

更新:恭喜,XCode 4.3.2下这个Bug依旧

–以上–

Tagged with:  

Obj-C用起来真是各种happy,比如现在有这样一种情况:有一个类,我们希望它能响应一个消息(message),但是这个类没有相应的方法(method),而你又偏偏不能重写/继承这个类。这时我们可能会想到,能不能动态地给类添加一个方法呢?感谢Obj-C,仅需简单几步就能实现。

先看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
 
@interface EmptyClass:NSObject
 
@end
 
@implementation EmptyClass
 
@end
 
void sayHello(id self, SEL _cmd) {
    NSLog(@"Hello");
}
 
- (void)addMethod {
    class_addMethod([EmptyClass class], @selector(sayHello2), (IMP)sayHello, "v@:");
 
    // Test Method
    EmptyClass *instance = [[EmptyClass alloc] init];
    [instance sayHello2];
 
    [instance release];
 
}

我们首先定义了一个EmptyClass,继承NSObject,没有任何自带方法,接着定义了一个函数。这里提一句,Obj-C的方法(method)就是一个至少需要两个参数(self,_cmd)的C函数,这个函数仅仅输出一句Hello。接下来在addMethod方法中,我们调用class_addMethod()为EmptyClass添加方法,class_addMethod()是这样定义的:

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

参数说明:

cls:被添加方法的类

name:可以理解为方法名,这个貌似随便起名,比如我们这里叫sayHello2

imp:实现这个方法的函数

types:一个定义该函数返回值类型和参数类型的字符串,这个具体会在后面讲

接着创建EmptyClass的实例,调用sayHello2,运行,输出Hello,添加方法成功。

接下来说一下types参数,
比如我们要添加一个这样的方法:-(int)say:(NSString *)str;
相应的实现函数就应该是这样:

1
2
3
4
int say(id self, SEL _cmd, NSString *str) {
    NSLog(@"%@", str);
    return 100;//随便返回个值
}

class_addMethod这句就应该这么写:

1
class_addMethod([EmptyClass class], @selector(say:), (IMP)say, "i@:@");

其中types参数为"i@:@“,按顺序分别表示:

i:返回值类型int,若是v则表示void

@:参数id(self)

::SEL(_cmd)

@:id(str)

这些表示方法都是定义好的(Type Encodings),关于Type Encodings的其他类型定义请参考官方文档

最后调用say:方法:

1
2
int a = [instance say:@"something"];
NSLog(@"%d", a);

输出something和100。

关于本文所涉及内容的详细信息请参考Objective-C Runtime Reference

本文参考了:

推荐去看看
—以上—

Tagged with:  

这是一篇译文,(原文"Avoiding Image Decompression Sickness"在此),原文是我看过的非常不错的一篇关于iOS图片显示的一些文章,解决了我的一些疑惑和问题,因此翻译过来分享,为保证一定的通顺性其中一部分内容与原文有些许出入,但我尽量保证了意思的一致性,欢迎指正批评,横线之间为译文,略挫,见谅:


当开始iCatalog.framework的工作时,我发现使用大尺寸图片会引起一些恼人的问题,“大”意味着这个图片有足够大的分辨率(1024×768)来覆盖iPad的整个屏幕,或者覆盖未来Retina Display iPad(如果有的话)的双倍分辨率(2048×1536)屏幕。

想像一个杂志类型的App,一个分页的UIScrollView,每页显示一个UIImageView,一旦某一页进入屏幕区域你就要为这个页创建或者重用一个UIImageView并把它放到scrollView的当前显示区域,即使这个页只有一个像素进入到屏幕区域,你还是要做这些工作。这在模拟器上运行得非常好,但在真机上进行测试,你会发现每次进入下一页时都会有一个明显的延迟。这个延迟来自于将图片从文件解压缩并渲染到屏幕上这一系列的工作。不幸的是UIImage仅在图片将要显示的时候做这个解压工作。

因为添加一个view到当前的view层次结构中必须在主线程上进行,所以图片的解压缩和之后渲染到屏幕上的工作也在主线程进行,这就是这个延迟产生的原因,这个问题也可以在store里的其他有类似这种效果的app中发现。

一般我们使用的图片有两种主要格式,jpeg和png。Apple通常推荐你使用png作为用户界面的图片格式,这些图片会被一个叫pngcrush开源的工具优化(译者注:这个工具就在/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/pngcrush ),这样对于iOS设备就可以在显示时更快地进行解压和渲染。iPad平台上首批出现的杂志应用,比如Wired,就曾用过png作为杂志内容图片的格式,这导致了这个应用的某一版本大小超过了500MB(link)[http://www.cocoanetics.com/2010/05/saturday-morning-breakfast-wired-emag/]

虽然png格式的图片会被事先优化好,但是这并不意味着在所有情况下png都是最佳的图片格式,png对于那些app中自带的图片来说非常好,但是对于要从internet上down下来的图片来说又会怎样呢。png和jpeg这两种格式都有各自的优缺点:

png格式的图片有alpha通道,jpeg则没有。png无损压缩,jpeg允许你选择0-100%的压缩质量。如果需要alpha通道(透明),就只能用png格式。但是如果你不需要一个完美的图片,就可以使用jpeg格式,jpeg格式会忽略那些你看不到的信息,对于大部分的图片可以使用60-70%的压缩质量而不对图片造成明显的影响,对于比如文字那样有"sharp pixels"的图片就可能需要较高的压缩质量,对于照片可以使用较低的压缩质量。

来看一下一个图片的空间消耗:

  1. 磁盘空间或者通过internet传输所消耗的空间
  2. 解压缩空间,通常是长X宽X高X4字节(RGBA)
  3. 当显示在一个view中时,view本身也需要空间来存储layer

对于这里的第一个问题,有一个可能的优化方法:将压缩的文件拷贝到内存中不如映射到内存中,NSData有能力来假设一块磁盘空间是在内存中的,这样当访问这个图片时实际上就是从磁盘访问而不是从内存。据说CGImage知道哪种访问方式是最高效的,UIImage只是将CGImage封装了一下。

对于“将这些像素显示到屏幕上最快要多久?”这个问题,显示一个图片所消耗的时间由以下三个因素决定:

  1. 从磁盘上alloc/init UIImage的时间
  2. 解压缩的时间
  3. 将解压缩后的比特转换成CGContext的时间,通常需要改变尺寸,混合,抗锯齿工作。

要逐一解答各个问题,我们需要一个benchmark来测量。

测试环境和测试内容

Continue reading »

Tagged with: