比最差的API(ETW)更差的API(LTTng)是什么样炼成的, 谈如何写一个好的接口

现近来,拥有着 80% 的市场份额的 Android
是最主流的手机操作系统。它运行在不可胜言的智能手机、平板以及其余种种各种的装置上。仅凭那或多或少,我们是否可以认为
 Android 编程是几乎而轻松的啊?

近期这几天在帮柠檬看她的APM系统要什么样收集.Net运行时的各样风云,
这个事件包蕴线程开头, JIT执行, GC触发等等.
.Net在windows上(NetFramework, CoreCLR)通过ETW(伊芙nt Tracing for
Windows), 在linux上(CoreCLR)是透过LTTng跟踪事件.

几年前,Miley Cyrus 还在唱着流行乐,Justin(Justin) Bieber
还梳着他那闻名的“Bieber”发型,Malcolm 还在 AC/DC 乐队,而与此同时 Android
开发还一对一复杂。Android 开发者对于Android
系统开发最简便易行的行使都有一大堆问题。

ETW的API设计已经被过多少人诟病,
微软推出的类库krabsetw中直指ETW是最差的API再者把操作ETW的文本命名为噩梦.hpp.
而且那篇小说中, Casey
Muratori解释了为啥ETW是最差的API, 原因包蕴:

为啥?嗯,亲爱的读者,问题出在各样地方:

  • 事件类型应用了位标志(最多只好有32个), 没有考虑到未来的气象
  • 差其他接口共用一个大的构造体, 接口的输入和输出不强烈
  • 让调用者写无论怎么看都是多余的代码
  • 让调用者使用魔法数字(而不是提供一个枚举值)
  • 取名带有误导性
  • 再次来到值的意思不统一
  • 动用过度复杂, 没有先想好用例
  • 文档里面没有完好的以身作则代码, 只能够从零碎的代码拼凑

漏洞层出的IDE:你有没有试过用一把铲子去收拾你的汽车?或者你开着您曾外祖父的40年前的
Yugo 小车去把妹?在Android世界中,对于 Android 开发,大家有一个官方
IDE——Eclipse,它有一大堆问题,在10分钟以内有限支撑让你抓狂。Eclipse ADT
插件对于越来越多的繁杂工程以来也是满载漏洞、缓慢而不和谐的。大家对此良好恶心,祈祷能暴发奇迹来改正那总体。

系统崩溃:Gingerbread (2.3.7)在 Android
系统版本中占据着一定大的市场份额(至少15-20%)。正如你已清楚的,Android
正通过4.0本子(Ice Cream
Sandwich)经历着千头万绪的翻修进度。系统有了新的用户界面元素、新的配备硬件API、新的显示器密度等等,这就导致了大家必须小心地优化和编制咱们的应用来驱动在新版本Android和旧版本
Android
都能运行杰出。所有的那所有都大幅度地震慑了我们的成本过程和促成了越多的
bug 和 crash,以至于延长了开支时间。

悠悠的仿真器:大家需求在不一致的 Android
系统版本和屏幕尺寸测试大家的选用,所以我们必须买至少20种 Android
设备。听起来是还是不是很疯狂?好呢,大家能动用仿真器来解决。然则你曾有没有试过用默许的
Android
仿真器?它的缓慢让人忧伤,当你的施用正在被布署到您的仿真器的时候,你会让您自己去数商务楼前边停的车的数额来打发时光。

用户界面(UI):Android 应用无聊死了。若是你亵渎看一眼 iOS
应用,你会看出那个应用充满了生活气息而且色彩缤纷。所有的事物都是毋庸置疑的,动作转换,从左到右、从右到左……而我辈的利用是死的,倘诺我们想要进步我们的用户体验,老旧的Gingerbread
会很快抹杀大家的梦想和向往。

而是Casey Muratori的篇章对自己襄助很大,
我只用了1天时间就写出了应用ETW收集.Net运行时事件的以身作则代码.
日后我开端看怎么着使用LTTng收集这几个事件,
按照自己过去的经验linux上的类库api寻常会比windows的好用, 但LTTng是个例外.

但是这个都是2013的事了。

我首先件做的工作是去寻找怎么着在c程序里面LTTng的接口,
我打开了她们的文档下一场先导浏览.
登时我意识了她们的文档只谈了什么样行使代码发送事件,
却未曾其余表明怎么样用代码收受事件, 我发觉到自己应当去看源代码.

一个新起来

主人一起都在上年爆发了变更,改变暴发的如此之快,以至于你很简单地失去对它们的随行脚步,然后问自己“那都是哪些时候暴发的?”更紧要的是百分之百
Android
生态系统提升了好多——我们有了新的硬件(智能手表),新的软件(Gradle,Android
Studio),新的体系(Android 5.0 Lollipop)。

