SQUARE - a full-stack engineer
SQUARE - a full-stack engineer
Foundation

从头梳理之–内存与内存管理

前言

对于大多数开发者,特别是 C, Objective-C, Swift 等相关的开发者来说,已经很了解如何避免内存泄漏,解决循环引用等问题。但如果继续深入讨论为何会需要手动管理内存,为何内存泄漏仅在特定场景下产生,如何尽可能减少内存开销等问题时,能回答很清楚的人就相对较少了。

所以还是有必要从头梳理一下关于内存的基本知识与概念了,如果我们掌握了这些知识,相信很多问题就会迎刃而解了。


基础知识

  • 位 :( bit ) 是电子计算机中最小的数据单位。每一位的状态只能是0或1。
  • 字节:1 Byte = 8 bit ,是内存基本的计量单位,
  • 字:”字” 由若干个字节构成,字的位数叫做字长,不同档次的机器有不同的字长。
  • KB :1KB = 1024 Byte。也就是1024个字节。
  • MB : 1MB = 1024 KB。类似的还有GB、TB。

  • 内存编址:计算机中的内存按字节编址,每个地址的存储单元可以存放一个字节(8个bit)的数据,CPU通过内存地址获取指令和数据,并不关心这个地址所代表的空间具体在什么位置、怎么分布,因为硬件的设计保证一个地址对应着一个固定的空间,所以说:内存地址和地址指向的空间共同构成了一个内存单元。

  • 内存地址:内存地址通常用十六进制的数据表示,例如通常在C或者Objective-C中输出一个变量的地址可能为:0x7fff5fbff79c,这就是一个用十六进制的数表示的地址。

下图的整数100在三种进制中的表示:

什么是内存?

从硬件的角度来说,它是重要的部件之一.

也是硬盘与 CPU 之间沟通的桥梁。所有的应用程序运行时都会放入内存,然后交由 CPU 进行计算执行。CPU 能通过寻址找到内存对应的数据。

严格意义上讲,内存不仅仅是我们经常所指的内存条。寄存器(Register)、缓存(Cache) 都属于内存的一种。

所以内存一般分为 RAM(main memory), SRAM(cache), Register。

寄存器是 CPU 的组成部分,因为在CPU内,在设计上和CPU同频,所以CPU对其读写速度是最快的,不需要IO传输。

部分缓存的设计也基本保持了与 CPU 同频,所以速度相对内存也是比较快。

理论上讲,缓存和寄存器是一样快的。不过因为缓存里面的东西不一定就是需要的,如果不存在就要一级一级往下找,直到内存。因为不同的缓存或内存时钟频率不一样,所以CPU在查找时需要一定的等待时间。

CPU 寻找数据进行运算的流程是:

CPU -> Register -> L1 Cache -> L2 Cache -> 内存

RAM 内存的主频现在主流是1600左右,单位是MHz,这比CPU的速度要低的多。

32 位与 64 位是什么概念?

所谓多少位一般是指处理器(CPU)的运算能力与寻址能力。

  1. 运算能力

比如32位,一次能处理32位,也就是4个字节的数据(1字节=8位)。而64位处理器则能处理8个字节的数据。

如果我们将总长128位的指令分别按照16位、32位、64位为单位进行编辑的话:旧的16位处理器,比如Intel 80286 CPU需要8个指令,32位的处理器需要4个指令,而64位处理器则只要两个指令。

  1. 寻址能力

除了运算能力之外,与32位处理器相比,64位处理器的优势还体现在系统对内存的控制上。

相比32位的CPU来说,64位CPU最为明显的变化就是增加了8个64位的通用寄存器,内存寻址能力提高到64位。

32位的内存地址空间为2的32次方,即4GB。64位可获得更大的寻址空间,能识别4G以上的内存。

另外,要实现真正意义上的64位计算,光有64位的处理器是不行的,还必须得有64位的操作系统以及64位的应用软件才行,如果应用软件不支持,反而在64位机器上会变慢。

寄存器 (Register)

为了处理数据,暂时储存结果,或者做间接寻址等动作,每个处理器都具备一些内建的内存,这些能够在不延迟的状态下存取的内存就称为寄存器。

