一个初始化结构体的用法

近来敲代码的时候,由于技术含量不高,所以就格外注重“代码好不好看”这个问题。
在无聊的Coding中看到一个没有用过的语法还是小有惊喜的。
记在这里:

主要是这段

    DemoStruct demos[4] = {
        {.val = 1, .point = &a},
        {.val = 2},        
        {.point = &b},
        {.val = 3, .point = &c},
    };

对于结构复杂一点的struct,这么写是不是挺好看的 :]

[iOS]一句话Tip之redefinition of class

编译工程发现报redefinition of classXXX的错,但是确实是采用#import而不是#include包含头文件的时候,请从Finder里看看整个工程目录下是不是有两个同名的头文件…有的话这就是罪魁祸首,删之。

为了这问题折腾了好半天,当时多么希望能搜到一点提示…

redefinition错误指编译器发现重复定义的类或者结构,用objective-C的时候建议全部采用#import,可以避免一个头文件被包含多次。
当然遇到坑爹的IDE又是另外一回事了…

[Tip]iOS上的OpenGLES无显示问题

最近的项目用到了OpenGLES。
两个很容易出问题的地方,容易造成很难找到的Bug。

一个是EAGLLayer的大小,必须是32的倍数。否则用OpenGL画出来的东西,统统不会被显示。
Apple的文档:In iOS 4.2 and later, the performance of Core Animation rotations of renderbuffers have been significantly improved, and are now the preferred way to rotate content between landscape and portrait mode. For best performance, ensure the renderbuffer’s height and width are each a multiple of 32 pixels.
Apple Document

另一个是纹理的大小,必须是2的次方。
也就是说,如果使用一张图片来生成纹理,那么图片的长和宽的长度都必须是2的次方,否则加载纹理失败。

PlistBuddy

最近由于工作需要,发现了这么一个小工具。

PlistBuddy是一个Mac里的命令行下读写plist文件的工具。
位于/usr/libexec/下,由于这个路径不在默认的PATH里,需要通过绝对路径/usr/libexec/PlistBuddy引用。

使用起来很简单,功能也很丰富。

举几个例子:
A.plist内容如下

B.plist内容如下

通过命令打印出plist的内容

/usr/libexec/PlistBuddy -c "print" A.plist

得到结果

Dict {
    KeyA = ValueA
    KeyC = ValueC
    KeyB = ValueB
}

通过命令得到一个entry的值

$ /usr/libexec/PlistBuddy -c "print KeyA" A.plist 

输出

ValueA

这些都是简单的功能。PlistBuddy最好用的一个功能应该算是merge了。
merge命令可以合并两个plist文件,一个认为是源文件,一个作为目标文件。
比如命令

/usr/libexec/PlistBuddy -c "merge A.plist" B.plist

PlistBuddy把A.plist中的条目逐个插入到B.plist中,如果B.plist中已经有相同Key的条目就跳过此条目。
如上面的命令,得到结果

$ /usr/libexec/PlistBuddy -c "merge A.plist" B.plist 
Duplicate Entry Was Skipped: KeyA
$ /usr/libexec/PlistBuddy -c "print" B.plist 
Dict {
    KeyC = ValueC
    KeyD = ValueD
    KeyA = ValueNewA
    KeyB = ValueB
}

得到新的B.plist如下

更多命令可以通过man或者-h来查看

$/usr/libexec/PlistBuddy -h
man PlistBuddy

贝塞尔曲线拟合

接上一篇Blog,这里用贝塞尔曲线来平滑多个点。

和样条插值不同,在多个点上作贝塞尔曲线的时候,曲线只穿过首尾两个点,中间的点都是作为控制点。

移动控制点,曲线也随之形变,可以造成一种拉扯的效果。在各种作图工具中,经常使用贝塞尔曲线来画曲线。一般的操作都是先画一条线段,然后可以通过拖动一个控制点来调整线段的弯曲程度。

作多点贝塞尔曲线只需要一个公式。所有的点的X值,被归一化到[0,1]区间内。
具体理论,可以参考这个页面Bézier curves。89年创建的,可有年头了。

这里还是贴代码吧。

首先需要得到X区间的总长度。

CGPoint startPt = [[_points objectAtIndex:0] CGPointValue];
CGPoint endPt = [[_points objectAtIndex:(self.pointCount - 1)] CGPointValue];
float amount = endPt.x - startPt.x;

然后就是曲线方程了,这个比样条插值要简单不少。
rank是指总的阶数,也就是实际的点数。这个函数表示n个点的贝塞尔曲线在x处的值。
这里的ux属于区间[0,1]

float (^bezierSpline)(int rank, float ux) = ^(int rank, float ux) {
        
    float p = 0.0f;
        
    for (int i = 0; i < rank; i++)
    {
        CGPoint pt = [[_points objectAtIndex:i] CGPointValue];
            
        p += pt.y * powf((1 - ux), (rank - i - 1)) * powf(ux, i) * (factorial(rank - 1) 
                  / (factorial(i) * factorial(rank - i - 1)));
    }
    
    return p;
};