种种人都对此负有进献——谷歌、设备创制商、开发者。每个人都有相同的目的。问她们同样的这些问题:“OK。现在大家有稳定的系统,十亿计的利用和十亿计的用户——大家怎么才能更为简化和增加Android?我们怎么才能使得开发过程更好?”那就是 open access和 open source
原则呈现的他们的潜力——每个人都得以做出改变、发生升高、创立新的东西的四野。

很难列出整个的生成,但自我做了一个列表来列出其中(在我看来)最根本的成形:

初始化LTTng

使用LTTng跟踪事件首先必要创制一个对话, 启用事件和添加上下文参数,
然后启用跟踪, 在命令行里面是那般的调用:

lttng create --live
lttng enable-event --userspace --tracepoint DotNETRuntime:GCStart_V2
lttng add-context --userspace --type vpid
lttng add-context --userspace --type vtid
lttng start

lttng那一个命令的源代码在github上,
通过几分钟的探寻自己意识lttng的次第命令的兑现都是保存在本条文件夹下的.
打开create.c后又发现了创制会话调用的是lttng_create_session函数,
lttng_create_session函数可以由此引用lttng.h调用.
再过了几秒钟我写出了第一行代码

int ret = lttng_create_session_live("example-session", "net://127.0.0.1", 1000000);

运转后即刻就报错了, 错误是”No session daemon is available”.
原因是lttng-sessiond以此程序没有启动,
lttng是因此一个独自服务来治本会话的, 而那么些服务须求手动启动.

接纳独立服务本身没有错, 但是lttng-sessiond其一程序提供了重重参数,
如果一个只想跟踪用户事件的主次启动了这么些服务并点名了忽略内核事件的参数,
然后其它一个跟踪内核事件的程序将不可以正常运作.
没错的做法是选取systemd来启动那一个服务, 让系统管理员决定用什么参数,
而不是让调用者去启动它.

解决这些问题只须求简单残忍的两行, 启动时固然已经起步过新进程会破产,
没有任何影响:

system("lttng-sessiond --daemonize");
std::this_thread::sleep_for(std::chrono::seconds(1));

现在lttng_create_session_live会回去成功了, 然而又发现了新的问题,
创设的对话是由一个单独的劳务管理的, 尽管当前历程退出会话也会设有,
第二次创造的时候会再次回到一个已存在的错误.
本条题目和ETW的问题一模一样, 解决方法也如出一辙,
在创立会话前关闭它就可以了.

于是代码变成了那样:

system("lttng-sessiond --daemonize");
std::this_thread::sleep_for(std::chrono::seconds(1));
lttng_destroy_session(SessionName);
int ret = lttng_create_session_live("example-session", "net://127.0.0.1", 1000000);

透过一段时间后, 我用代码完结了和命令行一样的功用:

// start processes, won't replace exists
system("lttng-sessiond --daemonize");
std::this_thread::sleep_for(std::chrono::seconds(1));

// create new session
lttng_destroy_session(SessionName);
int ret = lttng_create_session_live(SessionName, SessionUrl, LiveSessionInterval);
if (ret != 0) {
    std::cerr << "lttng_create_session: " << lttng_strerror(ret) << std::endl;
    return -1;
}

// create handle from session
lttng_domain domain = {};
domain.type = LTTNG_DOMAIN_UST;
lttng_handle* handle = lttng_create_handle(SessionName, &domain);
if (handle == nullptr) {
    std::cerr << "lttng_create_handle: " << lttng_strerror(ret) << std::endl;
    return -1;
}

// enable event
lttng_event event = {};
event.type = LTTNG_EVENT_TRACEPOINT;
memcpy(event.name, EventName.c_str(), EventName.size());
event.loglevel_type = LTTNG_EVENT_LOGLEVEL_ALL;
event.loglevel = -1;
ret = lttng_enable_event_with_exclusions(handle, &event, nullptr, nullptr, 0, nullptr);
if (ret < 0) {
    std::cerr << "lttng_enable_event_with_exclusions: " << lttng_strerror(ret) << std::endl;
    return -1;
}

// add context
lttng_event_context contextPid = {};
contextPid.ctx = LTTNG_EVENT_CONTEXT_VPID;
ret = lttng_add_context(handle, &contextPid, nullptr, nullptr);
if (ret < 0) {
    std::cerr << "lttng_add_context: " << lttng_strerror(ret) << std::endl;
    return -1;
}

// start tracing
ret = lttng_start_tracing(SessionName);
if (ret < 0) {
    std::cerr << "lttng_start_tracing: " << lttng_strerror(ret) << std::endl;
    return -1;
}

到那边截止是否很简单? 即便没有文档, 不过那一个api都是非常简单的api,
看源代码就可以测算怎样调用.

1.ANDROID STUDIO

