10 | Android 高级进阶(源码剖析篇) Square 高效易用的 IO 框架 okio(三)

作者简介:ASCE1885, 《Android 高级进阶》作者。
本文由于潜在的商业目的,未经授权不开放全文转载许可,谢谢!
本文分析的源码版本已经 fork 到我的 Github

1520301887857.jpg

本文我们重点来介绍 okio 中的 segment 和 SegmentPool,segment 是一个底层的数据结构,本质上是一个字节数组,同时由于它被用于链表或者循环链表中,因此,segment 中也定义了链表的相关操作。而 SegmentPool 中保存的是未使用的 segment 的集合,需要使用 segment 时从这个池子中取用,用完后及时释放回这个池子,两者的关系如下图所示:

关系图

segment

接下来我们首先介绍 segment,从上图可以看到,segment 有两个核心概念:

  • 读取指针 pos:segment 数据读取时下一个要读取的字节位置
  • 写入指针 limit:segment 数据写入时将会写入的第一个字节位置

segment 根据其中的字节数据 final byte[] data 是否能够被多个 segment 共用,可以分为共享 segment非共享 segment,通过变量 boolean shared 来标识;同时同一份数据多个 segment 在使用时,通过变量 boolean owner 来区分谁是这份数据的拥有者,一份数据只有一个拥有者,其他的都是使用者,一般来说,数据拥有者对共享的 final byte[] data 数组拥有读写权限,数据使用者对其只有只读权限。

从 Segment 类的构造方法也可以看出它是否共享 segment:

/** segment 中字节数组的大小 */
static final int SIZE = 8192;

/** segment 拆分时,当数据量少于这个值时不做数据共享 */
static final int SHARE_MINIMUM = 1024;

/** segment 中实际存放数据的地方 */
final byte[] data;

/** 数据读取指针 */
int pos;

/** 数据写入指针 */
int limit;

/** 为 true 表示有其他 segment 和当前 segment 共享 data 数组 */
boolean shared;

/** 为 true 时表示当前 segment 拥有 data 数组,并能够进行数据的写入 */
boolean owner;
  
/** 非共享 segment */
Segment() {
    this.data = new byte[SIZE];
    this.owner = true;
    this.shared = false;
}

/** 共享 segment */
Segment(Segment shareFrom) {
    this(shareFrom.data, shareFrom.pos, shareFrom.limit);
    shareFrom.shared = true;
}

/** 共享 segment */
Segment(byte[] data, int pos, int limit) {
    this.data = data;
    this.pos = pos;
    this.limit = limit;
    this.owner = false;
    this.shared = true;
}

链表操作

当 segment 被用于链表或者循环链表时,就会存在指向链表前面一个 segment 的指针 prev,或者指向后面一个 segment 的指针 next,同时存在链表元素的添加和删除,熟悉数据结构的同学应该知道,这是典型的链表操作,链表元素的添加和删除基本上就是指针的指向操作。

双向链表在某个元素后面插入一个新元素的步骤如下:

  • 将新元素的前向指针 prev 指向当前元素
  • 将新元素的后向指针 next 指向当前元素的后面一个元素,也就是当前元素的 next 指针
  • 当前元素的前向指针改为指向新元素
  • 当前元素下一个元素的前向指针改为指向新元素

代码如下所示:

/** 链表或者双向循环链表中当前元素的后面一个元素指针 */
Segment next;

/** 双向循环链表中当前元素的前一个元素指针 */
Segment prev;

/** 在当前 segment 后面添加一个新的 segment */
public Segment push(Segment segment) {
    segment.prev = this;
    segment.next = next;
    next.prev = segment;
    next = segment;
    return segment;
}

双向链表删除当前元素并返回当前元素的后面一个元素的步骤如下:

  • 获取当前元素的后面一个元素(缓存起来)
  • 当前元素的前一个元素的后向指针改为指向当前元素的后一个元素
  • 当前元素的后一个元素的前向指针改为指向当前元素的前一个元素
  • 当前元素的前向指针和后向指针都设置为空,从而从链表中删除