下面很容易了,画图的时候步长为1,求得ux之后,带入方程得到点的y值,画曲线。

    [path moveToPoint:startPt];
    
    for (float curX = startPt.x; (curX - endPt.x) < 1e-5; curX += 1.0f)
    {
        float u = (curX - startPt.x) / amount;
        [path addLineToPoint:
            CGPointMake(curX, bezierSpline(self.pointCount, u))];
    }
    
    CGContextSetLineWidth(context, 1.0f);
    CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
    CGContextAddPath(context, path.CGPath);
    CGContextStrokePath(context);
}

和三次样条相比,由于不经过中间点,丢失细节比较多,平滑度更好。

平滑前
no bezier

三次样条
no bezier

贝塞尔曲线
no bezier

示例代码: Sample-CurveFit
和上个例子放在一个repo里。

参考: Wiki

三次样条插值和曲线拟合

很多东西不在手上用着就容易忘,尤其是书本知识。就弄这么个类别,叫作“书到用时方恨少”,来记录这些知识。

曲线拟合是一个“数值计算“中的一个基本内容。在实际的项目中,使用拟合的目的就是从有限个点得到一条平滑曲线。曲线本身也是由点构成的,所以如何从有限个点得到曲线上的其它点,就是插值所关注的内容。

插值的方法有很多,把这些个点逐个用直线段连起来也是一种插值。

样条插值是一种工业设计中常用的、得到平滑曲线的一种插值方法,三次样条又是其中用的较为广泛的一种。

Google 三次样条插值可以看得到不少材料,这里就不罗列公式了,直接看看在代码里,我们怎么做。

首先我们需要各个点的坐标,以x,y表示。

        const int len = [_points count];
        float x[len];
        float y[len];
        for (int i = 0; i < len; i++)
        {
            CGPoint p = [[_points objectAtIndex:i] CGPointValue];
            x[i] = p.x;
            y[i] = p.y;
        }

取变量x,y

从算法中可以得知,我们的目标是样条插值函数,这是一个分段函数,x最高次数为三次,在各个点二次连续可导以保证最终函数曲线的光滑性。
我们每两个点求一个三次函数,我们有n个点,那么这里就需要4(n-1)个方程。
目前我们有n个点的坐标,有n-2个连接点,有n个函数两次连续可导,这里有n+n-2+2*(n-2)共4n-6个方程,还差两个条件。
这里一般有三种处理方法,最方便的,也是我们这里使用的是自然三次样条,也就是在首尾两个点上二次导为0。

具体计算不在此列举了,根据算法构建一个方程组求一组中间值sx,左边是一个三对角矩阵。

        float h[len];
        float u[len];
        float lam[len];
        for (int i = 0; i < len-1; i++)
        {
            h[i] = x[i+1] - x[i];
        }    

        u[0] = 0;
        lam[0] = 1;
        for (int i = 1; i < (len - 1); i++)
        {
            u[i] = h[i-1]/(h[i] + h[i-1]);
            lam[i] = h[i]/(h[i] + h[i-1]);
        }
        
        float a[len];
        float b[len];
        float c[len];
            
        float m[len][len];
        for (int i = 0; i < len; i++)
        {
            for (int j = 0; j < len; j++)
            {
                m[i][j] = 0;
            }
            if (i == 0)
            {
                m[i][0] = 2;
                m[i][1] = 1;
                
                b[0] = 2;
                c[0] = 1;
            }
            else if (i == (len - 1))
            {
                m[i][len - 2] = 1;
                m[i][len - 1] = 2;
                
                a[len-1] = 1;
                b[len-1] = 2;
            }
            else
            {
                m[i][i-1] = lam[i];
                m[i][i] = 2;
                m[i][i+1] = u[i];
                
                a[i] = lam[i];
                b[i] = 2;
                c[i] = u[i];
            }
        }

求三对角矩阵,自下而上对角线上的参数是a,b,c

当然需要得到方程组右边的值

        float g[len];
        g[0] = 3 * (y[1] - y[0])/h[0];
        g[len-1] = 3 * (y[len - 1] - y[len - 2])/h[len - 2];
        for (int i = 1; i < len - 1; i++)
        {
            g[i] = 3 * ((lam[i] * (y[i] - y[i-1])/h[i-1]) + u[i] * (y[i+1] - y[i])/h[i]);
        }

下面就是求解这个方程组了,对于三对角矩阵,使用追赶法

        //< Solve the Equations
        float p[len];
        float q[len];

        p[0] = b[0];
        for (int i = 0; i < len - 1; i++)
        {
            q[i] = c[i]/p[i];
        }

        for (int i = 1; i < len; i++)
        {
            p[i] = b[i] - a[i]*q[i-1];
        }
        
        float su[len];
        float sq[len];
        float sx[len];
        
        su[0] = c[0]/b[0];
        sq[0] = g[0]/b[0];
        for (int i = 1; i < len - 1; i++)
        {
            su[i] = c[i]/(b[i] - su[i-1]*a[i]);
        }

        for (int i = 1; i < len; i++)
        {
            sq[i] = (g[i] - sq[i-1]*a[i])/(b[i] - su[i-1]*a[i]);
        }
        
        sx[len-1] = sq[len-1];
        for (int i = len - 2; i >= 0; i--)
        {
            sx[i] = sq[i] - su[i]*sx[i+1];
        }        