我们最欢欣的Andorid 开发的 IDE
终于成为了平稳的1.0版本了。我不会研究太多关于 AS
为何对于开发进程来说是最好的有关细节,因为大家曾经有两篇登出的博客覆盖了这一主旨。我会说
Eclipse ADT 插件已经不被合法赞成选用,我也强烈指出你把拥有的选取迁移到
 Android Studio。向 谷歌 致敬!

 新Android Studio Logo

收获事件

在告知LTTng启用跟踪后, 我还亟需得到发送到LTTng的轩然大波,
在ETW中得到事件是由此挂号回调获取的:

EVENT_TRACE_LOGFILE trace = { };
trace.LoggerName = (char*)mySessionName.c_str();
trace.EventRecordCallback = (PEVENT_RECORD_CALLBACK)(StaticRecordEventCallback);
trace.BufferCallback = (PEVENT_TRACE_BUFFER_CALLBACK)(StaticBufferEventCallback);
trace.ProcessTraceMode = PROCESS_TRACE_MODE_EVENT_RECORD | PROCESS_TRACE_MODE_REAL_TIME;
TRACEHANDLE sessionHandle = ::OpenTrace(&trace);
if (sessionHandle == INVALID_PROCESSTRACE_HANDLE) {
    // ...
}
ULONG processStatus = ::ProcessTrace(&sessionHandle, 1, nullptr, nullptr);

自我寻思lttng有没有这么的机制,
首先我看看的是lttng.h中的lttng_register_consumer函数,
那几个函数的诠释如下:

This call registers an "outside consumer" for a session and an lttng domain.
No consumer will be spawned and all fds/commands will go through the socket path given (socket_path).

翻译出来就是给会话注册一个表面的主顾, 听上去和自我的渴求很像啊?
那一个函数的第三个参数是一个字符串, 我想见是unix socket, lttng会通过unix
socket发送事件过来.
于是自己写了那样的代码:

ret = lttng_register_consumer(handle, "/tmp/custom-consumer");

一实施及时报错, 错误是Command undefined, 也就是命令未定义,
服务端不协助这么些命令.
因此查找发现lttng的源代码中一贯不其它调用这几个函数的地点,
也就是说那个函数是个装饰.
看起来这几个方式行不通.


经过一番摸索,
我发觉了live-reading-howto本条文档,
里面的始末非常少但是可以见见使用lttng-relayd其一服务可以读取事件.
读取事件近期只协理TCP, 使用TCP传输事件数量不仅复杂而且效能很低,
绝对ETW直接通过内存传递数据那无疑是个鲁钝的办法.
即使愚钝可是照旧要屡次三番写, 我起来看那TCP传输用的是怎么着协议.

对传输协议的解释文档在live-reading-protocol.txt,
这篇文档写的很不好, 但总比没有好.
lttng-relayd拓展相互使用的是一个lttng自己创立的半双工二进制协议,
设计如下:

客户端发送命令给lttng-relayd内需遵从以下的格式

[data_size: unsigned 64 bit big endian int, 命令体大小]
[cmd: unsigned 32 bit big endian int, 命令类型]
[cmd_version: unsigned 32 bit big endian int, 命令版本]
[命令体, 大小是data_size]

发送命令的设计小意思, 大多数二进制协议都是那般设计的,
问题在于接收命令的设计.
接收命令的格式完全依靠于发送命令的系列,
例如LTTNG_VIEWER_CONNECT其一命令发送过去会收到以下的多寡:

[viewer_session_id: unsigned 64 bit big endian int, 服务端指定的会话ID]
[major: unsigned 32 bit big endian int, 大版本]
[minor: unsigned 32 bit big endian int, 中版本]
[type: 客户端的类型]

能够见见接收的数量从未数据头, 没有数据头怎么着支配收取多少数量吧?
那就须要客户端定义的回应大小必须和服务端完全一致, 一个字段都不可能漏.
服务端在后头的翻新中无法给重回数据随意添加字段,
重返多少字段须求取决于发送过来的cmd_version,
保持api的包容性将会丰富的麻烦.
目前在lttng中cmd_version是一个留给字段,
也就是她们尚未仔细的想过api的更新问题.
不错的做法应该是回到数据也理应提供一个数据头,
然后同意客户端忽略多出去的数据.


