Gone With the Wind

伟大的单元测试

什么是单元测试?

在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

举个例子

待测试类

1
"use strict";

class Car {

  constructor() {
    this.status = "stop";
  }

  // 开车
  start() {
    this.status = "start";
    this._lockDoor();
  }

  // 停车
  stop() {
    this.status = "stop";
    this._unlockDoor();
  }

  // 锁车门
  _lockDoor() {
    console.log( "door locked" );
    this._lockFrontDoor();
    this._lockBackDoor();
  }

  // 解锁车门
  _unlockDoor() {
    console.log( "door unlocked" );
    this._unlockFrontDoor();
    this._unlockBackDoor();
  }

  _lockFrontDoor() {
    console.log( "front door locked" );
  }

  _lockBackDoor() {
    console.log( "back door locked" );
  }

  _unlockFrontDoor() {
    console.log( "front door unlocked" );
  }

  _unlockBackDoor() {
    console.log( "back door unlocked" );
  }

};

module.exports = Car;

以逻辑单元为最小单位的测试

1
"use strict";

const assert = require( "assert" );

const Car = require( "./sample" );

describe( "test Car", function() {

  it( "start car", function () {
    let car = new Car();

    assert.equal( car.status, "stopped" );

    let locked   = false;
    // MOCK
    car._lockDoor = function () {
      locked     = true;
    }

    car.start();

    assert.equal( car.status, "running" );
    assert.equal( locked, true );

  } );

  it( "lock door", function () {
    let car = new Car();

    let frontDoorLocked = false;
    let backDoorLocked  = false;
    car._lockFrontDoor  = function () {
      frontDoorLocked     = true;
    };

    car._lockBackDoor   = function () {
      backDoorLocked      = true;
    };

    car._lockDoor();

    assert( frontDoorLocked, true );
    assert( backDoorLocked, true );

  } );

} );

以业务单元为最小单位的测试

1
"use strict";

const assert = require( "assert" );

const Car = require( "./sample" );

describe( "test Car", function() {

  it( "start car", function () {
    let car = new Car();

    assert.equal( car.status, "stop" );

    let _lockFrontDoor  = car._lockFrontDoor;
    let _lockBackDoor   = car._lockBackDoor;

    let frontDoorLocked = false;
    let backDoorLocked  = false;

    car._lockFrontDoor  = function () {
      frontDoorLocked     = true;
      _lockBackDoor();
    };

    car._lockBackDoor   = function () {
      backDoorLocked      = true;
      _lockFrontDoor();
    };

    car.start();

    assert.equal( car.status, "running" );
    assert.equal( frontDoorLocked, true );
    assert.equal( backDoorLocked, true );

  } );

} );

一些单元测试原则

  • 单元测试必须由最熟悉代码的人(程序的作者)来写。
  • 单元测试应该在最低的功能/参数上验证程序的正确性。
  • 单元测试过后,机器状态保持不变。
  • 单元测试要快(一个测试运行时间是几秒钟,而不是几分钟)。
  • 单元测试应该产生可重复、一致的结果。
  • 独立性,单元测试的运行/通过/失败不依赖于别的测试,可以人为构造数据,以保持单元测试的独立性。
  • 单元测试应该覆盖所有代码路径,包括错误处理路径,为了保证单元测试的代码覆盖率,单元测试必须测试公开的和私有的函数/方法。
  • 单元测试应该集成到自动测试的框架中。
  • 单元测试必须和产品代码一起保存和维护。

单元测试的好处

  • 保证现有代码的功能都是正常的。
  • 若不清楚代码而修改了一个逻辑,运行一下单元测试就知道影响到了什么地方。
  • 反向推动代码做到一个单元的功能最小化。
  • 写单元测试的过程中可以重新梳理自己的代码,找出bug。
  • 重构系统而不改变功能时,对照单元测试来写自己的功能就可以,即使之前的代码不是自己写的。

单元测试不好的地方

  • 时间成本,直接导致人力成本上升。不过也有可能前提代码质量上的提升可以减少后期很多修BUG的时间。
  • 有些外部依赖较多并且逻辑相对简单的时候,初始化外部依赖或者mock的精力复杂度可能已经超过了业务代码本身。
  • 设计影响。有时候,为了方便测试,甚至已经影响到你的业务代码的逻辑。
  • 需求快速变化期间。有时候需求变化非常快速,单元测试会降低业务变化的敏捷度。

MOCK

MOCK 简单来说就是纸老虎。

业务代码

1
"use strict";

