Index

google protocol buffer总结

以前公司的项目使用protobuf,现在公司的项目不用这个。所以总结一下以前的经验,以备以后再用的时候能快速捡回来。

protocol buffer简称protobuf或者pb,出自Google之手。pb跨平台,解决了不同平台的数据字节对齐和串行化问题;使用Google新的编码方式,压缩后体积小,传输效率高。

Google官方的版本里面,只有Java 、C++和C版本。OC版本的在github上有一些项目,推荐Google Protocol Buffers for Objective-CProtocol Buffers for Objective C

按照git上的说明安装pb

Google Protocol Buffers总结-基础篇

转载请注明出处:http://elijahdou.github.io/

以前的公司主要是用protobuf做协议和数据传输,现在公司不用这个,为了以后方便使用,把以前的使用经验总结一下,方便以后重拾回来。


protobuf

Protocol Buffers简称protobuf或者pb,是由Google开发维护的夸语言、跨平台的 可扩展的结构化数据串行机制,官方地址Protocol Buffers

现在常用的网络数据传输的结构化方式有:XML、JSON和Protobuf等。有很多篇介绍他们的对比和不同:blogprotobuf,json,xml,binary,Thrift之间的对比 使用 Protocol Buffers 代替 JSON 的五个原因。网上还有很多其他对比的帖子。不过简言之,protobuf的优缺点如下:

  • 效率高:用protobuf序列化后的数据大小是json的10+分之一,xml格式的20+分之一,是二进制序列化的10+分之一。protobuf虽然会需要一定的本地编解码成本,但是数据压缩体积变小,网络传输速度加快,在效率和体验上的提升非常可观。
  • 语法简答易学:保证做一次就会。
  • 夸语言、跨平台、可扩展:不收平台的限制,并且无偿向后兼容。
  • 缺点:唯一的缺点就是可读性差,因为数据都是编码后的,不像XML和JSON可读。

安装

Google的官方版本,只提供了JAVA C++ C 和 Python版本的pb,但是在github的开源项目上有OC和最新的SWIFT语言的版本。git还在维护的版本有qzix/protobuf-objcalexeyxo/protobuf-objc,其中后者还支持swift,推荐使用后者,同时安装步骤也在该页。借助homebrew安装更方便,如果已安转homebrew请跳过第一步,摘录如下:

操作前请先获取系统权限,sudo一下

  1. ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"
  2. brew install automake
  3. brew install libtool
  4. brew install protobuf
  5. ln -s /usr/local/Cellar/protobuf/2.6.0/bin/protoc /usr/local/bin (optional)
  6. git clone git@github.com:alexeyxo/protobuf-objc.git
  7. ./build.sh
  8. Add /src/runtime/ProtocolBuffers.xcodeproj in your project.

使用-编写.proto源文件

使用pb的第一步就是编写.proto源文件,选用自己顺手的任一款编辑器即可。对应OC中的类,protobuf称为message消息,你会发现其语法结构与常见语言类似,所以简单易学。举例代码入下:

message Person {
    required int32 age = 1;
    repeated string name = 2;
    optional string email = 3;
}

.proto源文件格式从上面的源码中可以猜测一个大概,举例并解析如下:

message msgName(消息名称) {
    required/repeated/optional(限定符) dataType(数据类型) variableName(变量名) =(赋值号) serialNumber(序号,必须从1开始,不能跳跃);(结束的分号)
}

关于msgName

.proto中可以有多个message,dateType也可以是自定义message,但是源文件最好与主message同名,即mainMsgName.proto,否则在使用的时候会出现一些名称不一致的问题,虽然仍能影响使用,但是会增加使用难度。
tips:一些公共的协议类的.proto源文件,可以独立成一个文件,在其他使用到的该消息的文件的开头位置用import引入即可,如import "myproject/CommonMessages.proto"