看完协议以后, 我在想既然使用了二进制协议,
应该也会提供一个sdk来减弱解析的工作量吗?
通过一番寻找找到了一个头文件lttng-viewer-abi.h,
包括了和lttng-relayd相互使用的数据结构体定义.
以此头文件在源代码里面有, 可是却不在LTTng公布的软件包中,
那代表使用它须求复制它到品种里面.
复制外人的源代码到项目里面无法那么不论是,
看了一下LTTng的开源协议,
include/lttng/*src/lib/lttng-ctl/*下的文书是LGPL,
其他文件是GPL,
也就是地方假定把这几个头文件复制到自己的品类里面,
自己的品类必须采纳GPL协议开源
,
不想用GPL的话只可以把其中的内容自己一行行重新写, 还无法写的太像.

既是是测试就不管那样多了, 把这一个头文件的代码复制过来就从头持续写,
首先是连接受lttng-relayd:

int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (fd < 0) {
    perror("socket");
    return -1;
}
sockaddr_in address = {};
address.sin_addr.s_addr = inet_addr("127.0.0.1");
address.sin_family = AF_INET;
address.sin_port = htons(5344);
ret = connect(fd, (sockaddr*)&address, sizeof(address));
if (ret < 0) {
    perror("connect");
    return -1;
}

连天成功之后的相互流程在阅读方面的说道文档将来可以整理如下:

初始化
    客户端发送命令 LTTNG_VIEWER_CLIENT_COMMAND + 构造体 lttng_viewer_connect
    服务端返回构造体 lttng_viewer_connect
    客户端发送命令 LTTNG_VIEWER_CREATE_SESSION + 构造体 lttng_viewer_create_session_response
    服务端返回构造体 lttng_viewer_create_session_response
列出会话
    客户端发送命令 LTTNG_VIEWER_LIST_SESSIONS, 不带构造体
    服务端返回构造体 lttng_viewer_list_sessions + 指定长度的 lttng_viewer_session
附加到会话
    客户端发送命令 LTTNG_VIEWER_ATTACH_SESSION + 构造体 lttng_viewer_attach_session_request
    服务端返回构造体 lttng_viewer_attach_session_response + 指定长度的 lttng_viewer_stream
循环 {
    如果需要获取新的流 {
        客户端发送命令 LTTNG_VIEWER_GET_NEW_STREAMS + 构造体 lttng_viewer_new_streams_request
        服务端返回构造体 lttng_viewer_new_streams_response + 指定长度的 lttng_viewer_stream
    }
    如果需要获取新的元数据(metadata) {
        枚举现存的metadata流列表 {
            客户端发送命令 LTTNG_VIEWER_GET_METADATA + 构造体 lttng_viewer_get_metadata
            服务端返回构造体 lttng_viewer_metadata_packet + 指定长度的payload
        }
    }
    枚举现存的trace流列表 {
        客户端发送命令 LTTNG_VIEWER_GET_NEXT_INDEX + 构造体 lttng_viewer_get_next_index
        服务端返回构造体 lttng_viewer_index
        检查返回的 index.flags, 如果服务端出现了新的流或者元数据, 需要先获取新的流和元数据才可以继续
        客户端发送命令 LTTNG_VIEWER_GET_PACKET + 构造体 lttng_viewer_trace_packet
        服务端返回构造体 lttng_viewer_trace_packet + 指定长度的payload
        根据metadata packet和trace packet分析事件的内容然后记录事件
    }
}

是不是认为很复杂?
因为协议决定了服务端发给客户端的数量没有数据头,
所以服务端不可以积极推送数据到客户端, 客户端必须主动的去开展轮询.
若果你放在心上到构造体的称号,
会发现一些构造体前面有request和response而部分没有,
假如不看上下文只看构造体的称谓很难猜到它们的作用.
科学的做法是拥有请求和重回的构造体名称末尾都添加request和response,
不要去大约这么些字母而浪费思考的时间.


为了发送命令和接收构造体我写了一部分助手函数, 它们并不复杂,
使用TCP交互的先后都会有近似的代码:

int sendall(int fd, const void* buf, std::size_t size) {
    std::size_t pos = 0;
    while (pos < size) {
        auto ret = send(fd,
            reinterpret_cast<const char*>(buf) + pos, size - pos, 0);
        if (ret <= 0) {
            return -1;
        }
        pos += static_cast<std::size_t>(ret);
    }
    return 0;
}

int recvall(int fd, void* buf, std::size_t size) {
    std::size_t pos = 0;
    while (pos < size) {
        auto ret = recv(fd,
            reinterpret_cast<char*>(buf) + pos, size - pos, 0);
        if (ret <= 0) {
            return -1;
        }
        pos += static_cast<std::size_t>(ret);
    }
    return 0;
}

template <class T>
int sendcmd(int fd, std::uint32_t type, const T& body) {
    lttng_viewer_cmd cmd = {};
    cmd.data_size = htobe64(sizeof(T));
    cmd.cmd = htobe32(type);
    if (sendall(fd, &cmd, sizeof(cmd)) < 0) {
        return -1;
    }
    if (sendall(fd, &body, sizeof(body)) < 0) {
        return -1;
    }
    return 0;
}

开头化连接的代码如下:

lttng_viewer_connect body = {};
body.major = htobe32(2);
body.minor = htobe32(9);
body.type = htobe32(LTTNG_VIEWER_CLIENT_COMMAND);
if (sendcmd(fd, LTTNG_VIEWER_CONNECT, body) < 0) {
    return -1;
}
if (recvall(fd, &body, sizeof(body)) < 0) {
    return -1;
}
viewer_session_id = be64toh(body.viewer_session_id);

后边的代码相比干燥我就简单了,
想看完整代码的可以看这里.


跻身循环后会从lttng-relayd获得三种有效的数据:

  • 元数据(metadata), 定义了跟踪数据的格式
  • 跟踪数据(trace), 包罗了事件信息例如GC起先和终止等

赢得元数据利用的是LTTNG_VIEWER_GET_METADATA命令,
获取到的元数据内容如下:

Wu@"Jtf@oe/* CTF 1.8 */