求得了参数,现在就得到分段插值函数了。这种函数实际上有两个参数。
一个是所在的段位,这个是整数。另一个才是自变量。
在Objective-C里用block做一个临时的函数很是方便。

        double (^func)(int k, float vX) = ^(int k, float vX) {
            double p1 =  (ph[k] + 2.0 * (vX - px[k])) * ((vX - px[k+1]) * (vX - px[k+1])) * py[k] / (ph[k] *ph[k] * ph[k]);
            double p2 =  (ph[k] - 2 * (vX - px[k+1])) * powf((vX - px[k]), 2.0f) * py[k+1] / powf(ph[k], 3.0f);
            double p3 =  (vX - px[k]) * powf((vX - px[k+1]), 2.0f) * psx[k] / powf(ph[k], 2.0f);
            double p4 =  (vX - px[k+1]) * powf((vX - px[k]), 2.0f) * psx[k+1] / powf(ph[k], 2.0f);
            return p1 + p2 + p3 + p4;
        };

到此为止,得到了曲线的函数。当然在实际画曲线的时候,我们仍然是逐个点画,
取点的时候取得密集一些就形成曲线了。这里delta取为1。

        for (int i = 0; i < [_points count]; i++)
        {
            CGPoint pt = [[_points objectAtIndex:i] CGPointValue];
            if (i == 0)
            {
                CGPathMoveToPoint(path, NULL, pt.x, pt.y);
            }
            else
            {
                CGPoint curP = [[_points objectAtIndex:i-1] CGPointValue];
                float delta = 1.0f;
                for (float pointX = curP.x; fabs(pointX - pt.x) > 1e-5f; pointX += delta)
                {
                    float pointY = func(i-1, pointX);
                    CGPathAddLineToPoint(path, NULL, pointX, pointY);
                }
            }
        }

拟合前

拟合后

示例代码在GitHub上: Sample-CurveFit

[Demo]OmniGridView

OmniGridView Code (GitHub)

OmniGridView

This is a half-finished grid view for iOS.
Cause Apple’s UITableView only allows us to add and reuse vertical cells, we need to write our own
scroll view when we want to have horizontal cells.

HOW TO USE

Drag the OmniGridView Folder to your project. Use the OmniGridView like any other UIView.
Assign a delegate to OmniGridView and implement the OmniGridViewDelegate.

Please refer to the OmniGridTestViewController.m to see the information you need to provide.

这是一个半成品的Grid View。 因为Apple的UITableView只提供垂直滚动的表格,我们可能会需要水平滚动的表格。
这里提供一个全方向滚动包含单元格重用机制的Grid View。

如何使用

把OmniGridView文件夹加入你的工程。就像使用UIView一样使用OmniGridView。
设置OminiGridView的delegate实现OmniGridViewDelegate。

请参考OmniGridTestViewController.m的实现.

#pragma mark OmniGridViewDelegate

@implementation OmniGridTestViewController (Private)

- (OmniGridCell *)gridCellAt:(OmniGridLoc *)gridLoc inGridView:(OmniGridView *)gridView {
    OmniGridCell *gridCell = [gridView dequeueReusableGridCell];
    if (!gridCell)
    {
        gridCell = [[OmniGridCell alloc] init];
        gridCell.layer.borderColor = [UIColor blackColor].CGColor;
        gridCell.layer.borderWidth = 1.0f;
        gridCell.textLabel.textAlignment = UITextAlignmentCenter;
    }
    
    gridCell.textLabel.text = [NSString stringWithFormat:@"(%d,%d)", gridLoc.row, gridLoc.col];
    
    return gridCell;
}

- (float)gridCellHeightInGridView:(OmniGridView *)gridView {
    return 200.0f;
}

- (float)gridCellWidthInGridView:(OmniGridView *)gridView {
    return 200.0f;
}

- (int)numberOfGridCellsInRow:(int)row inGridView:(OmniGridView *)gridView {
    return 12;
}

- (int)numberOfRowsInGridView:(OmniGridView *)gridView {
    return 12;
}

@end

[技巧]从地址得到symbol

做iOS开发的时候,常常会遇到crash,需要分析call stack的时候。
有时候App在别人的设备崩溃,把crash report在自己的机器上打开,Xcode没有自动的进行符号化。
这时候就需要自己去把地址解析成符号。
大前提是,必须有相同版本App对应的.dSYM文件。
这时候打开Terminal,进入到build/Debug-iphoneos
使用命令:
$atos -arch armv7 -o XXX.app/XXX 0xabcdef
XXX是你的App名字,用需要解析的地址替换上面的0xabcdef
armv7是编译App时所用的Architecture,也可能是armv6,如果在simulator上的App,这个位置应该用i386

参考:stackoverflow