关于required/repeated/optional(限定符)

  1. 在每个消息中必须至少留有一个required类型的字段(在实际使用中,可以全部是optional的),这样的变量在相应类的初始化时必须初始化,否则报错。
  2. 每个消息中可以包含0个或多个optional类型的字段,从名字可以看出,可选的,可以不初始化或者不使用该字段。推荐尽可能使用optional限定符,即使协议文档规定为required,两者仍可以兼容,并且避免以后协议修改造成的重写和重新编译。
  3. repeated表示的字段可以包含0个或多个数据。需要说明的是,这一点有别于C++/Java中的数组,因为后两者中的数组必须包含至少一个元素,可以理解为repeated也是一种可选类型。
  4. 如果打算在原有消息协议中添加新的字段,同时还要保证老版本的程序能够正常读取或写入,那么对于新添加的字段必须是optional或repeated。道理非常简单,老版本程序无法读取或写入新增的required限定符的字段。

关于dataType(数据类型)

这个很简单,数据类型名字也很好记,有表格如下:

.proto Type Notes C++ Type Java Type
double    double  double
float    float  float
int32 Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.  int32  int
int64 Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.  int64  long
uint32 Uses variable-length encoding.  uint32  int
uint64 Uses variable-length encoding.  uint64  long
sint32 Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.  int32  int
sint64 Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.   int64  long
fixed32 Always four bytes. More efficient than uint32 if values are often greater than 228  uint32  int
fixed64 Always eight bytes. More efficient than uint64 if values are often greater than 256.  uint64  long
sfixed32 Always four bytes.  int32  int
sfixed64 Always eight bytes.  int64  long
bool    bool  boolean
string A string must always contain UTF-8 encoded or 7-bit ASCII text.  string  String
bytes May contain any arbitrary sequence of bytes. string ByteString

oc语言的数据类型参考C++ 版本即可,此外pb还支持枚举类型的数据定义,举例如下:

enum UserStatus {
     OFFLINE = 0;
     ONLINE = 1;
}
message UserInfo {
    required int64 acctID = 1;
    required string name = 2;
    required UserStatus status = 3;
}
message LogonRespMessage {
    required LoginResult logonResult = 1;
    required UserInfo userInfo = 2;
}

使用-编译.proto源文件

完成源文件的编写之后,就是要编译该源文件,编译的环境和命令在安装时就已经配置好了,我们只需要调用指令编译即可。编译得到的结果对于OC来说会生成一对.h .m文件,即一个类,类名即为.proto文件的名字;对于swift语言因该是生成一个.swift文件。编译指令以如下:

protoc .proto源文件路径 --objc_out="输出文件路径"

protoc .proto源文件路径 --swift_out="输出文件路径"


使用-项目中引用

将上一步生成的目标文件引入到项目中,就可以当做普通的类来使用了。

tips:不同的oc版本的pb的开源项目略有不同,不过大同小异,如类名的命名方式等,一个较大的区别是有的支持ARC,有的不支持ARC 需要禁用,所以推荐用cocopods引入。

实际使用举例,以前面的Person.proto为例,常规的使用步骤如下(当然要导入必要的头文件):

- (void)viewDidLoad 
{
    // 串行化一个,Person结构的数据
    NSData *personData = [self makePersonDataWithAge:21
                                               names:@[@"测试", @"test"]
                                               email:@"test@zuche.com"];
    NSLog(@"data : %@", personData); // 用于数据传输
    
    // 反串行化得到的串行化数据
    Person *aPerson = [Person parseFromData:personData];
    NSLog(@"person age : %d\n person names : %@\n person email : %@\n", aPerson.age, aPerson.names, aPerson.email);  // 接收到的数据的还原
}


- (NSData *)makePersonDataWithAge:(int32_t)age names:(NSArray *)names email:(NSString *)email
{
    PersonBuilder *builder = [[PersonBuilder alloc] init]; // 有的版本的是这样的Person_Builder,不过大同小异
    builder.age = age;
    builder.email = email;
    [builder setNamesArray:names];
    
    Person *args = [builder build];
    
    return [args data];
}

swift Demo

总结