const DB = require( 'database' );

class Mock {

  getList() {
    const result = DB.query( "select * from table" );
    for( let i = 0; i < result.length; i ++ ) {
      // do somting
    };

    return result;
  }

};

module.exports = Mock;

测试代码

1

const Mock = require( './mock' );

const DB   = require( 'db' );

describe( "test Mock", function () {

  it( "get list", function () {
    DB.query = function ( sql ) {
      return [ 1, 2, 3 ];
    };

    const car    = new Car();
    const result = car.getList();

    assert.equal( result, [ 1, 2, 3 ] );
  } );

} );

为什么我的代码需要mock?

优点

  • 可以不用去关注一些底层支撑系统,例如DB,如果不MOCK,那么还需要初始化一个DB,使用前使用后都要去清理环境,给自己擦屁股。
  • 可以更加关注于自己业务代码的业务逻辑,例如去初始化一个DB,需要很多配置信息。
  • 不会因为数据库的故障导致自己的单元测试失败。
  • 隔绝别的模块,别的模块即使只申明了接口,也可以正常跑完自己的单元测试。

缺点

  • 如果mock对象很复杂,属性很多,方法也很多,那就需要自己去录制相当多的工作去实现这个mock.
  • 有些语言难以mock。一些强类型语言和编译型语言对于mock还是比较难的。
  • mock会掩盖某些环境。比如各种输出的时间格式会根据用户的环境而定,不同的地域也会有不同的格式,这个时候用mock返回自己所在地区或者自己所在环境的格式,看似单元测试全部通过,实则埋下了隐患。
  • 在不清楚被mock对象的所有边界行为时,可能会导致程序没有处理一些边界问题而超出预期。

TDD

TDD(Test Drive Development),单元测试带来的一个非常好的开发方式。TDD思想让你在写代码之前,就先写单元测试,这个时候可以让你把整个代码从整体层面上运行起来,然后再去实现每个方法具体的功能。

优点

  • 在设计阶段,划分方法,划分类的时候,无需关注某个类或者方法的具体的实现,只要达到功能即可。
  • 在实现某个类或者某个方法的时候,无需关注这么设计是否合理,直接按照方法名实现方法即可。

BDD

BDD指的是Behavior Drive Development,也就是行为驱动开发。这里的B并非指的是Business,实际上BDD可以看作是对TDD的一种补充,当然你也可以把它看作TDD的一个分支。因为在TDD中,我们并不能完全保证根据设计所编写的测试就是用户所期望的功能。BDD将这一部分简单和自然化,用自然语言来描述,让开发、测试、BA以及客户都能在这个基础上达成一致。因为测试优先的概念并不是每个人都能接受的,可能有人觉得系统太复杂而难以测试,有人认为不存在的东西无法测试。所以,我们在这里试图转换一种观念,那便是考虑它的行为,也就是说它应该如何运行,然后抽象出能达成共识的规范。如果你用过JBehave之类的BDD框架,你将会更好的理解其中具体的流程。这里我推荐一篇具体阐述的文章。

DDD

Deadline Drive Development.

软件开发进度表

时间 汇报进度 真实进度
第一天 20% 5%
第二天 50% 10%
第三天 70% 10%
第四天 80% 10%
第五天 90% 15%
第六天 100% 100%

iOS中实现高精度定时器的方法

背景

最近自己业余时间在开发一个录音软件,要画一个声音的音频图,大概类似下面的样子:

在一秒钟内要取大概20次当前声音的大小,用以绘制成一幅图,那么首先,想当然的当然是用NSTimer了,写了代码如下:

1
// 当然这里把每秒钟需要的次数设置成一个变量会更好。
[NSTimer scheduledTimerWithTimeInterval:1 / 20
                                repeats:YES
                                  block:^(NSTimer * _Nonnull timer) {
                                    // do somting.
                                  }];

block里面去取了一下当前音量,绘制成了一个图,代码运行起来没问题,音量正确得取到了,并且也绘制在了图上面,乍一看发现没什么问题,等了十几秒再回头看,发现貌似时间过得很慢,明明感觉已经起码过了十秒,但是录音图上停留在6秒7秒的样子,很疑惑,在block中,加了一个Log,就看出缘由了。

设置的明明是0.05秒执行一次,但是从图中来看,基本都是隔了0.1秒才执行一次,很少有0.5秒才执行一次,NSTimerrunloop运行的时候执行,runloop中有其他任务时势必会影响到NSTimer,但是没想到影响会那么大,查阅了官方文档后发现,iOS设备中,NSTimer的精度在0.1秒左右,看来是需要另寻他法了。