寄存器是数据处理的重要一环,如果经常使用汇编语言,对它应该会非常熟悉。

IA-32构架提供了16个基本寄存器,这16个基本寄存器可以归纳为如下几类:

  • 通用寄存器
  • 段寄存器
  • 状态和控制寄存器
  • 指令寄存器

通用寄存器
32位通用寄存器有八个, eax, ebx, ecx, edx, esi, edi, ebp, esp

他们主要用作逻辑运算、地址计算和内存指针,具体功能如下:

  • eax 累加和结果寄存器
  • ebx 数据指针寄存器
  • ecx 循环计数器
  • edx i/o指针
  • esi 源地址寄存器
  • edi 目的地址寄存器
  • esp 堆栈指针
  • ebp 栈指针寄存器

当然,以上功能并未限制寄存器的使用,特殊情况为了效率也可作其他用途。

在 64-bit 模式下,有16个通用寄存器,但是这16个寄存器是兼容32位模式的,

32位方式下寄存器名分别为 eax, ebx, ecx, edx, esi, edi, ebp, esp, r8d – r15d

在64位模式下,他们被扩展为 rax, rbx, rcx, rdx, rdi, rsi, rbp, rsp, r8 – r15,其中 r8 – r15 这八个寄存器是64-bit模式下新加入的寄存器。

段寄存器
段寄存器是因为对内存的分段管理而设置的。计算机需要对内存分段,以分配给不同的程序使用(类似于硬盘分页)。

把内存分为很多段,每一段有一个段基址,当然段基址也是一个20位的内存地址。不过段寄存器仍然是16位的,它的内容代表了段基址的高16位,这个16位的地址后面再加上4个0就构成20位的段基址。而原来的16位地址只是段内的偏移量。这样,一个完整的物理内存地址就由两部分组成,高16位的段基址和低16位的段内偏移量,当然它们有12位是重叠的,它们两部分相加在一起,才构成完整的物理地址。

段寄存器又分为 cs, ds, ss, es, fs, gs。

  • cs 代码段寄存器
  • ds, es, fs, gs 数据段寄存器
  • ss 堆栈段寄存器

在 64-bit 模式下,这6个寄存器并无变化,只是使用上略有区别。

状态和控制寄存器 EFLAGS
这个寄存器表示的意义非常丰富,程序中并不直接操作此寄存器,并由此衍生出很多操作指令。

指令寄存器 EIP
标志当前进程将要执行指令位置,在64位模式下扩展为 RIP 64位指令寄存器。

OK 以上,关于寄存器,在这里我们只需要了解这么多就够了,不用深入。

内核空间与用户空间

为了保护操作系统不会被运行的用户程序所干扰,所以分为了内核空间与用户空间,所有的应用程序会被分配在用户空间。

内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存,内核代码和数据总是可寻址的,随时准备处理中断和系统调用。

现代的操作系统都处于32位模式下。每个进程一般都能寻址4G的物理空间。但是我们的物理内存不足 4G,通常我们使用一种叫做虚拟内存的技术来实现,因为可以使用硬盘中的一部分来当作内存使用。

虚拟空间中的内存布局 (Memory layout)

如果一个应用程序在运行时,会被装载到用户内存空间,并构成一个虚拟地址空间(virtual address space)。

典型的虚拟地址空间有四个部分构成

  • 执行的代码 (Executable code)
    这个空间包括了可被机器执行的代码,为只读区域

  • 静态数据 (Static data)
    这个部分包括了静态分配的变量

  • 堆 (Heap)
    这个部分包括了动态分配的变量

  • 栈 (Stack)
    这个部分包括了临时变量,返回地址,执行参数

每个部分分别占用了一块或多块连续的地址空间,他们被操作系统所管理。

如果我们想看看操作系统自己的内存分布,可以用下面的命令

C
1
2
3
4
5
cat /proc/self/maps
 
...
00aed000-00b0e000 rw-p 00000000 00:00 0 [heap]
…

 

第一列显示了内存地址块,第二列显示一些标记,如只读,可写等,三,四,五,六列分别表示偏移地址,设备,节点,名称

C 语言应用程序的内存布局

除了核心的内核空间外,其它的空间分为 5 种类型:

  • 文本段 (Text segment)