typealias integer { size = 8; align = 8; signed = false; } := uint8_t;
typealias integer { size = 16; align = 8; signed = false; } := uint16_t;
typealias integer { size = 32; align = 8; signed = false; } := uint32_t;
typealias integer { size = 64; align = 8; signed = false; } := uint64_t;
typealias integer { size = 64; align = 8; signed = false; } := unsigned long;
typealias integer { size = 5; align = 1; signed = false; } := uint5_t;
typealias integer { size = 27; align = 1; signed = false; } := uint27_t;

trace {
    major = 1;
    minor = 8;
    uuid = "a3df4090-0722-4a74-97a4-81e066406f03";
    byte_order = le;
    packet.header := struct {
        uint32_t magic;
        uint8_t  uuid[16];
        uint32_t stream_id;
        uint64_t stream_instance_id;
    };
};

env {
    hostname = "ubuntu-virtual-machine";
    domain = "ust";
    tracer_name = "lttng-ust";
    tracer_major = 2;
    tracer_minor = 9;
};

clock {
    name = "monotonic";
    uuid = "f397e532-4837-402b-8cc9-700ed92a339d";
    description = "Monotonic Clock";
    freq = 1000000000; /* Frequency, in Hz */
    /* clock value offset from Epoch is: offset * (1/freq) */
    offset = 1514336042565610080;
};

typealias integer {
    size = 27; align = 1; signed = false;
    map = clock.monotonic.value;
} := uint27_clock_monotonic_t;

typealias integer {
    size = 32; align = 8; signed = false;
    map = clock.monotonic.value;
} := uint32_clock_monotonic_t;

typealias integer {
    size = 64; align = 8; signed = false;
    map = clock.monotonic.value;
} := uint64_clock_monotonic_t;

struct packet_context {
    uint64_clock_monotonic_t timestamp_begin;
    uint64_clock_monotonic_t timestamp_end;
    uint64_t content_size;
    uint64_t packet_size;
    uint64_t packet_seq_num;
    unsigned long events_discarded;
    uint32_t cpu_id;
};

struct event_header_compact {
    enum : uint5_t { compact = 0 ... 30, extended = 31 } id;
    variant <id> {
        struct {
            uint27_clock_monotonic_t timestamp;
        } compact;
        struct {
            uint32_t id;
            uint64_clock_monotonic_t timestamp;
        } extended;
    } v;
} align(8);

struct event_header_large {
    enum : uint16_t { compact = 0 ... 65534, extended = 65535 } id;
    variant <id> {
        struct {
            uint32_clock_monotonic_t timestamp;
        } compact;
        struct {
            uint32_t id;
            uint64_clock_monotonic_t timestamp;
        } extended;
    } v;
} align(8);

stream {
    id = 0;
    event.header := struct event_header_compact;
    packet.context := struct packet_context;
    event.context := struct {
        integer { size = 32; align = 8; signed = 1; encoding = none; base = 10; } _vpid;
        integer { size = 32; align = 8; signed = 1; encoding = none; base = 10; } _vtid;
    };
};

event {
    name = "DotNETRuntime:GCStart_V2";
    id = 0;
    stream_id = 0;
    loglevel = 13;
    fields := struct {
        integer { size = 32; align = 8; signed = 0; encoding = none; base = 10; } _Count;
        integer { size = 32; align = 8; signed = 0; encoding = none; base = 10; } _Depth;
        integer { size = 32; align = 8; signed = 0; encoding = none; base = 10; } _Reason;
        integer { size = 32; align = 8; signed = 0; encoding = none; base = 10; } _Type;
        integer { size = 16; align = 8; signed = 0; encoding = none; base = 10; } _ClrInstanceID;
        integer { size = 64; align = 8; signed = 0; encoding = none; base = 10; } _ClientSequenceNumber;
    };
};

其一元数据的格式是CTF Metadata,
这一个格式看上去像json可是并不是, 是LTTng的店家温馨创设的一个文本格式.
babeltrace中包罗明白析这些文本格式的代码,
然而没有开屏弃何解析它的接口,
也就是一旦你想协调分析只可以写一个词法分析器.
这几个格式其实可以采取json表示, 体积不会增多多少,
可是那集团就是发明了一个新的格式扩大使用者的负担.
写一个词法分析器要求1天时间和1000行代码, 那里我就先跳过了.