这只是pb使用的基础用法,看源码能得到很多收获。pb的精髓在于其编码方式,有兴趣的可以看一下。这一篇只是讲了pb的基本使用,再其使用过程中,还有很多需要注意的点,这一篇中就不讲了。

补充延伸阅读

Google Protocol Buffers 概述

Protobuf语言指南

Google Protocol Buffers 编码(Encoding)

weak变量的生命周期及具体实现方法

本文转自weak的生命周期:具体实现方法,看着不错,有助于理解OC就收藏了。

我们都知道weak表示的是一个弱引用,这个引用不会增加对象的引用计数,并且在所指向的对象被释放之后,weak指针会被设置的为nilweak引用通常是用于处理循环引用的问题,如代理及block的使用中,相对会较多的使用到weak

之前对weak的实现略有了解,知道它的一个基本的生命周期,但具体是怎么实现的,了解得不是太清晰。今天又翻了翻《Objective-C高级编程》关于__weak的讲解,在此做个笔记。

我们以下面这行代码为例:
{
    id __weak obj1 = obj;
}

当我们初始化一个weak变量时,runtime会调用objc_initWeak函数。这个函数在Clang中的声明如下:

id objc_initWeak(id *object, id value);
其具体实现如下:
id objc_initWeak(id *object, id value)
{
    *object = 0;
    return objc_storeWeak(object, value);
}
示例代码轮换成编译器的模拟代码如下:
id obj1;
objc_initWeak(&obj1, obj);

因此,这里所做的事是先将obj1初始化为0(nil),然后将obj1的地址及obj作为参数传递给objc_storeWeak函数。

objc_initWeak函数有一个前提条件:就是object必须是一个没有被注册为__weak对象的有效指针。而value则可以是null,或者指向一个有效的对象。

如果value是一个空指针或者其指向的对象已经被释放了,则objectzero-initialized的。否则,object将被注册为一个指向value__weak对象。而这事应该是objc_storeWeak函数干的。objc_storeWeak的函数声明如下:

id objc_storeWeak(id *location, id value);
其具体实现如下:
id objc_storeWeak(id *location, id newObj)
{
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;
    ......
    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry:
    oldObj = *location;
    oldTable = SideTable::tableForPointer(oldObj);
    newTable = SideTable::tableForPointer(newObj);
    ......
    if (*location != oldObj) {
        OSSpinLockUnlock(lock1);
#if SIDE_TABLE_STRIPE > 1
        if (lock1 != lock2) OSSpinLockUnlock(lock2);
#endif
        goto retry;
    }
    if (oldObj) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }
    if (newObj) {
        newObj = weak_register_no_lock(&newTable->weak_table, newObj,location);
        // weak_register_no_lock returns NULL if weak store should be rejected
    }
    // Do not set *location anywhere else. That would introduce a race.
    *location = newObj;
    ......
    return newObj;
}

我们撇开源码中各种锁操作,来看看这段代码都做了些什么。在此之前,我们先来了解下weak表和SideTable

weak表是一个弱引用表,实现为一个weak_table_t结构体,存储了某个对象相关的的所有的弱引用信息。其定义如下(具体定义在objc-weak.h中):

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    ......
};

其中weak_entry_t是存储在弱引用表中的一个内部结构体,它负责维护和存储指向一个对象的所有弱引用hash表。其定义如下:

struct weak_entry_t {
    DisguisedPtr referent;
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line : 1;
            ......
        };
        struct {
            // out_of_line=0 is LSB of one of these (don't care which)
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };
};

其中referent是被引用的对象,即示例代码中的obj对象。下面的union即存储了所有指向该对象的弱引用。由注释可以看到,当out_of_line等于0时,hash表被一个数组所代替。另外,所有的弱引用对象的地址都是存储在weak_referrer_t指针的地址中。其定义如下:

typedef objc_object ** weak_referrer_t;

SideTable是一个用C++实现的类,它的具体定义在NSObject.mm中,我们来看看它的一些成员变量的定义:

class SideTable {
private:
    static uint8_t table_buf[SIDE_TABLE_STRIPE * SIDE_TABLE_SIZE];
public:
    RefcountMap refcnts;
    weak_table_t weak_table;
    ......
}

RefcountMap refcnts,大家应该能猜到这个做什么用的吧?看着像是引用计数什么的。哈哈,貌似就是啊,这东东存储了一个对象的引用计数的信息。当然,我们在这里不去探究它,我们关注的是weak_table。这个成员变量指向的就是一个对象的weak表。

了解了weak表和SideTable,让我们再回过头来看看objc_storeWeak。首先是根据weak指针找到其指向的老的对象:

oldObj = *location;

然后获取到与新旧对象相关的SideTable对象:

oldTable = SideTable::tableForPointer(oldObj);
newTable = SideTable::tableForPointer(newObj);

下面要做的就是在老对象的weak表中移除指向信息,而在新对象的weak表中建立关联信息:

if (oldObj) {
    weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
if (newObj) {
    newObj = weak_register_no_lock(&newTable->weak_table, newObj,location);
    // weak_register_no_lock returns NULL if weak store should be rejected
}

接下来让弱引用指针指向新的对象:

*location = newObj;

最后会返回这个新对象:

return newObj;

objc_storeWeak的基本实现就是这样。当然,在objc_initWeak中调用objc_storeWeak时,老对象是空的,所有不会执行weak_unregister_no_lock操作。

而当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?当释放对象时,其基本流程如下:

  • 调用objc_release
  • 因为对象的引用计数为0,所以执行dealloc
  • dealloc中,调用了_objc_rootDealloc函数
  • _objc_rootDealloc中,调用了object_dispose函数
  • 调用objc_destructInstance
  • 最后调用objc_clear_deallocating

我们重点关注一下最后一步,objc_clear_deallocating的具体实现如下:

void objc_clear_deallocating(id obj) 
{
    ......
    SideTable *table = SideTable::tableForPointer(obj);
    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    OSSpinLockLock(&table->slock);
    if (seen_weak_refs) {
        arr_clear_deallocating(&table->weak_table, obj);
    }
    ......
}

我们可以看到,在这个函数中,首先取出对象对应的SideTable实例,如果这个对象有关联的弱引用,则调用arr_clear_deallocating来清除对象的弱引用信息。我们来看看arr_clear_deallocating具体实现:

PRIVATE_EXTERN void arr_clear_deallocating(weak_table_t *weak_table, id referent) {
    {
        weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
        if (entry == NULL) {
            ......
            return;
        }
        // zero out references
        for (int i = 0; i < entry->referrers.num_allocated; ++i) {
            id *referrer = entry->referrers.refs[i].referrer;
            if (referrer) {
                if (*referrer == referent) {
                    *referrer = nil;
                }
                else if (*referrer) {
                    _objc_inform("__weak variable @ %p holds %p instead of %p\n", referrer, *referrer, referent);
                }
            }
        }
        weak_entry_remove_no_lock(weak_table, entry);
        weak_table->num_weak_refs--;
    }
}

这个函数首先是找出对象对应的weak_entry_t链表,然后挨个将弱引用置为nil。最后清理对象的记录。

通过上面的描述,我们基本能了解一个weak引用从生到死的过程。从这个流程可以看出,一个weak引用的处理涉及各种查表、添加与删除操作,还是有一定消耗的。所以如果大量使用__weak变量的话,会对性能造成一定的影响。那么,我们应该在什么时候去使用weak呢?《Objective-C高级编程》给我们的建议是只在避免循环引用的时候使用__weak修饰符。

另外,在clang中,还提供了不少关于weak引用的处理函数。如objc_loadWeak, objc_destroyWeak, objc_moveWeak等,我们可以在苹果的开源代码中找到相关的实现。等有时间,我再好好研究研究。

参考

《Objective-C高级编程》1.4: __weak修饰符

Clang 3.7 documentation – Objective-C Automatic Reference Counting (ARC)

apple opensource – NSObject.mm