通常存放执行的代码,只读段。

  • 初始化的数据段 (Initialized data segment)

这个区域分为”只读数据段”和”可读写的数据段”

只读数据段是程序使用的一些不会被更改的数据。
可读写数据段会放入在程序中声明,具有初值的变量,以供程序运行时读写。
如全局 ( main 函数外) 字符串定义 char s[] = “hello world” 或 int debug=1 。

这样的全局声明 const char* string = “hello world” 中,“hello world” 被放入只读区,而字符串指针变量 string 会被放入读写区。
这样的声明 static int i = 10 与 int I = 10 都会被存入数据段。

  • 未初始化的数据段 (Uninitialized data segment)

又称为 BSS (block started by symbol) 段。
这样的声明 static int I; 或 全局的 int j; 都会放入这个区域。

  • 栈 (Stack)

栈空间主要用于以下3种数据的存储:

  • 函数内部的动态变量
  • 函数的参数
  • 函数的返回值

栈空间是动态开辟与回收的。在函数调用过程中,如果函数调用的层次比较多,所需的栈空间也逐渐加大,对于参数的传递和返回值,如果使用较大的结构体,在使用的栈空间也会比较大。

栈上的局部变量往往通过偏移地址去访问,如果通过 push/pop 会浪费大量时间。

栈通常都会放在高位区,它遵循 LIFO (后进先出) 机制。

Objective-C
1
2
3
4
5
6
7
8
#include
int main(int argc, const char * argv[]) {
int a = 100;
int b = 100;
printf("%p \n",&a); // 0x7fff5fbff79c
printf("%p \n",&b); // 0x7fff5fbff798
return 0;
}

变量a的地址 0x7fff5fbff79c 比变量 b 的地址 0x7fff5fbff798 要大。所以它也是从高位开始按顺序一条条出栈执行。

  • 堆 (Heap)

堆开始于 BSS 段的未尾。然后往高地址增长。Heap 区通常由 malloc, realloc, and free 命令管理。Heap 区可以在一个进程中被多个库或动态加载的模块所共享。

我们来看看一个最简单的 C 程序,在 linux 系统上编译后它的内存部局是什么样的:

Objective-C
1
2
3
4
5
#include
int main(void)
{
return 0;
}

Objective-C
1
2
3
4
$ gcc memory-layout.c -o memory-layout
> size memory-layout
text data bss dec hex filename
1115 552 8 1675 68b memory-layout

然后我们增加一个全局变量,看看内存有什么变化

Objective-C
1
2
3
4
5
6
7
#include
int global; /* Uninitialized variable stored in bss*/
 
int main(void)
{
return 0;
}

重新编译

Objective-C
1
2
3
4
5
$ gcc memory-layout.c -o memory-layout
$ size memory-layout
 
text data bss dec hex filename
1115 552 12 1675 68f memory-layout

如果我们增加一个 static 变量

Objective-C
1
2
3
4
5
6
7
8
#include
int global; /* Uninitialized variable stored in bss*/
 
int main(void)
{
static int i; /* Uninitialized static variable stored in bss */
return 0;
}

Objective-C
1
2
text data bss dec hex filename
1115 552 16 1683 693 memory-layout

让我们实例化这个变量,它会被放入 Data Segment (DS)

Objective-C
1
2
3
4
5
6
7
8
#include
int global; /* Uninitialized variable stored in bss*/
 
int main(void)
{
static int i = 100; /* Initialized static variable stored in DS*/
return 0;
}

Objective-C
1
2
text data bss dec hex filename
1115 556 12 1683 693 memory-layout

如果我们实例化全局变量后,它也会被放入 DS

Objective-C
1
2
3
#include
int global = 10; /* Uninitialized variable stored in bss*/
...

Objective-C
1
2
text data bss dec hex filename
1115 560 8 1683 693 memory-layout

注意:如果你是在 Mac 系统上编译,查看内存布局时,会是这样的情况

Objective-C
1
2
__TEXT __DATA __OBJC others dec hex
4096 4096 0 4294971392 4294979584 100003000

同样也是分为 Text 区,Data 区。