接下去获取跟踪数据,
使用的是LTTNG_VIEWER_GET_NEXT_INDEX和LTTNG_VIEWER_GET_PACKET命令.
LTTNG_VIEWER_GET_NEXT_INDEX重返了当前流的offset和可获得的content_size,
这里的content_size单位是位(bit),
也就是内需除以8才足以算出能够得到多少字节,
关于content_size的单位LTTng中从不其余文档和注释表达它是位,
唯有一个测试代码里面的某行写了/ CHAR_BIT.
语言,使用LTTNG_VIEWER_GET_PACKET命令,
传入offset和content_size/8可以获得跟踪数据(借使不/8会获得到剩余的数据或者重临ERR).
实际上再次回到的跟踪数据如下:

000000: c1 1f fc c1 29 82 6b fe 24 10 4c 6b 97 91 4d c3  ....).k.$.Lk..M.
000010: ed d4 41 8f 00 00 00 00 03 00 00 00 00 00 00 00  ..A.............
000020: 92 91 49 96 08 0a 00 00 07 a0 58 b9 08 0a 00 00  ..I.......X.....
000030: 50 05 00 00 00 00 00 00 00 80 00 00 00 00 00 00  P...............
000040: 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000050: 03 00 00 00 1f 00 00 00 00 92 91 49 96 08 0a 00  ...........I....
000060: 00 e1 1b 00 00 03 00 00 00 02 00 00 00 01 00 00  ................
000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1f  ................
000080: 00 00 00 00 4d ae a7 af 08 0a 00 00 e1 1b 00 00  ....M...........
000090: 04 00 00 00 02 00 00 00 01 00 00 00 00 00 00 00  ................
0000a0: 00 00 00 00 00 00 00 00 00 00                    ..........

跟踪数据的格式是CTF Stream
Packet
,
也是一个自定义的二进制格式, 要求般配元数据解析.
babeltrace中一样没有开放解析它的接口(有python
binding但是从未解析数据的函数), 也就是须要自己写二进制数据解析器.

操作LTTng + 和relayd通信 + 元数据词法分析器 +
跟踪数据解析器全体加起来推断需求2000行代码,
而这所有应用ETW只用了100多行代码.
糟糕的布署, 复杂的使用, 落后的文档, 各个种种的自定义协议和数码格式,
不提供SDK把LTTng打造成了一个比ETW更难用的跟踪系统.
近年来在github上LTTng唯有100多星而babeltrace唯有20多,
也申明了从未稍微人在用它们.
本身不精通为啥CoreCLR要用LTTng, 但欣慰的是CoreCLR
2.1会有新的跟踪机制EventPipe,
到时候可以更简便的落到实处跨平台捕获CoreCLR跟踪事件.

自我眼前写的调用ETW的代码放在了这里,
调用LTTng的代码放在了这里,
有趣味的能够去参考.

2.GRADLE

Gradle 是工程自动化工具,它曾经代表 Apche Ant 成为 Android
应用关键的构建系统。它在 Android
开发者中万分流行。因为大家透过它差不离可以自动化所有事情——从将大家的应用区分成不一样风格、正确配置签名等等

故而,他变成了一种类的“管理”工具,大家用来定义和保险大家的工程安装。Gradle
也是测试自动化库和活动构建服务器大量加强的首要原因。测试自动化库和自行构建服务器又给
 Android
系统带来了连绵不断集成(CI)开发进度。可是或不是一切都是那么令人开展——Gradle也在履行进度上遭到批评。在千头万绪工程地方Gradle 也确实很慢,但我们愿意这几个题目会在接下去的本子和发行中化解。

教训

最差的API(ETW)和更差的API(LTTng)都看过了, 那么相应怎么着防止他们的谬误,
编写一个好的API呢?

Casey
Muratori
事关的训诫有:

3.LOLLIPOP

谷歌 说 Lollipop 是自人类诞生以来 Android 系统最大的升官,谷歌说的正确性。 Android
的每个部分都有相应的改动和升级,可是我们也从没看到开发者对这个改动有如何的反馈。固然将旧设备升级到
Lollipop 还有许多题材,可是我们盼望那会在接下去的本子中解决。

规划API的第一条和第二条规则: “永远都从编写用例开端”

统筹一个API时, 首先要做的是站在调用者的立足点, 想想调用者必要哪些,
如何才能最简易的直达这么些须求.
编写一个差不多的用例代码永远是设计API中必须的一步.
决然则多的去想内部贯彻, 假诺内部贯彻机制让API变得复杂,
应该想办法去抽象它.

4.LOLLIPOP 的外在—— MATERIAL DESIGN