平均误差:18.11

while循环

具体思路就是新开一个线程,然后跑一个while循环去检测,代码如下:

1
dispatch_queue_t timerQueue = dispatch_queue_create("timer", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(timerQueue, ^{
        while (YES) {
            usleep( 50000 );
            NSLog(@"timer");
        };
    });

平均误差:65.33毫秒

貌似比NSTimer还不靠谱,经过测试,发现精度基本上在200MS左右。

CADisplayLink的调用是根据屏幕刷新率来的,通常iOS设备的刷新率在60HZ,那么一秒钟就能够被调60次。

1
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(testDisplayLink)];
link.paused = NO;
[link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

平均误差:0.002(在每秒60次调用的情况下)

相当精确,在固定刷新率下,精度基本在0.001毫秒的样子,但是测试发现CADisplayLink有以下问题:

  1. 时间间隔只能是 1 / 60,没法自定义,可以设置 1 / 60的倍数。
  2. 首先精度在 1 / 60秒的样子,无法再精确了,在此基础上,调用间隔非常精确。
  3. 全局只能有一个CADisplayLink实例在运行。
  4. 屏幕在熄灭状态下无法调用此方法。

dispatch_after

通过递归得调用这个函数来实现

1
dispatch_after(
                   dispatch_time(
                                 DISPATCH_TIME_NOW,
                                 (int)(0.05 * (double)NSEC_PER_SEC)
                                 ),
                   dispatch_get_main_queue(), ^{
                       NSLog(@"dispatch after");
                       [self after];
                   });

平均误差:12.17毫秒。

目前看来是误差最小的了,如果不算CADisplayLink的话。

官方提供的高精度解决方案

官网地址:https://developer.apple.com/library/content/technotes/tn2169/_index.html

平均误差:0.34毫秒

大概测试了一下,间隔在1毫秒的时候,误差在0.7毫秒,间隔在5毫秒的时候误差0.36,间隔在2毫秒的时候误差在0.7毫秒,间隔在4毫秒的时候误差为0.3毫秒,间隔在3毫秒的时候误差在0.49

间隔 误差
5ms 0.36ms
4ms 0.3ms
3ms 0.49ms
2ms 0.7ms
1ms 0.7ms

总体来说,这种方式的精度在5毫秒左右,5毫秒已经能满足绝大多数需求了。

获取最小有效像素区域

背景

最近在做一个需求是对页面某一块区域截图,本身没什么,但是发现需要截图的区域非常大,然后里面的内容通常只有一小点,这样我把整张图片拿出去就会发现只有中间一点点有东西,其他地方都是空白的,用户体验就会比较差了,记得PhotoShop里面有一个功能是能截取整张图片的最小有效像素区域,本来以为canvas自带有这种功能,没想到还是要自己实现的,虽然功能很简单,但是网上也没找着相应类库,可能是我搜索的姿势不对,或者这个需求实在太简单了,没人封装成类库了,虽然完全没做过图像处理,也开始找一些资料。

文字描述比较抽象,放上一张图片。

原始图片

处理后图片

canvas

网页内容是用Html2Canvas把DOM截图成了canvas,原理不是很清楚,大概是读取DOM样式然后在canvas里面重新画一遍,这不是重点,这个时候我们已经拿到画成了的canvas了,改怎么处理呢?

1
var canvas  = document.getElementById( 'canvas' );
var ctx     = canvas.getContext( '2d' );
var imgData = ctx.getImageData( 0, 0, canvas.width, canvas.height );

通过getImageData方法就能够拿到canvas的原始像素点信息了,四个参数描述了一个矩形,返回的像素点信息就是这个矩形内的信息。

返回的是一个Uint8ClampedArrayUint8ClampedArrayJavaScript中的类型数组,具体的可以参看这里,简单来看就是这个数组中的元素都是unsigned int并且都是8位的,所以取值范围是0~255,非常适合用来存放ImageData,而且类型数组因为类型都是确定的,使用起来比普通数组速度上会快很多。

ImageData

拿到了原始的像素点数据,来看看数据长什么样子,首先看数组长度,假设是一个100X100的图片,那么理论上像素点的数量是100100,也就是10000个,拿到ImageData的数组看长度其实有40000,简单猜测一下大概是10000 rgba,看具体的数组内容很容易就能够验证我们的猜想。

一个典型的ImageData大概长这个样子,这是一个从左往右,从上到下按顺序排列的一个数组:

1
imageData = [ 
              0,   0,   0,   0,
              255, 255, 255, 1,
              122, 122, 122, 0.5,
              ....
            ];

如何获取最小像素区域?

要实现这个功能的本质,其实就是如何寻找最上、最右、最下以及最左四个点(P1,P2,P3,P4),有了这四个点,我们就能画出一个最小矩形,把这个图像框在里面。

一维组数映射到二维空间

根据数组下标计算当前点在二维空间的坐标,方法其实很简单。

看下面这张图,可以认为这是一张 5X4 的一张图片,那么总共就有20个像素点,每个格子里的0-19数字就意味着这是在一维数组里面的下标N,由此可得

X = N % width;
Y = Math.floor( N / width );

pixel

全数组扫描

首先能想到的最简单的办法,就是循环整个数组,当alpha值大于0的时候,即可判断这个点是一个有效像素点,然后整个数组循环完成之后,即可知道得到P1,P2,P3,P4四个点。

以下是实现代码:

1
function getEffectiveRect( imageData, width, height ) {
  // 上点, Y值最小
  var p1 = [ 0, height ];
  // 右点, X值最大
  var p2 = [ 0, 0 ];
  // 下点, Y值最大
  var p3 = [ 0, 0 ];
  // 左点, X值最小
  var p4 = [ width, 0 ];

  var data    = imageData.data;
  var number  = 0;
  console.time( 'image1' );
  for ( var i = 0; i < data.length; i ) {
    number++;
    var r = data[ i++ ];
    var g = data[ i++ ];
    var b = data[ i++ ];
    var a = data[ i++ ];
    if ( a === 0 ) {
      continue;
    }
    var x = number % width;
    var y = ( number - x ) / width;
    if ( p1[ 1 ] > y ) {
      p1[ 0 ] = x;
      p1[ 1 ] = y;
    }

    if ( p2[ 0 ] < x ) {
      p2[ 0 ] = x;
      p2[ 1 ] = y;
    }

    if ( p3[ 1 ] < y ) {
      p3[ 0 ] = x;
      p3[ 1 ] = y;
    }

    if ( p4[ 0 ] > x ) {
      p4[ 0 ] = x;
      p4[ 1 ] = y;
    }
  }

  return { p1, p2, p3, p4 };
}

从矩形的四个角分别去寻找

具体的思路如下图:

from-rect

理论上只要有效像素区域越大,那么需要计算的量就越少,速度也就越快,以下是我的实现代码:

1
function getEffectiveRect2( imageData, width, height ) {
  var data = imageData.data;

  console.time( 'image2' );
  var p1 = [];
  var p2 = [];
  var p3 = [];
  var p4 = [];

  // 寻找P1
  var len = 0
  for ( var i = 0; i < data.length / 4; i ++ ) {
    len   += 7;
    var a = data[ len ];
    if ( a !== 0 ) {
      var x = i % width;
      var y = ( i - x ) / width;
      p1    = [ x, y ];
      break;
    }
  };

  //寻找P3
  var len = 0;
  for ( var i = data.length / 4 - 1; i >= 0; i -- ) {
    len  += 7;
    var a = data[ len ];
    if ( a !== 0 ) {
      var x = i % width;
      var y = ( i - x ) / width;
      p3    = [ x, y ];
      break;
    }
  }

  // 寻找P2
  for ( var i   = width - 1; i >= 0; i -- ) {
    if ( p2.length ) {
      break;
    }
    var a = p1[ 1 ];
    var b = p3[ 1 ];
    var len = i + a * width;
    for ( var j = a; j < b; j ++ ) {
      len += width;
      // var point = i + j * width;
      var point = len;
      var a = data[ point * 4 + 3 ];
      if ( a !== 0 ) {
        var x = point % width;
        var y = ( point - x ) / height;
        p2    = [ x, y ];
        break;
      }
    }
  };

  // 寻找P4
  for ( var i = 0; i < width; i ++ ) {
    if ( p4.length ) {
      break;
    }
    for ( var j = p1[ 1 ]; j < p3[ 1 ]; j ++ ) {
      var point = i + j * width;
      var a = data[ point * 4 + 3 ];
      if ( a !== 0 ) {
        var x = point % width;
        var y = ( point - x ) / height;
        p4    = [ x, y ];
        break;
      }
    }
  }

  return { p1, p2, p3, p4 };
}

将两种方法分别去计算最开头的图片,两种方式的计算时间分别是:

1
image1: 10.83ms
image2: 15.32ms

结论

方法2引入的复杂的逻辑反而降低了计算时间,逻辑最简单的全数组扫描比第二种方式快了30%,因为涉及到的东西比较多,目前还不清楚到底是什么原因导致第二种方式速度如此慢,测试了一下,只有当一整张图片几乎全部有像素点的时候,两种方法的速度才差不多。后续我会继续寻找原因,猜测可能是方法2的计算量比方法1多,而方法1更多的是比较和赋值。

计算机眼中的数字

0.1 + 0.2 = ?

即使是没上过学的人,也知道答案是0.3,但是,在看似无所不能,计算速度远超人类的计算机来说,答案就不是0.3了。看看chrome下的结果:

1
0.1 + 0.2

0.30000000000000004

再看看python:

1
Python 2.7.10 (default, Jul 30 2016, 18:31:42) 
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 0.1 + 0.2
0.30000000000000004
>>>

再试试C语言:

1
#include "stdio.h"
 
  int main(){

  double a = 0.1 + 0.2;
  printf( "%.17lf", a );

}
1
0.30000000000000004

qustion

产生原因

对于数字的存储绝大多数语言遵循的都是IEEE 754规范,简单来说就是一些革命先烈告诉你当你在二进制计算机中想要一个浮点数的时候应该如何去表达。

首先,内存是有限的,所以一个数字,当它声明好了之后所占的位数是固定的,比如在C语言中,double类型所占的字节数是8,那么总共就是64位(可能某些奇葩编译器会有些差异),革命先烈们是怎么规定的呢?

double

有一个通用的计算公式:

float-cal

  1. (-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
  2. M表示有效数字,大于等于1,小于2。
  3. 2^E表示指数位。

举例来说,十进制的5.0,写成二进制是101.0,相当于1.01×2^2。那么,按照上面V的格式,可以得出s=0,M=1.01,E=2。
十进制的-5.0,写成二进制是-101.0,相当于-1.01×2^2。那么,s=1,M=1.01,E=2。

有几点需要注意:

  • 因为有效数字M的第一位必然是1,所以存储的时候就把这一位舍去了,可以空出一位用来存储,在计算的时候加回来就行了。

  • 指数E没有符号位,无法表示负数,所以在计算的时候减去一个半数,比如:double是64位,11位用来存储指数,假设你要保存指数10,那么就必须存储1023 + 10,也就是10000001001。ps: 以上讨论的都是64位浮点数,如果是float,也就是32位的浮点数,指数位数是8,保存指数10就是:127 + 10

对于指数E,还有一下三条更细的规则:

1
(1)E不全为0或不全为1。这时,指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
(2)E全为0。这时,浮点数的指数E等于1-127(或者1-1023),有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
(3)E全为1。这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);如果有效数字M不全为0,表示这个数不是一个数(NaN)。

好了,说完了背景,那么就可以来看看到底是什么原因了。

整数的二进制和十进制之间的相互转换,相信大家都会算,小数部分,很多人可能已经忘记如何计算了。

总体来说就是 乘二取整,比如0.1

1
0.1 * 2
0.2      >> 0
0.2 * 2
0.4      >> 0
0.4 * 2
0.8      >> 0
0.8 * 2
1.6      >> 1
0.6 * 2
1.2      >> 1

0.2 * 2
0.4      >> 0
0.4 * 2
0.8      >> 0
0.8 * 2
1.6      >> 1
......

0.1 变成了 0.0001 1001 1001 1001 .... 1001无限循环
0.2 变成了 0.0011 0011 0011 0011 .... 0011无限循环

对于计算机来说,0.1和0.2的二进制就变成了一个无尽数,其实大部分小数转换成二进制都是无穷数。于是,二进制的0.1和0.2就变成了:

竖线后面代表存储位数之外

1
0.0001 1001 1001 ...... 1001 | 1001   
0.0011 0011 0011 ...... 0011 | 0011

二进制相加之后变成了:

1
...... 1100 | 1100

由于存储位数限制,必须舍去竖线后面的有效数字,按照舍0进1的方式,就变成了 .... 1101,所以整个二进制数字就变成了:

1
0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101

转换成十进制就是:

1
0.30000000000000004440892098500626

就变成了我们最初所看到的结果。

至此,我们开头的问题已经有了答案,那么下面有几个引申问题:

  • JS中类型存储位是52位,那么可以准确表达最大的数字就是:Math.pow(2, 53), 也就是:9007199254740992,那么以下几个表达式的值是什么呢?

9007199254740992 + 1
9007199254740992 + 2
9007199254740992 + 3
9007199254740992 + 4

  • 0.1, 0.2, 0.3, 0.4 转换成二进制小数都是无穷的,那么以下表达式的结果如何呢?

0.1 + 0.2 === 0.3
0.1 + 0.3 === 0.4
0.2 + 0.4 === 0.6
0.2 + 0.3 === 0.5

iOS开发--1天从入门到放弃--基础

写了一段时间的iOS,把iOS和javascript在各方面做一下对比,记录一些比较基础的东西。

iOS开发包含哪些东西?

  • 二门语言:Objective-C,Swift
  • 一个基础框架:Cocoa Touch
  • 一个IDE:XCode

我需要准备哪些东西?

  • Mac电脑,身残志坚的朋友可以选择黑苹果。
  • 最好有一台iPhone,身残志坚的朋友没得选了,通常在模拟器里面没问题的东西在真机里面会有一些诡异的问题,特别是涉及到摄像头以及麦克风等等硬件的时候。

Objective-c

从名字上可以见,Objective-c就是对象化的C语言,所以支持C语言,同时扩展了一些面向对象的内容,比如:Class

Objective-c与类C类的语言的一个比较大的区别是,Objective-c使用消息传递模型,从如下代码可以看出:

假设我要调用car实例的move方法,

C++

1
car.move();

Objective-c

1
[car move];

从语法上可以看出一二,C++中确实是调用了car实例的move方法,如果car实例没有这个方法,那么编译期间就会报错,但是Objective-c仅仅是向car实例发送了一个move消息,代码只会在运行时去检查car实例到底有没有能力对move消息进行回应,从中可以看出Objective-c是一门动态语言。

新建一个iOS应用

打开xcode,选择红框中的项目。

创建项目

然后选择Single View Application,顾名思义:单页应用。

取个名字:IOS-dev-demo,然后选择保存的地方,一个新的iOS应用就创建好了。

创建文件夹

xcode会自动帮我们创建四个group,分别是IOS-dev-demoIOS-dev-demoTestsIOS-dev-demoUITestsproducts,作用如下:

  • IOS-dev-demo 项目的主目录,所有的项目业务代码都保存在这个group下。
  • IOS-dev-demoTests 单元测试目录。
  • IOS-dev-demoUITests UI测试目录。
  • Products 相关编译文件保存目录。

基本我们用到最多的就是我们的项目主目录了。

运行我们的项目

运行按钮

CMD+R 或者点击左上方三角形的运行按钮,即可在模拟器上运行,刚创建好只是一个空的应用。

搭建我们的应用

为什么用搭建这个词呢,因为Cocoa Touch已经帮助我们封装好了很多内部组件,和安卓不同,Cocoa Touch的内部组件通常都有一个比较好的交互和外观,典型应用是微信,微信基本都使用了内部组件,内部组件也最符合用户使用习惯。

搭建界面有两种方式:

  1. storyboard,可视化组件拖拉,类似于Dreamweaver,这种方式不做介绍了,快速搭建应用首选,也存在些许弊端,有兴趣的可以看看这篇文章
  2. 纯代码,类似于手写HTML代码。

如何添加组件?

拿一个button组件来说,在ViewController类中的viewDidLoad方法中添加如下代码:

1
UIButton *button = [[UIButton alloc] init];
button.frame     = CGRectMake(100, 100, 100, 50);
[button setTitle:@"按钮测试" forState:UIControlStateNormal];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];