代码段、只读数据段、读写数据段、未初始化数据段属于静态区域,而堆和栈属于动态区域。代码段、只读数据段和读写数据段将在链接之后产生,未初始化数据段将在程序初始化的时候开辟,而堆和栈将在程序的运行中分配和释放。

设想一下,如果系统将程序初始化阶段把所有变量全部分配好,那得要多少内存。所以有这样段设计,将动态的部分根据需要再去分配,这才有了运行时的概念。

基本数据类型的内存空间

变量在内存中以二进制形式存储,一个变量占用的存储空间,不仅和变量类型有关,还和编译环境有关,同一种类型的变量在不同编译环境下占用的存储空间不一样。比如开发中常用的基本数据类型char、int等在不同编译环境下就会占用不同大小的空间。

在 64 位机器下的许多程序设计环境,int 变量仍然是 32 位宽,不过 long 是 64 位宽。这就是为什么 Objective-C 中会有 NSInteger 的定义,它会根据操作系统位数自由选择不同的数据类型。

我们通过一个例子来验证 int 的内存空间。

Objective-C
1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
int a = 1;
int b = 2;
printf("%p\n",&a); //0x7fff5fbff79b
printf("%p\n",&b); //0x7fff5fbff797
return 0;
}

我们可以看到 b 与 a 刚好是 4 个字节。

C语言中数组的存储和普通的变量不太一样,数值中存储的元素,是从所占用的低地址开始存储的。例如

Objective-C
1
2
3
4
5
6
7
8
int main(int argc, const char * argv[]) {
char chars[4] = {'l','o','v','e'};
printf("chars[0] = %p\n",&chars[0]); //0x7fff5fbff79c
printf("chars[1] = %p\n",&chars[1]); //0x7fff5fbff79d
printf("chars[2] = %p\n",&chars[2]); //0x7fff5fbff79e
printf("chars[3] = %p\n",&chars[3]); //0x7fff5fbff79f
return 0;
}

仅仅通过上面的字符数组例子还不能完全说明数组在内存中关于其元素存储和元素中值的存储关系。如果换用一个整型数组就能看出一些差别。

Objective-C
1
2
3
4
5
6
int main(int argc, const char * argv[]) {
int nums[2] = {5, 6};
printf("nums[0] = %p\n",&nums[0]); // 0x7fff5fbff7a0
printf("nums[1] = %p\n",&nums[1]); // 0x7fff5fbff7a4
return 0;
}

数组在使用过程中遇到的最多的问题可能就是下标越界,下面的代码就是越界访问数组示例:

Objective-C
1
2
3
4
5
6
7
8
9
int main(int argc, const char * argv[]) {
char charsOne[2] = {'a', 'b'};
char charsTwo[3] = {'c', 'd', 'e'};
charsTwo[3] = 'f';
printf("charsOne[0] = %p\n",&charsOne[0]); // 0x7fff5fbff79e
printf("charsTwo[0] = %p\n",&charsTwo[0]); // 0x7fff5fbff79b
printf("charsOne[0] = %c\n",charsOne[0]); // f
return 0;
}

结构体变量占用的内存空间是其成员占用最大内存空间的整数倍

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Person {
int age;
int hegiht;
int weight;
};
int main(int argc, const char * argv[]) {
struct Person p1 = {1, 2, 3};
int size = sizeof(p1);
printf("%i\n",size); // 4 * 3 = 12
printf("变量的地址:%p\n",&p1); //0x7fff5fbff750
printf("%p\n",&p1.age); //0x7fff5fbff750
printf("%p\n",&p1.hegiht); //0x7fff5fbff754
printf("%p\n",&p1.weight); //0x7fff5fbff758
return 0;
}

内存对齐

对于结构体来说,会有内存对齐的规范。
首先结构体从首个成员分配空间;如果空间不够则重新分配,如果空间剩余则会把下一个成员的数据存储到剩余的空间中

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Student {
char id;
double score;
int age;
}
int main(int argc, const char * argv[]) {
struct Student s1 = {'A', 10.0, 15};
int size = sizeof(s1);
printf("%i\n",size); //24
printf("%p\n",&s1.id); //0x7fff5fbff788
printf("%p\n",&s1.score); //0x7fff5fbff790
printf("%p\n",&s1.age); //0x7fff5fbff798
return 0;
}