对此这一个叫作 Material Design 的金光闪闪的新 Android UI
有那些要写。那是近日几年Android
系统最要紧创新点之一,它完全改变了我们运用的观感。我最欣赏 Material
Design
的是它彻底改变了用户体验条件——一切都重点。即使是细微的底细也不可以被忽略。大家不可以不对种种用户交互、点击、触摸等做出响应。因为,那正如
谷歌(Google)所说的,那么些动作都是有意义的。大家必须运用金鼎文、拥抱新的鲜活的色彩、每一步使用动画片、大字体,简单地说,我们要给我们的选用以生命。Material
Design 同样也完全符合 Android
生态系统,适应各类差距的屏幕尺寸。那也就是干吗我们的运用是形似的,不过在不相同的阳台具有不均等的外观。

 Material Design 动画

考虑到未来的恢弘

因为须要会不断变更, 设计API的时候理应为未来的更动预留空间,
有限帮衬向后卓殊性.
例如ETW中监听的轩然大波类型.aspx)使用了位标记,
也就是参数是32位时最八只能有32种事件,
考虑到未来有更多事件应该把事件类型定义为一连的数值并提供额外的API启用事件.
近期有众多接口在规划时会考虑到版本, 例如用v1和v2区分,
那是一个很好的策略.

5.LOLLIPOP 的内在—— ART

各样人都在谈论设计、UI、UI
元素、动画、色彩······,不过大家是开发者,大家感兴趣的是表面之下的东西。而且,哇!!!那引擎真是美极了:ART,新的运作系统。为了记录,ART
并不是什么新东西—它被介绍为 基特(Kit)kat 上支持的运转种类。通过引入
Lollipop,它完全代表了 Dalvik,成为主系统。由于众多原因 ART
是伟人的,但我只提及其中两点:

一、它使用
AOT(ahead-of-time)编译,那表示它把高中级语言(Dalvik字节码)编译成系统二进制码。那就造成大家使用更短的进行时间、更少的
CPU 占用、更少的电池组消耗。在一边,安装进度也就更长。

二、他提供 multidex 支持。Dalvik dex
文件有个至关紧要瑕疵—它们只可以分包65,356种方法。大家必须协会好大家的
Android
应用以使方法毫无超过那个界定。固然这几个数字可能看起来很大,但是一旦您把
谷歌 Play
服务(几乎各样应用都必要)算在内,再添加有些外部函数库,你就能自由超过这一个限制。ART
以一种突破了字节码以重重 dex 文件打包到一个单身的 APK
的措施社团你的运用。

明明接口的输入和出口

不用为了节约代码去让一个接口接收或者再次回到多余的音讯.
在ETW中有的是接口都共用了一个大构造体EVENT_TRACE_PROPERTIES,
调用者很难搞领会接口使用了构造体里面的什么样值, 又影响了怎么值.
统筹API时应有明了接口的目标, 让接口接收和重临须要且最少的音信.

6.ANDROID 无处不在

俺们开头给智能手表、电视机、小车开发使用,为啥要在此下马呢?倘若您坐在你的屋子,喝着了一杯热咖啡,花一两分钟看看你的周围。在接下去的这几年你恐怕会看出至少五样运行着
Android
系统的设备—TV、台式机、平板、相机、自行车、厨房电器、恒温器、小车等等。Android
先导作为一种试验,它被认证可以运转在任何一个独具小型微处理器的东西上边。

提供完整的示范代码

对调用者来说, 100行的演示代码常常比1000行的文档更有意义.
因为接口的设计者和调用者拥有的知识量平日不对等,
调用者在未曾观察实际的例证以前, 很可能不可能清楚设计者编写的文档.

7.智能手机质地的升高

智能手机仍旧Android
系统的中坚装备。长期以来,智能手机的完整质地有问题。老旧的Android
设备比老旧的 魅族 更丑更慢——iOS
寻常感觉更通畅。对于那多少个被广大中国创制商们生产的廉价设备来说,那种感受越来越如此。

幸运地是,Android
智能手机的质料和进度逐步进步,所以今日我们有过多适合各种人的预算和内需的新设施。如若你想拥有一台手机,它具备很高的相机分辨率、出色的筹划、强大的统计机和电量,这不是个问题——大家都有。

自家个人最欢快的品牌是诺基亚,它的无绳电话机—Moto X、Moto G和Moto E
都拥有雅观的线条,同时也真的有着很好的性价比。而在同时,谷歌的一个团协会正力于模块化手机的开发。Project Ara 目的在于彻底动摇 Android
世界,倘若整个举办顺遂,它有可能会来到人们眼前。

Project Ara 部分

毫无接纳魔法数字

那是不少接口都会犯的荒谬, 例如ETW中决定事件附加的音信时, 1代表时间戳,
2表示系统时间, 3表示CPU周期计数.
即使你要求传递具有某种意义的数字给接口,
请务必在SDK中为该数字定义枚举类型.

自家从LTTng中接收到的教训有:

下一步何去何从?