[self.view addSubview:button];

重新编译运行一下我们的代码,就可以看到一个黑色的按钮在界面上了。

下面用浏览器的方式展示如何添加一个按钮,相信大家一定都非常熟悉了。

1
const button = document.createElement( 'button' );
button.textContent  = '按钮测试'
button.style.width  = '100px';
button.style.height = '50px';
button.style[ 'margin-left' ]   = '100px';
button.style[ 'margin-top'  ]   = '100px';
button.style.color  = 'rgb(0, 0, 0)'

document.body.appendChild( button );

对比一下两段代码:

1
// 创建一个组件
UIButton *button = [[UIButton alloc] init];                                ||| let button = document.createElement( 'button' );

// 设置组件的left, top, width, height属性
button.frame     = CGRectMake(100, 100, 100, 50);                          ||| button.style.width  = '100px';
                                                                           ||| button.style.height = '50px';
                                                                           ||| button.style.margin-left   = '100px';
                                                                           ||| button.style.margin-top    = '100px';

// 设置组件的'标题'属性
[button setTitle:@"按钮测试" forState:UIControlStateNormal];                 ||| button.textContent  = '按钮测试'

// 设置组件标题的颜色
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; ||| button.style.color  = 'rgb(0, 0, 0)'

// 将组件添加到当前画布上
[self.view addSubview:button];                                             ||| document.body.appendChild( button );