按照前面的说明,系统为 s1 分配内存时以 sizeof(double) 8个字节为单位,所以为 s1.id 分配了8个字节的空间,但是由于id定义为char类型,所以只占了8个字节中数值最小的一个内存空间;由于前面剩下的 8 – 1 = 7个字节不足以存放double类型的值,所以接着为s1.score分配8个字节并占满8个字节空间;最后为s1.age分配8个字节并占用了前4个字节空间。故s1变量在内存中占用的内存大小为 8 X 3 = 24个字节。

如果调换结构体Student成员之间的顺序如下,情况又会发生变化。

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Student {
double score;
char id;
int age;
};
int main(int argc, const char * argv[]) {
struct Student s1 = {'A', 10.0, 15};
int size = sizeof(s1);
printf("%i\n",size); //16
printf("%p\n",&s1.score) //0x7fff5fbff790
printf("%p\n",&s1.id); //0x7fff5fbff790
printf("%p\n",&s1.age); //0x7fff5fbff79c
return 0;
}

这是由于第二次为 s1.id 分配内存时没有完全占满8个字节的空间,而且第三次为s1.age分配时其需要的4个字节空间也没有超出剩余的8-1 = 7个字节空间,所以s1.age的值按照内存对齐的原则就存放在了第二次分配的8个字节的后4位空间中。

总结:结构体变量所占存储空间受其不同类型的成员排列顺序及编译器内存对齐影响,开发中尽量将相同类型的成员依次定义,有助于节省内存空间。

内存管理

根据以上知识点,我们知道内存管理分为系统和手动管理两个部分。
BSS, Data, Text, Stack 区都是系统自动管理的。Heap 区是手动创建,所以需要手动释放。否则内存会不断的增大,直到进程被强制退出。

不过对于栈区 (Stack ),虽然是由系统管理的,但它也是动态分配的。所以如果栈特别大,比如递归函数,会不断的压栈,也会造成内存耗尽。另外,如果在函数里定义的变量,也会分配在栈区,假设有一个无限循环,局部变量不停的生成,如果不及时释放,也会造成内存溢出。

所以无论是何种内存管理方式,垃圾回收机制,无外乎要注意以上几点。比如 iOS 开发中的 ARC 机制,就是采用了引用计数,来帮我们管理 Heap 区的内容,防止我们遗忘释放对象。 Autorelease Pool 也是一种管理栈区变量的一种机制。

内存泄漏

内存泄漏是指分配的内存未能及时释放,导致这部分的内存长期占用,产生了浪费,甚至还会导致系统资源不足引起崩溃。

根据上述提到的点,这种场景确实是会发生的。

场景一

Objective-C
1
2
int *pOld = (int*) malloc( sizeof(int) );
int *pNew = (int*) malloc( sizeof(int) );

这两段代码分别创建了一块内存,并且将内存的地址传给了指针 pOld 和 pNew。此时指针 pOld 和 pNew 分别指向两块内存。

如果接下来进行这样的操作:

pOld = pNew;

pOld 指针就指向了 pNew 指向的内存地址,这时候再进行释放内存操作:

free(pOld);

此时释放的 pOld 所指向的内存空间就是原来 pNew 指向的,于是这块空间被释放掉了。但是 pOld 原来指向的那块内存空间还没有被释放,不过因为没有指针指向这块内存,所以这块内存就造成了丢失。

场景二

另外,不应该进行类似下面这样的操作:

malloc( sizeof(int) );

这样的操作没有意义,因为没有指针指向分配的内存,无法使用,而且无法通过 free() 释放掉,造成了内存泄露。

泄漏检测

我们可以用 leaks 命令 (Mac 系统下);

首先在命令行下用 MallocStackLogging=1 ./test.out & 打开 Malloc 调试日志。
当你运行上面的命令后,会出现一个线程 id。然后运行命令:leaks 线程 ID, 即可看到是否有泄漏。
注意: 进程需要保持运行状态,否则退出后,所有的内存都会被销毁,就没办法跟踪了。可以用 sleep(100) 能让它一直运行

比如

Objective-C
1
2
3
4
5
6
7
8
9
10
11
12
13
#include
#include
void f()
{
char *c = malloc(24); // 泄漏在此,没有对应的 free 操作
}
 