代码如下所示:

/** 双向链表删除当前元素并返回当前元素的后面一个元素 */
public @Nullable Segment pop() {
    Segment result = next != this ? next : null;
    prev.next = next;
    next.prev = prev;
    next = null;
    prev = null;
    return result;
}

相信工科相关专业的同学应该都学习过严蔚敏的那本经典的数据结构教程,印象不深的可以回味一下。

segment 的拆分

为了高效的利用内存和方便 Buffer 的操作,segment 支持拆分和合并,拆分指的是将一个 segment 中从读取指针 pos 开始到写入指针 limit 之间的数据拆分到两个 segment 中。使用者需要指定一个字节数 byteCount 用来分割这部分数据,[pos ~ pos+byteCount) 之间的数据拆分到新的 segment 中,[pos+byteCount ~ limit) 之间的数据保留在原来的 segment 中。在链表的视角看,新拆分出来的 segment 位于原来 segment 的前面。

在拆分的过程中,涉及到新 segment 的生成,为了获得高性能,有如下两个理念截然相反的问题需要关注和平衡:

  • 尽可能避免数据拷贝,这个可以通过前面介绍过的共享 segment 来实现
  • 当拷贝数据量很小时,避免使用共享 segment,因为共享部分数据是只读的,而且可能会导致链表中存在很多数据量很小的 segment,影响性能

因此,在拆分时,只有当数据量 byteCount 不小于 segment 大小的八分之一(即 1024 字节)时,才会使用共享 segment,否则使用非共享 segment,并通过 System.arraycopy 进行数据的拷贝。然后修改新产生的 segment 的指针指向并将其插入链表中,代码如下所示:

public Segment split(int byteCount) {
    // 参数范围校验
    if (byteCount <= 0 || byteCount > limit - pos) throw new IllegalArgumentException();
    
    // 拆分出来的新 segment
    Segment prefix; 
    if (byteCount >= SHARE_MINIMUM) {
      // 共享 segment
      prefix = new Segment(this);
    } else {
      // 非共享 segment
      prefix = SegmentPool.take();
      System.arraycopy(data, pos, prefix.data, 0, byteCount);
    }

    prefix.limit = prefix.pos + byteCount;
    pos += byteCount;
    prev.push(prefix);
    return prefix;
}

上面代码中如果拆分出来的新 segment 走的是共享 segment 分支代码,那么拆分后新的 segment 和原来的 segment 在数据结构上是共享的,也就是两者的数据都存放在同一个 final byte[] data 中,两者不同的只是读取指针 pos 和写入指针 limit 的位置,如下图所示:

还有 56% 的精彩内容
©著作权归作者所有,转载或内容合作请联系作者
支付 ¥5.20 继续阅读
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • 1.OkHttp源码解析(一):OKHttp初阶2 OkHttp源码解析(二):OkHttp连接的"前戏"——HT...
    隔壁老李头阅读 10,489评论 24 42
  • 第一章 Nginx简介 Nginx是什么 没有听过Nginx?那么一定听过它的“同行”Apache吧!Ngi...
    JokerW阅读 32,453评论 24 1,003
  • 前言 Okio是一款轻量级IO框架,由安卓大区最强王者Square公司打造,是著名网络框架OkHttp的基石。Ok...
    开发者小王阅读 13,969评论 5 50
  • 前几天突然特别想回47,想再去喂喂怡湖的大金鱼,再去看看环形楼顶上我写的字,不知道有没有被刷掉。记忆里反复回味的场...
    LLLLY阅读 227评论 0 0
  • 这些天,比较忙碌。周边发生了较多的人、事变化,都没时间去好好关注。 昨天加班到凌晨,我也是拼了! 寄...
    随心语录阅读 294评论 0 1