其实从逻辑上来看两段代码非常相似,唯一不同的仅仅是语法而已。

那么从系统层面看,两者如何?

首先来看看html

htmlc

再看看iOS

cocoac

响应UI事件

通常在页面上放置了一个按钮并不是为了展示信息,而是为了能让用户进行交互,那么来看看两者如何进行UI响应。

Javascript

1
button.addEventListener( 'click', function( event ) {
  alert( 'clicked' );
} );

拿到button对象,然后给这个button注册一个名字叫click的事件,再给这个注册方法一个回调函数,然后用户在这个按钮上单击的话,就能够触发这个回调函数了。

Objective-c

1
[button addTarget:self action:@selector(clickHandler:) forControlEvents:UIControlEventTouchUpInside];

调用button对象的addTarget方法,为UIControlEventTouchUpInside事件,也就是单击事件添加self也就是当前实例下的clickHandler作为事件处理函数。这里也可以选择别的类的方法,只要selector中的方法名能在addTarget对象中找到就行。
iOS中只能使用类的方法作为事件处理函数,没法使用类似匿名函数的block来进行事件处理。

回调

Javascript中发送一个http请求

1
let xhr = new XMLHttpRequest();
xhr.open( 'GET', 'http://xxxx.com' );
xhr.send();

xhr.onreadystatechange = function(){
  // do something
}