int main()
{
f();
sleep(100);
return 0;
}

四月 8, 2018by square
FacebookTwitterPinterestGoogle +Stumbleupon
Foundation

剖析几种流行的 iOS 设计模式–MVC;MVVM;VIPER

— 原文首发在简书,现移值过来。

在这篇文章里,我们争取用最精简的语言,解释清楚这几种设计模式到底给我带来了什么便利。

以看图说话的方式逐一解释,最后总结

 

这是我们最早接触,也最熟悉的设计模式了

但要记住 Controller 不是我们通常理解的 ViewController !Controller 你可以认为是 Helper 方法。他不掌管 View 的生命周期。也就是说和 UIKit 无关!ViewController 你可以把他理解为 Scene 或 装配中心。

感谢有人做了一个精辟的总结。看图理解吧:

 

 

这才是真正的 MVC 使用姿势。


MVP

 

从对比可以看到 MVC 的 Controller 变成了 Presenter  , UIView 或 UIViewController 为 View 层。View 持有 Presenter,Presenter 持有 Model。View 与 Model 完全隔离。

当事件发生,交由 Presenter 进行业务逻辑处理,Presenter 从 Model 拿数据,拿到数据后然后将数据返回给Presenter,Presenter 再返回给 View 。可以理解 Presenter 是一个中间人。

同样 Presenter 应完全不依赖 UIKit 。如果需要与 View 交互,可以采用 Protocol 协议进行约定,调用。保证逻辑清楚,可测试。

最简样列代码:

MVP Example

注意中文标注的地方。

另外还有一个 Supervising Presenter MVP 的升级版本,将 View 与 Model 进行绑定。View 会受到 Model 的变更影响,同时 Presenter 依旧可以控制 View 状态。这种模糊的职责关系,还是少用为好。

MVVM

和 MVP 一样,View Model 承担了类似 Presenter 中间层的角色。区别在于”数据与用户行为”的绑定是在 View 与 View Model 层上发生的。这个时候不影响这个设计架构中 View Model 应该承担的角色属性:更新 View 状态。

关于绑定我们可以用 KVO 模式及函数式编程来实现我们的所需。如果你想更高效的完成这个动作,可以参考现有的解决方案:
1.  KVO: RZDataBinding or the SwiftBond
2.  Functional programming:  ReactiveCocoa, RxSwift or PromiseKit

不过,对于 ReactiveCocoa 我只能说慎用,设计起来会非常麻烦。

这里,我们还列举最简单的例子,没有用到 KVO 或 RX 之类的设计,使用最为传统的 Protocol 来实现彼此的交互过程

MVVM Example

我们可以看重点中文标注的地方,这里通过协议里的定义,第一步 View 层发起数据请求 showGreeting 交由 ViewModel 进行数据处理,处理完成后,因 greeting 发生变化,触发 greetingDidChange 事件发生。

在这个实例里 ViewController 有设定 greetingDidChange 回调,所以实现了类似了上面所说的数据与行为绑定。

VIPER

从结构上来看,在 Presenter 中间多了一层 Interactor ,它主要起到了数据维护访问的职责。你可以封装更多的 Service 或 Managers 做为依赖调用。

对于页面间的跳转,这里增加了 Router 层,专门用于跳转逻辑,实际上现在不少架构已经加了各自的 Router 逻辑。这一点可能是从 Rails 框架继承而来。

下面的例子不包含 Router 层

VIPER Example

从以上最简的示例代码来看,View 层 (ViewController) 同样还是通过  Presenter 层 (GreetingViewEventHandler) 触发事件逻辑,只不过 Presenter 拿数据是从 Interactor 层(GreetingProvider)去拿。层与层之间还是通过协议通导,不依赖实体。

从这个架构来看,它的耦合性是最小的,不过也带了设计上的复杂性。所以使用时,要因地制宜。

总结

最后附上一位专家 Bohdan Orlov 的总结,它们的横向对比图,寻找自己最适合的模式使用。

iOS Arch comparison

耦合度 (Distribution)
可测性 (Testability)
易用性 (Ease of use)