写文档

99%的调用者没有看源代码的兴味或者能力,
不写文档没有人会知晓什么去调用你的接口.
当今有成百上千自动生成文档的工具, 用那么些工具得以减去过多的工作量,
可是您如故应该手动去编写一个入门的文档.

远离JAVA

咱俩早就缓解了 IDE 和种类版本的超过一半题材,大家就可以关切 Android
其余地点的题目。

恕我直言,在 Android 开发最焦点的题材中最珍惜的题目是 Java。对不起,Java
Harmony,基于 Java 7 或 Java6,但它不是
Java。不要让自家放错——我坚信Java是一门好的编程语言,但是本人也以为大家是时候打破常规了。大家要求初始搜索别的一门编程语言来取代
Java 成为 Android 开发的中坚语言。

看望大家最要紧的竞争者—Apple。他们已经介绍了一门全新的言语,叫做
斯维·夫特(Sw·ift),它构成了数个其余语言(如 Python、Ruby 或
C#)的最优特征。大家已经比 iOS
开发者开发同一应用须要更加多的时日,而那会使大家更慢。

那就是怎么大家要求新东西的插手了。大家曾经有了有关哪个语言能够替代Java的有的想方设法。我觉着是
Groovy。它的语法与 Java 万分相似(实际上,它是按照 Java
的),大家也有一些干活原型了。同时,也并非忘了它是 Gradle
的主语言——所以,为啥不把它用来Android 开发呢?或者可能是
Scala(它可以火速获得新用户),又或者是 Kotlin(Jake Wharton
方今写了一篇很好的关于用于 Android 的 Kotlin 的概论)?

不用轻易的去创建一个磋商

创办一个新的商议表示需求编制新的代码去分析它,
而且每个程序语言都要双重编辑五回.
只有你很有活力, 可以为主流的程序语言都提供一个SDK, 否则不引进那样做.
有的是品类都提供了REST API, 那是很好的矛头,
因为大约每个语言都有现成的类库可以方便地调用REST API.

数据库管理变得更好

自身要提议另一个题材—数据库管理 API。假如你再一次亵渎
Andoird,看一眼大家的竞争对手—iOS(要旨数据,将特别纯粹)——你会合到他们真的拥有大好的章程和成立数据库对象的GUI
和 CRUD 方法,数据库变化监听器。可是即使您回头看下默许的 Android API
——我们还尚未妻离子散写那个极大地震慑我们开发进度的 SQL 命令。

调节 SQL
错误不是一件简单的事—它那些消耗时间,我们也未曾翻动数据库数据的GUI。即使也有局部科学的
ORM 库(如 格林(Green)DAO、ActiveAndroid 或
SugarORM),但是它们都有和好的题材。我从不曾对它们统统令人满足—他们要不是行使很复杂,要不就是遗失一些东西(如数据库改变监听器)。我留意到了
Realm for Android 和
DBFlow,我梦想她们会缓解我具备的题材同时裁减执行时间。

当心的去定义二进制协议

概念一个好的二进制协议需求很深的造诣, LTTng定义的协议显然考虑的太少.
推介的做法是明显区分请求和回答, 请求和回复都应当有一个带有长度的头,
支持全双工通讯.
假定您想设计一个二进制协议,
强烈建议参考卡桑德拉数据库的讨小说档,
那个协议无论是设计照旧文档都是头等的水平.
而是一旦您未曾对传输性能有很苛刻的须求, 提出选用现成的合计加json或者xml.

结论

Android
在过去的几年暴发了伟大的变更。它已经从一个简约的智能手机系统提升为一个支持各样装备的无敌系统。时间会报告大家Android
将会成为啥。什么人知道几时大家会不会甚至可以用它来给核聚变反应堆编程,或者给”终结者“编程。PS.
显然终结者更有趣。

那是自身课余时间的翻译,错误很多,还请耐心指出,谢谢!

原稿链接:https://www.infinum.co/the-capsized-eight/articles/the-past-present-and-future-of-android-development

不要去创制一个DSL(Domain-specific language)

此间自己从不写轻易, 若是你有一个数据结构需求代表成文本,
请使用更通用的格式.
LTTng表示元数据时行使了一个团结创制的DSL,
但里面的始末用json表示也不会扩展多少体积,
也就是说创建一个DSL没有其它好处.
剖析DSL须求自己编排词法分析器,
尽管是经历老到的程序员编写一个也需求广大岁月(包括单元测试越来越多),
假诺使用json等通用格式那么编写解析的代码只须求几分钟.

写在结尾

即便如此那篇文章把LTTng批评了一番,
但那也许是当下天下唯一一篇涉嫌怎么样通过代码调用LTTng和收取事件数量的小说.
梦想看过那篇文章的规划API时多为调用者着想,
你偷懒省下几分钟往往会促成外人浪费几天的时间.

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图