Javascript基于事件驱动,通常使用一个匿名函数来作为请求结束时候的回调函数。

Objective-c

1
NSURL *url = [NSURL URLWithString:@"http://xxx.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest: request completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) {
    NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
         // ...
     }];
[task resume];

Objective-c 相对来说发送一个请求的方式稍微有一些复杂,看到completionHandler这个参数,这在Objective-c中被称为block,是一种类似于Javascript中的回调函数,当然也可以使用typedef声明一个block在多个场景下使用。

关于block更多的使用奥秘,可以点击查看这篇文章

动画

数据请求回来了,那么就要开始渲染页面了,iOS有良好的动画运行机制,可以比安卓更流畅地运行动画,良好的动画效果可以带来更好的用户体验。

Javascript

JS中动画效果大致有如下几种:

纯粹的JS实现的动画效果应该就只有如下一种了

  • setTimeout,定时更新形变达到每秒24次即可实现基本的动画效果。

示例代码:

1
let width = 100;
setInterval( function() {
  width++;
  button.style.width = `${width}px`;
}, 16 );

更多更高效的动画效果可以借助HTML5CSS3来实现

  • requestAnimationFrame,保持和系统相同的刷新率,得到更好体验的同时,有更高的效率。
  • css中的transition-duration,效率非常高,可以直接调用GPU来进行渲染。