PS:这里也提到了 Redux 模式,想了解 Redux 相关的内容话,看这个链接吧。研究不深,大致是和 FP (函数式编程) 与 State 流相关。

总而言之

  1. 没有杀手锏
    no Silver Bullet
  2. 分层治理
    分久必合,合久并分,看具体业务场景
  3. 低耦合,高内聚
    基本要素
  4. 遵从流行的框架与概念,有利于团队间的沟通成本
    如果一定要发明,请多推广你的概念
  5. 从简到繁,再化繁为简
    前一个繁是业务的属性,后一个繁是框架本身

三月 8, 2018by square
FacebookTwitterPinterestGoogle +Stumbleupon
Foundation

事件交付: 响应链

当你设计APP时,很可能要动态地响应事件。例如:触摸可能来自屏幕上不同的对象,你必须理解这些对象,知道如何正确的响应这些事件。

当用户生成的事件发生时,UIKit 会创建包含所需要处理的信息在一个事件对象里。然后将它放置在当前活动APP的事件队列中。对于触摸事件,它是将一组触摸封装在 UIEvent 对象里。对于运动事件,这个事件对象取决定于你使用的框架,并在里面有一些你所感兴趣的运动类型。

一个事件延特定的路径旅行,在它被交给下一个对象前,你都可以处理它。首先,单例的 UIApplication 对象从队列顶部拿到这个事件,并调度处理它。通常,它发送该事件给 Key Window 对象,然后它根据不同的事件类型传递给不同的实例对象。

Continue reading

十月 30, 2014by square
FacebookTwitterPinterestGoogle +Stumbleupon
Foundation

iOS 手势识别

手势识别

手势识别器将抽像度低的事件操作转化为更容易理解的动作,它们是附加在视图上的对象,并允许对这些动作进行回应。手势识别器解释这些触摸事件是否是一个特定的手势,比如轻扫,缩放,旋转。如果它们能正确识别,它们会发送一个动作给目标对象。这个对象是你指定的视图控制器,如下图所示。这样的设计模式强大简单,你可以动态指定响应动作,而且你可以添加一个手势识别给一个视图,而不用子类化视图。

一个附加在视图上的手势识别

Continue reading

八月 28, 2014by square
FacebookTwitterPinterestGoogle +Stumbleupon
Foundation

iOS 手势操作详解

为什么要有手势

在我们拿起手机时,除了语音外,最重要的交互操作就是通过手指触摸了。就如游戏机的手柄,电脑的键盘一样。在iPhone设备里,因为它没有物理键盘,所以每一个手势动作,必须准确的覆盖到每一个用户想要表达的真实意图。手势的重要性也不言而喻。

苹果如何设计手势

Continue reading

八月 26, 2014by square
FacebookTwitterPinterestGoogle +Stumbleupon
Foundation

UIKit Class Hierarchy

是时候复习一下基础内容了,先从最常用到的 UIKit 开始吧。

UIKit reference introduction

先看看这个层级结构图吧:
image


UIAcceleration:

加速计类: 加速度实为UIAcceleration对象实例,又被称为加速事件,它代表即时的三维空间上,三个不同轴上的加速度数据。

加速计三维空间

实用的场景中可以用于“摇晃”,游戏中用于控制对象移动等。

Continue reading

二月 24, 2014by square
FacebookTwitterPinterestGoogle +Stumbleupon
Foundation

iOS 上的蓝牙框架 – Core Bluetooth for iOS [译]

所须环境: iOS 6 以上

原文: Core Bluetooth for iOS 6

Core Bluetooth 是在iOS5首次引入的,它允许iOS设备可以使用健康,运动,安全,自动化,娱乐,附近等外设数据。在iOS 6 中,这个API被扩展了,让iOS也能成为数据提供方,也就是 Server(Peripheral)端,可能使它与其它 iOS 设备交互数据。

Core Bluetooth API 基于BLE4.0规范。这个框架涵盖了BLE标准的所有细节. 不过,仅仅只有新的iOS设备和MAC是兼容BLE标准的: iPhone 4S, iPhone5, Mac Mini, New iPad, MacBook Air, MacBook Pro. 并且 iOS 6 iPhone 模拟器也支持一样的标准.这对你在没有真机时,开发APP时是非常实用的。

相关的类