Objective-c

iOS中的动画主要是指Core Animation框架,这里是官方的文档

调用方式主要有以下三种:

  1. UIView 代码块调用
  2. UIView [begin commit]模式
  3. CoreAnimation中的类

简单贴一下最简单也最常用的 UIView 代码块调用的方式,其他方式的详细调用可以参考这里

1
CGRect newFrame = CGRect( 200, 200, 100, 50 );
[UIView animateWithDuration:1 animations:^{
  button.frame = newFrame;
}];

修改UI只能在主线程中执行,如果说要在回调函数中执行以上代码,需要嵌套在下面的代码中进行:

1
dispatch_async(dispatch_get_main_queue(), ^{
                
});

总结

有了以上这些东西,你能已经能把一个小功能呢给串起来了,但是iOS还有更加强大实用的各种组件,UINavigationControllerUITableViewControllerNSUrlSession,还有各种全局事件派发,多线程GCD,以及处理音视频的AVFoundation,更多功能,请看进阶篇。

数加Pai(机器学习平台)的上手体验

数加pai

数加pai是一个机器学习平台,提供可视化界面来操作算法,无需自己编写任何代码即可计算自己的数据。
如果你没有那么多数据也没有这种场景,那么选择一个外部算法平台也是一个不错的选择。

kaggle

一个类似阿里云天池的算法开发大赛平台,会定时发布各种任务,前几名有奖金,第一名$10W。