在CoreBluetooth框架中,有两个主要的角色:外设和中心(Peripheral and Central) ,整个框架都是围绕这两个主要角色设计的,它们之间有一系列的回调交换数据。
下图1展示了外设和中心( Peripheral and Central)的关系。

Fig1

外设创建或提供一些数据,中心使用这些设备提供的数据。在iOS6之后,iOS 设备也可以即是外设,也可以是中心,但不能在同时间扮演两个角色。

这两个组件在CoreBluetooth框架中是分别用两个类来表示的,中央是 CBCentralManager类,外设是 CBPeripheralManager类。

在中心,一个 CBPeripheral 对象表示正在连接中的外设,同样在外设里,一个 CBCentral 表示正在连接中的中心.

你可以理解外设是一个广播数据的设备,它开始告诉外面的世界说它这儿有一些数据,并且能提供一些服务。另一边中心开始扫描外面有没有
自己所需要的服务,如果发现后,会和外设做连接请求,一旦连接确定后,两个设备就可以传输数据了。

除了中心与外设,我们还得考虑他们用于交互的数据结构,这些数据在Services(服务)中被结构化,每个服务由不同的Characteristics(特性)所组成。特性定义为一种属性类型,并且对应一个逻辑值(比如0x2A49)。

你可以在developer bluetooth这里找到标准服务与特性的列表。

比如:

Continue reading

八月 15, 2013by square
FacebookTwitterPinterestGoogle +Stumbleupon
Foundation

ARC 与内存管理

ARC: Automatic Reference Counting (自动引用计数)

ARC 是 iOS 5 后推出的一项为Objective – C程序在编译时提供自动内存管理的功能。ARC可以让你把注意力集中在你感兴趣的代码,减少开发中的内存管理步骤,简化开发。

它通过指定的语法,让编译器(LLVM 3.0)在编译代码时,自动生成实例的引用计数管理部分代码。有一点,ARC并不是GC,它只是一种代码静态分析(Static Analyzer)工具。

在过往我们通常使用的是MRC: Manual Reference Counting(手动内存管理)。这些规则将逐渐变为本能,你会发现少一个release的代码怎么看怎么别扭,从而减少或者杜绝内存管理的错误。可以说MRC的规则非常简单,但是同时也非常容易出错。往往很小的错误就将引起crash或者leak之类问题。

很多人担心内存管理不受自己控制,其实这是对于ARC机制了解不足从而不自信,所导致的对新事物的恐惧。

下面我们从几个方面来详细介绍ARC到底如何实现,如何使用,它的好处,注意事项等。

需要的基本环境:

ARC is supported in Xcode 4.2 for OS X v10.6 and v10.7 (64-bit applications) and for iOS 4 and iOS 5. Weak references are not supported in OS X v10.6 and iOS 4.

注意:iOS4 不支持 weak 引用

原理

Continue reading

四月 24, 2013by square
FacebookTwitterPinterestGoogle +Stumbleupon

近期文章

  • 从头梳理之–内存与内存管理
  • 剖析几种流行的 iOS 设计模式–MVC;MVVM;VIPER
  • 重头梳理网络协议栈
  • 10 分钟将你的网站升级到 HTTPS
  • URLCache 设置不当影起的 App 故障

近期评论

    文章归档

    • 2018年四月
    • 2018年三月
    • 2017年十二月
    • 2017年八月
    • 2017年三月
    • 2017年二月
    • 2016年四月
    • 2015年十月
    • 2015年八月
    • 2014年十月
    • 2014年八月
    • 2014年五月
    • 2014年四月
    • 2014年二月
    • 2013年十一月
    • 2013年十月
    • 2013年八月
    • 2013年五月
    • 2013年四月
    • 2013年二月
    • 2013年一月
    • 1999年二月

    分类目录

    • Debug
    • Distribution
    • Feature
    • Foundation
    • Hello world
    • Interaction
    • iOS
    • Others
    • Performance
    • Third Library
    • Worflow
    • Xcode

    "If today was the last day of my life would I want to do what I am about to do today? And whenever the answer has been "No" for too many days in a row I know I need to change something"

    © 2016 copyright SQUARE // All rights reserved //
    沪ICP备15033584号-2