我们用这个平台上练手的项目来体验一下PAI。

找到首页中标着101的那一组,这些题目都是给你练手的,相对来说会比较简单一点,就选第一个,Digit Recognizer

题目分析

这个题目的主要任务的数字识别,很多手写的数字,左边导航栏home-data,进去可以下载本次的训练数据: train.csv和测试数据: test.csv

打开train.csv文件,主办方已经把需要识别的图片提取成像素值,直接训练就行了。

其中,一行代表一个数字的图片,第一列lable代表的是已经识别出来的具体的数字。

label pixel0 pixel1 …..
1 0 186 0
0 0 186 100
….. …… …… …..

寻找合适算法

这里我采用了kNN算法,具体可以点击这里查看算法详细介绍。

选择kNN是因为kNN算法比较好理解,对于我这种新手也比较容易看懂,缺点是kNN算法是惰性算法,计算时间会比较长。

开始解题

一开始我使用python的numpy包来进行解题,在本机跑了将近一个半小时之后,终于输出了结果,这里有两个缺点,一个是我要自己处理CSV文件,提取成数组给kNN算法组件,二来是笔记本性能捉襟见肘,想要调试算法参数每次都要等这么久简直不能忍,于是我想到数加机器学习平台

上传数据

pai依赖于MaxCompute,所以要先开通MaxCompute,在数加里面可以开通,开通后建两张表pai_test_trainpai_test_data,分别对应训练数据和测试数据。

首先下载MaxCompute CMD,这里点击新版客户端下载,下载完成后,配置AK以及运行CMD,关于如何建表,请查看帮助文档,注意要把所有字段也建了,和CSV文件相对应,这里字段有段多,可以用一个脚本生成sql. 然后使用 MaxCompute CMDtunnel命令上传CSV文件建表odps@ my_test_project> tunnel upload train.csv pai_test_train;,还有测试数据表odps@ my_test_project> tunnel upload test.csv pai_test_data;

使用pai来解题

从数加的管理控制台进入pai,然后按照以下步骤操作。

  1. 新建一个实验,取名pai_test,这时候我们会得到一个空的实验,接下来就是往实验上拖组件。
  2. 首先选择数据源,在左侧表列表中选择刚刚新建的pai_test_trainpai_test_data,分别拖到右侧的实验图上。这里表同步机制有延迟,如果在所有表中找不到新建的表,可以尝试一下搜索。
  3. 这时候我们的训练表和测试表已经在实验中ready了,就需要去找我们的算法组件了,在组件-机器学习-多分类中,K近邻就是我们需要的算法组件,同样拖拉到实验中。
  4. 组件都已经准备好了,那么就需要告诉pai如果来运行你的组件了,同样采用拖拉的方式,在pai_test_train表的组件下方有个小白点,鼠标拖拉一下可以拉出一根线,把这个线连接到K近邻算法组件上方的右边白点上,两个组件已经关联完成,同理另外一个表连接到右边白点上。
  5. 接下来就是配置算法组件了,要告诉算法,哪些是我的标签列,哪些是我的特征列,点击K近邻算法组件,在右侧的算法配置界面中,选择训练表特征列,这里把除了label字段外的所有字段都勾上,因为lable列是我们的标签列。
  6. 选择训练表的标签列中选择label字段,下面两个选项都是可选项,可以选择预测表的哪些特征别参与计算,还有产出表选择产出哪些字段,这些都可以选择默认,同理还有参数设置tab,如果对算法没什么研究,选择默认就可以了。
  7. 全部选择好了,点击下方的运行来进行计算。

产出数据

运行成功后,会产出一张新表,名字带有随机性,通常格式是pai_temp_5052_75767_1,因为kaggle没有和阿里云的产品进行打通,这里我们需要把数据下载成CSV文件,然后进行上传。

  1. 首先依然是通过tunnel命令进行下载: odps@ my_test_project> tunnel download pai_temp_5052_75767_1 -h true result.csv;,-h true命令表示带上表头。
  2. 打开CSV文件,找到prediction字段,这个就是我们的预测结果了,然后就是上传kaggle了。
  3. 因为kaggle对上传的字段有要求,这样他才能统计出分数,需要一个csv文件,有两列imageIdlabelimageId就是一列自增列,从1开始,label列就是我们的结果列。
  4. 打开101那个项目,找到Make a submission那一项,上传我们的CSV文件,就能得到分数了。
  5. 后续可以调整算法参数,慢慢改善算法,提高分数,提高排名,也可以尝试一下别的算法。

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment