14 Mar 2016

android FlatBuffers剖析

android FlatBuffers剖析

概述

FlatBuffers是google最新针对游戏开发退出的高性能的跨平台序列化工具,目前已经支持C++, C#, Go, Java, JavaScript, PHP, and Python (C和Ruby正在支持中),相对于json和Protocol Buffers,FlatBuffers在序列化和反序列化方面表现更为优异,而且需要的资源更少,更适合大部分移动应用的使用场景。

FlatBuffers的特点

除了高性能和低内存消耗的特点,FlatBuffers还有其他一些优势,官方的总结说明如下:

  • 不用解析也能对数据进行访问
    FlatBuffers使用二进制结构化的方法来存储数据,可以不用先对整段数据进行解析就可以直接在二进制结构中访问到指定成员的信息。并且FlatBuffers还支持存储数据结构的前后兼容。
  • 内存占用小,运行效率高
    FlatBuffers使用ByteBuffer进行数据存储,在FlatBuffers的序列化和反序列化过程中不会额外占用其他内存空间,FlatBuffers读取数据的速度和直接从内存读取原始数据相当,仅仅多了一个相对寻址的耗时。同时,数据存储在ByteBuffer中还能够比较容易地支持内存映射(mmap)和流式读写,进一步降低对内存的消耗。
  • 灵活
    FlatBuffers支持选择性地写入数据成员,这不仅为某一个数据结构在应用的不同版本之间提供了兼容性,同时还能使程序员灵活地选择是否写入某些字段及灵活地设计传输的数据结构。
  • 体积小,集成成本低
    项目中集成FlatBuffers只需要很少的额外代码。
  • 强类型约束
    如果数据结构不符合FlatBufferss文件的描述,那么会在编译期间就发现问题,而不会在运行时才暴露问题。
  • 使用方便
    可以兼容json等其他格式的解析。
  • 跨平台

FlatBuffers和Protocol Buffers是比较相似的,但是FlatBuffers不需要在读取成员变量之前必须将数据完全解析成对象,因为它所有信息的读取都是在对应的ByteBuffer中进行的,少了这些解析时必须为对象和成员变量分配的内存空间,就降低了解析过程中的内存消耗。json相对于FlatBuffers来说可读性更好,但是缺点也是明显的,那就是它的性能太低了,这点可以参见FlatBuffers的benchmarks。其他FlatBuffers的优势可以看white paper

FlatBuffers的基本使用

由于本文仅仅介绍在android应用中使用FlatBuffers的方法,因此基本使用方法也只针对java语言进行介绍,其他语言的使用介绍请参看官方介绍

下载FlatBuffers源码并编译

FlatBuffersS的源码包含flatc的源代码及支持的各种语言需要依赖的代码,目前托管在github上面(这里)。下载完成后首先需要将源码中的flatc源码编译成自己所用平台上的flatc工具,flatc源码支持使用visual studio和xcode进行编译,也支持使用cmake进行跨平台编译,在mac上使用cmake进行编译的方法可以参看这里。在编译得到flatc后,就可以先将源码目录下自己所用语言(这里是java/com/google/flatbuffers)目录引入到自己的工程目录下就可以进行下一步工作了。

编写schema文件

FlatBuffers需要一个用IDL语言描述的schema文件来定义传输数据的结构,IDL是一种类似于c语言的接口定义语言,它支持bool、short、float和double几种基本数据结构及数组、字符串、Struct和Table几种复杂类型。关于如何使用IDL来编写schema文件可以参看这里,此处就不做过多的描述。这里为了方面还是以官方demo的schema文件(monster.FlatBufferss)为例,相应的代码如下:

// Example IDL file for our monster's schema.
namespace MyGame.Sample;
enum Color:byte { Red = 0, Green, Blue = 2 }
union Equipment { Weapon } // Optionally add more tables.
struct Vec3 {
  x:float;
  y:float;
  z:float;
}
table Monster {
  pos:Vec3; // Struct.
  mana:short = 150;
  hp:short = 100;
  name:string;
  friendly:bool = false (deprecated);
  inventory:[ubyte];  // Vector of scalars.
  color:Color = Blue; // Enum.
  weapons:[Weapon];   // Vector of tables.
  equipped:Equipment; // Union.
}
table Weapon {
  name:string;
  damage:short;
}
root_type Monster;

编译schema文件

编写完schema文件后,就需要使用flatc将其转换成对应语言所对应的类,这里使用:

flatc --java samples/monster.FlatBufferss

将schema文件转换成了如下几个java文件:

03_flac编译生成的文件

这几个文件就是要读写这个schema文件对应的FlatBuffers数据结构所需要依赖的类。flatc会按照一套严格的标准来完成转换的工作,打开生成的这些文件可以看到,这几个类的实现有很多写死的常量,例如成员变量的索引和数组的大小等,这也就说明,一旦schema文件编写完成,也就相当于确定了相应数据的存储结构。flatc还支持很多参数来完成不同的工作,更多高级特性请参看这里

使用

将上一步使用flatc编译生成的java文件引入工程,这些文件就相当于是schema中定义的数据结构的java封装,我们可以很方便地通过这些类完成数据的序列化和反序列化工作。并且只要反序列化时使用的schema和序列化时使用的schema一致,那么一定可以完整地还原序列化时的数据,而和序列化和反序列化时使用的语言无关,这一点是通过FlatBuffers构建二进制数据的规则来保证的,稍后会具体分析这一点。引入编译生成的java类后,可以通过如下代码简单地进行demo测试。

    private static void testFlatBuffers() {
    
    /*************************** 序列化 ************************/
    
    /**
     *  FlatBufferBuilder是FlatBuffers进行序列化和反序列化的关键类,这个类内部定义了各种数据类型进行序列化和反序列化的规则,
     *  并且封装了一系列数据结构序列化和反序列化时需要进行的操作。
     */
    FlatBufferBuilder builder = new FlatBufferBuilder();    

    // 创建一个字符串名称,返回的是字符串在底层ByteBuffer中的偏移
    int monsterName = builder.createString("软泥麦塔");     

    // 通过生成的Monster类存储一个数组信息,其实最终数据还是通过FlatBuilder来完成存储的
    byte[] treasure = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };     
    int inv = Monster.createInventoryVector(builder, treasure);     

    /**
     * 根据Weapon类封装的方法创建武器信息,之所以Weapon类要封装这个操作是因为Weapon要根据
     * schema对自身的定义写死一些常量,例如Weapon的成员个数以及各个成员的索引。
     */
    int weapon1Name = builder.createString("axe");
    short axeDamage = 50;
    int axe = Weapon.createWeapon(builder, weapon1Name, axeDamage);

    int weapon2Name = builder.createString("锈刀");
    short swordDamage = 100;
    int sword = Weapon.createWeapon(builder, weapon2Name, swordDamage);
    
    // Pass the `weaps` array into the `createWeaponsVector()` method to create a FlatBuffer vector.
    int[] weaps = new int[2];
    weaps[0] = sword;
    weaps[1] = axe;
    int weapons = Monster.createWeaponsVector(builder, weaps);
    
    /**
     * 上面的那些类型是不能内联存储到Monster中的,因为它们属于schema中的复杂类型,需要先单独使用FlatBuilder在底层的
     * ByteBuffer中存储,然后将各个类型的偏移保存起来构建Monster信息。
     */
    Monster.startMonster(builder);      // 准备构建Monster信息
    Monster.addName(builder, monsterName);  // 使用之前得到的偏移构建怪兽名称
    /**
     * Vec3是Struct类型,Struct类型是用来保存固定不变的数据的,一旦规定变不可更改(不能增加也不能减少),
     * 并且FlatBuffers为了提高数据访问效率,Struct的数据访问没有适用相对寻址,而是适用了直接寻址,因此它的数据只能内联存储。
     */
    int postion = Vec3.createVec3(builder, 1, 2, 3);    // vector必须要内联存储
    Monster.addPos(builder, postion);
    Monster.addColor(builder, Color.Blue);  // 简单数据也进行内联存储
    Monster.addHp(builder, (short) 700);
    Monster.addMana(builder, (short) 10);
    Monster.addInventory(builder, inv);
    Monster.addWeapons(builder, weapons);
    Monster.addEquippedType(builder, Equipment.Weapon);
    Monster.addEquipped(builder, axe);
    int rootMonster = Monster.endMonster(builder); // monster 构造结束

    builder.finish(rootMonster); // 所有数据写入完毕,写入FlatBuffers root_table的位置

    ByteBuffer data = builder.dataBuffer();     //得到底层存储二进制数据的ByteBuffer
    
    // 写入数据到文件
    File file = new File("flatbuffersFile.txt");
    FileOutputStream out = null;
    FileChannel channel = null;
    try {
        out = new FileOutputStream(file);
        channel = out.getChannel();
        while (data.hasRemaining()) {
            try {
                channel.write(data);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        try {
            if (null != out) {
                out.close();
            }
            
            if (null != channel) {
                channel.close();
            }
        } catch (Exception e2) {
        }
        
    }
    
    /*************************** 反序列化 ************************/
    
    // 从文件中读取数据
    FileInputStream fis = null;
    FileChannel readChannel = null;
    try {
        fis = new FileInputStream(file);
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        readChannel = fis.getChannel();
        int readbytes = 0;
        while ((readbytes = readChannel.read(byteBuffer)) != -1)  {
            System.out.println("读到 " + readbytes + " 个数据");
        }
        byteBuffer.flip();  // position回绕,准备从ByteBuffer中读取数据
        
        Monster monster = Monster.getRootAsMonster(byteBuffer);     // 找到root_table的位置,即数据的入口
        
        // 测试读取的数据是否和写入的数据一致。
        System.out.println("monster name: " + monster.name() + " hp: " + monster.hp());
    } catch (Exception e) {
        e.printStackTrace();
        try {
            if (null != readChannel) {
                readChannel.close();
            }
            if (null != fis) {
                fis.close();
            }
        } catch (Exception e2) {
            e2.printStackTrace();
        }
    }
}

相关的操作都已经在注释中写明白了。总的来说,FlatBuffers组织数据的格式和java class文件的格式有点类似,一些复杂类型的成员都先按照约定的格式先写入底层的ByteBuffer,这类似于java class中的常量区,然后使用这些常量的偏移来构建Table,这就相当于java class文件中的“表”。这样的结构决定了一些复杂类型的成员都是使用相对寻址进行数据访问的,即先从Table中取到成员常量的偏移,然后根据这个偏移再去常量真正存储的地址去取真实数据。但是Strcut类型的数据算是一个例外,FlatBuffers规定Struct类型用于存储那些约定成俗、永不改变的数据,这种类型的数据结构一旦确定便永远不会改变,在这个规定之下,为了提高数据访问速度,FlatBuffers单独对Struct使用了直接寻址的方式,这也要求了其数据必须进行内联存储。

FlatBuffers数据存储结构

虽然通过使用flatc对idl文件进行编译后,会自动生成我们定义的数据结构的java类,并且在使用过程中我们也不用关心数据的存储细节。但是如果你想要了解为什么FlatBuffers会如此高效,那么首先就不得不清楚FlatBuffers中的各种不同类型的数据结构是如何存储的。
FlatBuffers底层使用了java的ByteBuffer进行数据存储,ByteBuffer可以算是java NIO体系中的重要成员,很多jvm单独为它从heap中划分了一块存储区域进行数据存储,这样就避免了java数据到native层的传输需要经过java heap到native heap的数据拷贝过程,从而提高了数据读写的效率。但是ByteBuffer是针对直接进行数据存取操作的,虽然它提供了诸如asIntBuffer等方法来构造包装类以便针对int等类型的数据进行读取,但是毕竟FlatBuffers存储的一般并不是单一的数据类型,因此如果让用户来直接操作底层的ByteBuffer的话还是非常麻烦的。幸运的是FlatBuffersBuilder已经为我们封装了很多操作。

FlatBuffers对ByteBuffer的基本使用原则

在后面详细介绍各种数据存储结构之前先说一下FlatBuffers是按照什么规则来使用ByteBuffer的,总的说来就是以下两点:

  • 小端模式
    FlatBuffers对各种基本数据的存储都是按照小端模式来进行的,因为这种模式目前和大部分处理器的存储模式是一致的,可以加快数据读写的数据。这一点FlatBuffers一般是在初始化ByteBuffer的时候调用ByteBuffer.order(ByteOrder.LITTLE_ENDIAN)来实现的。
  • 写入数据方向和读取数据方向不同
    和一般向ByteBuffer写入数据的习惯不同,FlatBuffers向ByteBuffer中写入数据的顺序是从ByteBuffer的尾部向头部填充,由于这种增长方向和ByteBuffer默认的增长方向不同,因此FlatBuffers在向ByteBuffer中写入数据的时候就不能依赖ByteBuffer的position来标记有效数据位置,而是自己维护了一个space变量来指明有效数据的位置,在分析FlatBuffersBuilder的时候要特别注意这个变量的增长特点。但是,和数据的写入方向不同的是,FlatBuffers从ByteBuffer中解析数据的时候又是按照ByteBuffer正常的顺序来进行的。FlatBuffers这样组织数据存储的好处是,在从左到右解析数据的时候,能够保证最先读取到的就是整个ByteBuffer的概要信息(例如Table类型的vtable字段),方便解析。如下图所示:

01_FlatBuffers中ByteBuffer数据增长方向

但是为什么FlatBuffers要费劲地在写的时候将数据做逆向增长?这个我也确实没有想到一个好的原因,我认为读写按照相同的顺序完全可以根据绝对地址来实现数据写入和定位读取,这个大家想到有什么好的原因可以来讨论一下。

FlatBuffers寻址特点

除了Struct类型和基本类型,FlatBuffers在ByteBuffer中存放的数据地址都是相对地址,也使用相对寻址的方式来在ByteBuffer中定位数据。存在这个特点的根本原因是因为FlatBuffers存放数据和解析数据的方向不一致造成的。因此在向ByteBuffer写入数据的时候先暂时使用offset来定义位置,offset实际上就是指定位置和ByteBuffer结尾(capacity)的距离,但是又因为之后解析数据的时候并不是从后向前解析的,因此在解析的时候不能依赖这个offset。那要通过何种手段来确定一个成员的位置呢?FlatBuffers采用的是相对位置。例如,一个复杂数据类型在Table数据字段中存储的是这个复杂数据真正存储位置和Table数据字段写入位置的相对位置,这样只要已知Table数据字段的写入位置,就能计算得到这个复杂数据的在ByteBuffer中的真正存储位置了。下面根据上面介绍过的实例并结合源码来以String类型在Table中的存储和写入来解释一下这个特点。
首先我们使用下面的代码来存储一个字符串数据到ByteBuffer中

  int monsterName = builder.createString("软泥麦塔");

下面我们具体分析一下这个函数到底做了什么工作,是如何将字符串存储起来的,列出FlatBuffersBuilder中几个相关函数:

/**
 * Encode the string `s` in the buffer using UTF-8.
 * 创建字符串,从左到右依次是字符串长度,字符串数据和结尾的0
 * @param s The string to encode.
 * @return The offset in the buffer where the encoded string starts.
 */
public int createString(String s) {
    byte[] utf8 = s.getBytes(utf8charset);
    addByte((byte) 0);                  // 以0结尾
    startVector(1, utf8.length, 1);     // 也是按照vector进行存储的,当做长度为utf8.length的vector
    bb.position(space -= utf8.length);  // 存储字符串,注意顺序
    bb.put(utf8, 0, utf8.length);       // 存储字符串
    return endVector();
}

/**
 * Add a `byte` to the buffer, properly aligned, and grows the buffer (if necessary).
 *
 * @param x A `byte` to put into the buffer.
 */
public void addByte(byte x) {
    prep(1, 0);
    putByte(x);
}

/**
 * Add a `byte` to the buffer, backwards from the current location. Doesn't align nor
 * check for space.
 *
 * @param x A `byte` to put into the buffer.
 */
public void putByte(byte x) {
    bb.put(space -= 1, x);
}

public void startVector(int elem_size, int num_elems, int alignment) {
    notNested();
    vector_num_elems = num_elems;
    prep(SIZEOF_INT, elem_size * num_elems);    // 初始化存储空间
    prep(alignment, elem_size * num_elems); // Just in case alignment > int.
    nested = true;
}

/**
 * Finish off the creation of an array and all its elements.  The array
 * must be created with {@link #startVector(int, int, int)}.<br>
 * 结束vector的创建,同时会将此vector的长度信息写入,同时返回当前vector相对于ByteBuffer的偏移
 * @return The offset at which the newly created array starts.<br>
 *     当前创建的vector相对于ByteBuffer结尾的偏移
 * @see #startVector(int, int, int)
 */
public int endVector() {
    if (!nested)
        throw new AssertionError("FlatBuffers: endVector called without startVector");
    nested = false;
    putInt(vector_num_elems);   // 保存vector的成员个数,不是存储空间长度
    return offset();    // 当前bytebuffer存放数据的长度
}

/**
 * Offset relative to the end of the buffer.<br>
 * 能够反映当前bytebuffer中存储数据的长度,也能表明当前对象相对于bytebuffer结尾的偏移
 * @return Offset relative to the end of the buffer.
 */
public int offset() {
    return bb.capacity() - space;
}

createString函数首先将字符串按照utf-8的方式进行了编码,并且在存储字符串数据之前先写了一个字节的0,以此作为字符串存储结尾的标志。大家注意下putByte这个函数的实现,可以发现每当FlatBuffers向ByteBuffer中写入数据的时候,都是先将space往ByteBuffer的头部移动指定长度,然后再写入数据,space的初始值为ByteBuffer.capacity,它维护了FlatBuffers向当前ByteBuffer写入位置信息,作用就类似于ByteBuffer原生的postion,是FlatBuffers逆向写入数据的产物。
FlatBuffers在实现字符串写入的时候将字符串的编码数组当做了一维的vector来实现,startVector函数是写入前的初始化,并且在写入编码数组之前我们又看到了先将space往前移动数组长度的距离,然后再写入,写入完成后调用endVector进行收尾,endVector再将vector的成员数量,在这里就是字符串数组的长度写入,然后调用offset返回写入的数据结构的起点。在进行下一步分析之前,可以根据上面的分析画出写入数据的结构,如下:

02_FlatBuffers中写入string的结构

之后就是将字符串写入Table,下面列出相关代码:

/**
 * Monster经过flatc编译后自动生成的代码
 */
public static void addName(FlatBufferBuilder builder, int nameOffset) {
    builder.addOffset(3, nameOffset, 0);
}

/**
 * Adds on offset, relative to where it will be written.
 * 计算并保存指定off偏移与当前table_data第i个成员数据字段之间的偏移,
 * 方便以后找到第i个成员的数据位置后能够根据这个值计算出真正第i个数据的存储位置
 * @param off The offset to add.<br>相对于整个ByteBuffer的偏移
 */
public void addOffset(int off) {
    prep(SIZEOF_INT, 0);  // Ensure alignment is already done.
    assert off <= offset();
    off = offset() - off + SIZEOF_INT; // 计算相对于当前写入位置的偏移
    putInt(off);
}

实际上Table的数据结构比较复杂,后面会单独分析,这里只讲string在Table中的存储的关键代码。当调用addName将字符串存储到ByteBuffer的时候,需要传入刚才调用builder.createString函数返回的字符串的offset,然后addOffset会计算出传入的offset相对于当前写入位置的偏移,并将这个偏移写入。这意味着什么?意味着只要定位到当前写入的这个位置,取出写入的int值,和当前位置相加就得到了存储string数据的真实地址。这也是后面取string数据的一个依据,下面就来分析。

/**
 * Monster经过flatc编译后自动生成的代码
 */
public String name() {
    int o = __offset(10);	  // 得到vtable[10]相对于vatble_data[10]的偏移
    return o != 0 ? __string(o + bb_pos) : null;
}


/**
 * Create a Java `String` from UTF-8 data stored inside the FlatBuffer.
 * <p/>
 * This allocates a new string and converts to wide chars upon each access,
 * which is not very efficient. Instead, each FlatBuffer string also comes with an
 * accessor based on __vector_as_bytebuffer below, which is much more efficient,
 * assuming your Java program can handle UTF-8 data directly.
 *
 * @param offset An `int` index into the Table's ByteBuffer.<br>
 * @return Returns a `String` from the data stored inside the FlatBuffer at `offset`.
 */
protected String __string(int offset) {
    offset += bb.getInt(offset);  // 当前位置和当前位置写入的相对偏移相加得到真实数据的位置
    if (bb.hasArray()) {    // ByteBuffer底层是否由byte数组支持
        return new String(bb.array(), bb.arrayOffset() + offset + SIZEOF_INT, bb.getInt(offset),
                FlatBufferBuilder.utf8charset);
    } else {
        // We can't access .array(), since the ByteBuffer is read-only,
        // off-heap or a memory map
        ByteBuffer bb = this.bb.duplicate().order(ByteOrder.LITTLE_ENDIAN);
        // We're forced to make an extra copy:
        byte[] copy = new byte[bb.getInt(offset)];  // string第一个int表示字符串数组的长度,因此bb.getint(offset)就是string有效数据的长度
        bb.position(offset + SIZEOF_INT); // 要多偏移一个整型长度 ,以便跳过记录字符串的长度的数据,这样才能拷贝到真正的有效数据
        bb.get(copy);
        return new String(copy, 0, copy.length, FlatBufferBuilder.utf8charset); // 根据二进制构造字符串
    }
}

这里先不讲name()函数如何通过vtable定位到table的数据字段,先认为__string() 函数传入的offset参数即是刚才写入string相对偏移的地址即可,从__string()函数的实现可以看到,首先是通过当前位置和写入的偏移计算出string数据存储的真正位置,然后根据string数据的存储格式取到string的真正数据,结束。
从上面分析流程可以看出,在FlatBuffers对ByteBuffer写入顺序和读取顺序不一致的情况,使用相对寻址都不用关心我们当前读取和写入的顺序这个细节。

FlatBuffers部分复杂数据结构存储分析

有了FlatBuffers数据存储结构的基础后,就可以紧接着分析FlatBuffers支持的几个复杂数据结构的存储了。

Struct类型

除了基本类型之外,FlatBuffers中只有Struct类型使用直接寻址进行数据访问。因此首先就来分析这种数据结构是如何进行存储的,还是结合之前的demo进行讲解。首先来看看Struct结构的java类实现:

/**
 * All structs in the generated code derive from this class, and add their own accessors.
 */
public class Struct {
  /** Used to hold the position of the `bb` buffer. */
  protected int bb_pos;
  /** The underlying ByteBuffer to hold the data of the Struct. */
  protected ByteBuffer bb;
} 非常简单,只有两个成员变量,*bb*就是FlatBuffers用来存储数据的ByteBuffer,*bb_post*用来指明这个Struct对应的真实数在ByteBuffer中的绝对位置。这两个成员只描述了Struct最基本需求,至于Struct有几个成员变量,以及如何从底层ByteBuffer中解析Struct中的各个对象则留给了Struct的子类来实现。例如,我们可以看看Vec3的实现:

public final class Vec3 extends Struct {
    public Vec3 __init(int _i, ByteBuffer _bb) {
        bb_pos = _i;
        bb = _bb;
        return this;
    }

    public float x() {
        return bb.getFloat(bb_pos + 0);
    }

    public float y() {
        return bb.getFloat(bb_pos + 4);
    }

    public float z() {
        return bb.getFloat(bb_pos + 8);
    }

    public static int createVec3(FlatBufferBuilder builder, float x, float y, float z) {
        builder.prep(4, 12);  // 准备,进行对齐及ByteBuffer长度不够时进行自动增长
        builder.putFloat(z);
        builder.putFloat(y);
        builder.putFloat(x);
        return builder.offset();
    }
};

从这个实现可以看到,子类自己维护了成员写入和解析的工作,例如自动根据索引写入x,y,z数据,以及根据索引从ByteBuffer中解析对应的成员变量等。因此,只要保证调用Vec3.__init方法的时候,i参数和调用Vec3.createVec3时返回的偏移相同,就一定可以成功从ByteBuffer中解析出数据。

Union类型

这个类型也比较特殊,FlatBuffers规定这个类型在使用上具有如下两个限制:

  1. Union类型的成员只能是Table类型。
  2. Union类型不能是一个schema文件的根。

实际上BF中没有任何类型来表示Union,在schema中指明类型为Union后经过flatc编译后会生成一个单独的类来对应声明的Union类型。例如demo中的Equipment就为Union类型,声明为:

union Equipment { Weapon }

经过编译后,生成的Equipment类如下:

public final class Equipment {
  private Equipment() { }
  public static final byte NONE = 0;
  public static final byte Weapon = 1;

  private static final String[] names = { "NONE", "Weapon", };

  public static String name(int e) { return names[e]; }
};	

Union类的实现只是保存了Union类能够存储和表示的类型的名称,为了能够实现类似于联合体的功能,在编译Union类型的时候会为使用这个Union类型的外部类型额外生成一个代表当前Union类型的type,这个type和生成的Union类中的某一个常量type对应。真是因为需要生成一个额外的type类型和Union对应,FlatBuffers才限制了Union类型不能作为schema的根。例如,在demo中的Monster类中有如下代码就是单独为其中的Equipment生成的:

public byte equippedType() {
    int o = __offset(20);
    return o != 0 ? bb.get(o + bb_pos) : 0;
}

public Table equipped(Table obj) {
    int o = __offset(22);
    return o != 0 ? __union(obj, o) : null;
}

public static void addEquippedType(FlatBufferBuilder builder, byte equippedType) {
    builder.addByte(8, equippedType, 0);
}

public static void addEquipped(FlatBufferBuilder builder, int equippedOffset) {
    builder.addOffset(9, equippedOffset, 0);
}

因此,在序列化Union的时候一般先写入Union的type,然后再写入Union的数据偏移;在反序列化Union的时候一般先解析出Union的type,然后再按照type对应的Table类型来解析Union对应的数据。

enum类型

FlatBuffers中的enum类型在数据存储的时候是和byte类型存储的方式一样的,因为和Union类型相似,enum类型在FlatBuffers中也没有单独的类与它对应,在schema中声明为enum的类会被编译生成单独的类。例如demo中的Color类被编译转换成了如下代码:

public final class Color {
  private Color() { }
  public static final byte Red = 0;
  public static final byte Green = 1;
  public static final byte Blue = 2;

  private static final String[] names = { "Red", "Green", "Blue", };

  public static String name(int e) { return names[e]; }
};

从类的实现上来看,Color类型只是简单地包括了枚举成员的声明和获取枚举成员名称的接口实现,在序列化和反序列化时,完全是将enum类型当做byte来处理的。例如Monster中对Color的处理如下:

public static void addColor(FlatBufferBuilder builder, byte color) {
    builder.addByte(6, color, 2);
}

public byte color() {
    int o = __offset(16);
    return o != 0 ? bb.get(o + bb_pos) : 2;  // 默认颜色为Blue,因此没有存储颜色的时候使用2
}

可以看到Monster类对Color的序列化和反序列化完全是按照byte来处理的。

Vector类型

Vector类型实际上就是schema中声明的数组类型,FlatBuffers中也没有单独的类型和它对应,但是它却有自己独立的一套存储结构,flatc编译生成的类负责按照这种结构来读写自己所用到的Vector类型的成员。Vector类型的存储结构如下:

03_flatbuffers中vector类型的存储结构

Vector在序列化数据时先会从高位到低位依次存储vector内部的数据,然后再在数据序列化完毕后写入Vector的成员个数。下面我们根据flatc生成的相关代码来看FlatBuffers是如何实现这个存储结构的:

/********** Monster.java **********/

public static void addWeapons(FlatBufferBuilder builder, int weaponsOffset) {
    builder.addOffset(7, weaponsOffset, 0);
}

public static int createWeaponsVector(FlatBufferBuilder builder, int[] data) {
    builder.startVector(4, data.length, 4);
    for (int i = data.length - 1; i >= 0; i--) {
        /** 
         * 由于weapon属于复杂类型,因此这里用addOffset记录成员变量,如果只是简单类型的vector,
         * flatc处理的时候会自动调用addInt等方法来记录简单数据,而不用进行相对寻址了。
         */
        builder.addOffset(data[i]);     
    }
    return builder.endVector();
}

public static void startWeaponsVector(FlatBufferBuilder builder, int numElems) {
    builder.startVector(4, numElems, 4);
}


public Weapon weapons(int j) {
    return weapons(new Weapon(), j);
}

public Weapon weapons(Weapon obj, int j) {
    int o = __offset(18);
    // 由于weapon vector成员记录的是各个weapon数据的存储地址相对成员数据写入位置的相对偏移,因此还要通过__indirect进行一次相对寻址
    return o != 0 ? obj.__init(__indirect(__vector(o) + j * 4), bb) : null;
}

public int weaponsLength() {
    int o = __offset(18);
    return o != 0 ? __vector_len(o) : 0;
}

/*********** Table.java ***********/

/**
 * Look up a field in the vtable.<br>
 * 得到vtable中记录的第vtable_index个成员的值(是相应成员在数据字段中的偏移)
 * @param vtable_index An `int` offset to the vtable in the Table's ByteBuffer.
 * @return Returns an offset into the object, or `0` if the field is not present.
 *          得到vtable_offset指定的table成员相对table data中的位置
 */
protected int __offset(int vtable_index) {
    int vtable = bb_pos - bb.getInt(bb_pos);    // bb_pos得到的是table的data字段相对于vtable的偏移,因此这种方法得到的是vatable的位置
    return vtable_index < bb.getShort(vtable) ? bb.getShort(vtable + vtable_index) : 0;   // 因为vtable_offset是相对于vtable的偏移,因此vtable_offset < bb.getShort(vtable)可以防止偏移越界
}

/**
 * Retrieve a relative offset.<br>
 * 进行一次相对寻址
 * @param offset An `int` index into the Table's ByteBuffer containing the relative offset.
 * @return Returns the relative offset stored at `offset`.
 */
protected int __indirect(int offset) {
    return offset + bb.getInt(offset);
}

/**
 * Get the length of a vector.
 *
 * @param offset An `int` index into the Table's ByteBuffer.
 * @return Returns the length of the vector whose offset is stored at `offset`.
 */
protected int __vector_len(int offset) {
    offset += bb_pos;  // 加上byteBuffer数据段在Table中的偏移。
    offset += bb.getInt(offset);  // ByteBuffer中数据段的偏移
    return bb.getInt(offset); // 得到数据段中表明有效数据长度的值
}

/**
 * Get the start data of a vector.<br>
 * 得到vector中有效数据相对于整个ByteBuffer的偏移
 *
 * @param offset An `int` index into the Table's ByteBuffer.
 * @return Returns the start of the vector data whose offset is stored at `offset`.
 */
protected int __vector(int offset) {
    offset += bb_pos;   // 得到Table中第offset个成员的地址
    return offset + bb.getInt(offset) + SIZEOF_INT;  // data starts after the length,相对寻址,得到vector数据的真正地址
}


/*************** FlatBuilder.java ***********/

public void startVector(int elem_size, int num_elems, int alignment) {
    notNested();
    vector_num_elems = num_elems;
    prep(SIZEOF_INT, elem_size * num_elems);    // 初始化存储空间
    prep(alignment, elem_size * num_elems); // Just in case alignment > int.
    nested = true;
}

/**
 * Finish off the creation of an array and all its elements.  The array
 * must be created with {@link #startVector(int, int, int)}.<br>
 * 结束vector的创建,同时会将此vector的长度信息写入,同时返回当前vector相对于ByteBuffer的偏移
 * @return The offset at which the newly created array starts.<br>
 *     当前创建的vector相对于ByteBuffer结尾的偏移
 * @see #startVector(int, int, int)
 */
public int endVector() {
    if (!nested)
        throw new AssertionError("FlatBuffers: endVector called without startVector");
    nested = false;
    putInt(vector_num_elems);   // 保存vector的成员个数,不是存储空间长度,除非vector是以byte为单位保存
    return offset();    // 当前bytebuffer存放数据的长度
}

这里进行简单分析。首先从序列化数据开始,首先调用startVector进行对齐等初始化工作,然后依次写入Vector的成员变量。注意由于Vector的成员是复杂的Table类型,因此flatc在处理的时候自动使用了addOffset的方法来写入成员的相对偏移,这意味着后面要反序列化数据的时候需要取出这个偏移再进行一次相对寻址才能访问到复杂类型成员的真正数据;但是如果Vector的成员是简单类型,例如byte或者int时,flatc会自动调用addByte或者addInt等函数来直接存储成员数据,这样在反序列化时可以直接取出存储的数据就能代表成员变量的值。写入成员数据后再调用endVector将Vector的成员个数写入,就完成了序列化的工作。
在反序列化的时候,先通过__offset得到Vector相对于外部Table数据字段的偏移,然后调用 __vector函数得到这个Vector真正存储数据的位置,但是刚才已经说明,由于Vector中存储的只是Weapon的相对地址,因此绝对偏移地址:__vector(o) + j x 4 写入的内容就是第j个Vector变量相对于写入地址的偏移,因此还要通过调用一次__indirect方法进行相对寻址才能得到Vector第j个成员Weapon的偏移地址。

Table类型

Table类型是FlatBuffers中的核心类型,也是其中存储最为复杂的类型,首先我们来看Table类型的存储结构,如下图:

04_flatbuffers中Table存储结构

单就结构来讲就看出这种类型的复杂性了,不过有了之前讲解的几种结构的基础,一点点剖析起来也不难。
首先可以将Table分为两个部分,第一部分是存储Table中各个成员变量的概要,这里命名为vtable,第二部分是Table的数据部分,存储Table中各个成员的值,这里命名为table_data。注意Table中的成员如果是简单类型或者Struct类型,那么这个成员的具体数值就直接存储在table_data中;如果成员是复杂类型,那么table_data中存储的只是这个成员数据相对于写入地址的偏移,也就是说要获得这个成员的真正数据还要取出table_data中的数据进行一次相对寻址,这个特点在上面已经强调过了,分析FlatBuffers的时候一定要牢记这个规则。
下面就开始结合demo和源码来分析Table的存储及序列化和反序列化的操作过程。首先看一下demo中调用startMonster开始进行序列化的时候做了什么处理,相关代码如下:

/********** Monster.java *************/

public static void startMonster(FlatBufferBuilder builder) {
    builder.startObject(10);
}


/********* FlatBuilders.java ********/

public void startObject(int numfields) {
    notNested();
    if (vtable == null || vtable.length < numfields)
        vtable = new int[numfields];
    vtable_in_use = numfields;
    Arrays.fill(vtable, 0, vtable_in_use, 0);
    nested = true;
    object_start = offset();
}

做的处理主要是在内存中为当前Table分配一个vtable并初始化,vtable的长度和Table的成员变量数相同,实际上vtable中的每一项记录的就是Table中对应的成员在table_data中的偏移,这一点我们稍后会分析到。还要注意的是,这里仅仅是在内存中分配了一个vtable数组,用于暂时记录Table数据,而没有直接将这些数据写入到底层的ByteBuffer。同时,使用object_start来记录了Table数据开始的偏移。
在调用了startMonster方法后就可以开始一步步添加Monster中的各个成员,Monster为添加各种成员封装了很多add方法,但是这些add方法对应到FlatBuffers的底层无非就两类:

  • 基本类型
    基本类型的add方法包括addInt、addDouble、addBoolean等等,它们只是在ByteBuffer中说占用的存储长度有所不同,本质上都是将成员的数据直接进行存储。
  • 偏移类型
    对应为addOffset方法,这个方法接受一个相对于底层ByteBuffer的绝对偏移,然后将这个偏移根据当前写入位置相对于底层ByteBuffer的绝对偏移计算得到相对偏移,然后再将这个相对偏移写入到底层ByteBuffer中。偏移类型在ByteBuffer中占4个字节,而且在底层的存储上和addInt没有任何差别,但是毕竟addOffset和addInt所表示的意思完全不同,因此FlatBuffers就通过flatc对schema的编译来保证生成的类一定遵循“复杂类型的偏移用addOffset,简单类型使用addInt”这种规则,并且凡是使用addOffset进行序列化存储的数据在反序列化时一定会进行一次相对寻址

这里可以从demo中择抄几段代码来进一步验证上面的分析。

/********* Monster.java ********/
public static void addMana(FlatBufferBuilder builder, short mana) {
    builder.addShort(1, mana, 150);
}

public short mana() {
    int o = __offset(6);
    return o != 0 ? bb.getShort(o + bb_pos) : 150;
}

public static void addName(FlatBufferBuilder builder, int nameOffset) {
    builder.addOffset(3, nameOffset, 0);
}

public String name() {
    int o = __offset(10);
    return o != 0 ? __string(o + bb_pos) : null;
}

/******** FlatBuilder.java *********/

/**
 * Add a `short` to the buffer, properly aligned, and grows the buffer (if necessary).
 *
 * @param x A `short` to put into the buffer.
 */
public void addShort(short x) {
    prep(2, 0);
    putShort(x);
}

/**
 * Add a `short` to the buffer, backwards from the current location. Doesn't align nor
 * check for space.
 *
 * @param x A `short` to put into the buffer.
 */
public void putShort(short x) {
    bb.putShort(space -= 2, x);
}

/**
 * Adds on offset, relative to where it will be written.<br>
 * 写入相对偏移信息,相对的是当前写入位置的偏移。
 * @param off The offset to add.<br>相对于整个ByteBuffer的偏移
 */
public void addOffset(int off) {
    prep(SIZEOF_INT, 0);  // Ensure alignment is already done.
    assert off <= offset();
    off = offset() - off + SIZEOF_INT;  // 相对寻址,得到的是相对于当前写入位置的偏移
    putInt(off);
}

/**
 * Add an `int` to the buffer, backwards from the current location. Doesn't align nor
 * check for space.
 *
 * @param x An `int` to put into the buffer.
 */
public void putInt(int x) {
    bb.putInt(space -= 4, x);
}

protected String __string(int offset) {
    offset += bb.getInt(offset);  // 相对寻址
    if (bb.hasArray()) {    // ByteBuffer底层是否由byte数组支持
        return new String(bb.array(), bb.arrayOffset() + offset + SIZEOF_INT, bb.getInt(offset),
                FlatBufferBuilder.utf8charset);
    } else {
        // We can't access .array(), since the ByteBuffer is read-only,
        // off-heap or a memory map
        ByteBuffer bb = this.bb.duplicate().order(ByteOrder.LITTLE_ENDIAN);
        // We're forced to make an extra copy:
        byte[] copy = new byte[bb.getInt(offset)];  // offset加上了偏移,因此bb.getint(offset)就是bytebuffer有效数据的长度
        bb.position(offset + SIZEOF_INT); // 要多加一个代表有效数据长度的偏移,这样才能拷贝到真正的有效数据
        bb.get(copy);
        return new String(copy, 0, copy.length, FlatBufferBuilder.utf8charset);
    }
}

从以上代码可以看到,FlatBuilder虽然为不同类型的存储封装了不同的方法,但是在底层序列化存储数据的时候不区分要存储的数据是那一类类型,不同数据类型在序列化和反序列化时的正确对应是由flatc生成的类来保证的。
当所有的数据存储完毕后,就可以调用Monster.endMonster方法来标志Monster数据存储的结束,这个函数一系列的操作很关键,下面来分析:

/******** Monster.java ********/

public static int endMonster(FlatBufferBuilder builder) {
    int o = builder.endObject();
    return o;
}

/**
 * Finish off writing the object that is under construction.
 *
 * @return The offset to the object inside {@link #dataBuffer()}.
 * @see #startObject(int)
 */
public int endObject() {
    if (vtable == null || !nested)
        throw new AssertionError("FlatBuffers: endObject called without startObject");

    addInt(0);  // 中间间隔4个字节 用来记录vatable相对于table_data的结尾位置的偏移   ---> tag 2

    //----------- 记录vatable信息到bytebuffer START ------------

    int vtableloc = offset();
    // Write out the current vtable.
    for (int i = vtable_in_use - 1; i >= 0; i--) {  // 从后向前依次记录成员vtable的成员偏移到bytebuffer中
        // Offset relative to the start of the table.
        /**
         * 注意这里的相对偏移的计算:
         * vtableloc = capacity - vtablePos;   // table_data相对于bytebuffer结尾的偏移
         * vtable[i] = capacity -  tableDataPos_i;   // table数据段中第i个字段相对于bytebuffer结尾的偏移
         * 那么第i个字段相对于table_data的偏移计算为:
         * offset = vtableloc - vtable[i] = tableDataPos_i - vtablePos;
         *  那么在之后知道table_data相对于bytebuffer偏移的情况下,要得到这个成员记录的偏移就直接可以用:
         *
         * vtablePos + offset = tableDataPos_i;
         *
         * 参见 :{@link Table#__offset(int)}
         */

        short off = (short) (vtable[i] != 0 ? vtableloc - vtable[i] : 0);   // 得到成员数据在table_data中的写入位置相对于table_data的偏移

        addShort(off);
    }

    final int standard_fields = 2; // The fields below: 因为后面还要写两个字段,因此这里是2
    addShort((short) (vtableloc - object_start));   // 再写入整个 table_data的长度
    addShort((short) ((vtable_in_use + standard_fields) * SIZEOF_SHORT));   // vtable 的长度   ---> tag 1

    //----------- 记录vatable信息到bytebuffer END   ------------

    // Search for an existing vtable that matches the current one.
    int existing_vtable = 0;
    outer_loop:
    for (int i = 0; i < num_vtables; i++) {
        int vt1 = bb.capacity() - vtables[i];       // vtables记录的是每个vtable的偏移
        int vt2 = space;    // 因为刚才才存储了一个vtable数据到bytebuffer,因此这里space就指向当前的vatable
        short len = bb.getShort(vt1);   // vtable的第一个short字段记录的就是该vatable的长度(见上面注释的tag 1)
        if (len == bb.getShort(vt2)) {
            for (int j = SIZEOF_SHORT; j < len; j += SIZEOF_SHORT) {
                if (bb.getShort(vt1 + j) != bb.getShort(vt2 + j)) {
                    continue outer_loop;
                }
            }
            existing_vtable = vtables[i];
            break outer_loop;
        }
    }

    if (existing_vtable != 0) {     // 存在和当前vtable一样的vatable
        // Found a match:
        // Remove the current vtable.
        space = bb.capacity() - vtableloc;  // 移动space到table_data的结尾
        // Point table to existing vtable.
        bb.putInt(space, existing_vtable - vtableloc);  // 记录当前table_data对应的vtable的位置(见 tag 2注释)
    } else {
        // No match:
        // Add the location of the current vtable to the list of vtables.
        if (num_vtables == vtables.length)
            vtables = Arrays.copyOf(vtables, num_vtables * 2);  // 扩容
        vtables[num_vtables++] = offset();  // 记录当前vtable
        // Point table to current vtable.
        bb.putInt(bb.capacity() - vtableloc, offset() - vtableloc); // 记录vatable相对于table的偏移位置
    }

    nested = false;
    return vtableloc;  // 注意返回的是table_data的偏移,不是vtable的偏移
}

代码中的一堆注释已经解释了操作的各个步骤完成的任务,总的说来,就是完成了table_data的构建,同时将内存中的vtable写入到了底层的ByteBuffer中。但是有一点需要注意的是,一般来说vtable和table_data就和上面图中画出的一样,是相邻的,但是从代码中来看,如果在调用endObject的时候ByteBuffer中已经存在一个和当前table_data需要的vtable一样的vtable存在时,当前的table_data是会直接复用ByteBuffer中的这个vtable,因此就可能出现二者不相邻的情况,这也说明了:一个vtable在ByteBuffer中可以对应多个table_data,因为vtable只记录Table结构相关的概要信息,和Table具体存储的数据无关 。这也是FlatBuffers将Table数据拆成两部分的一个原因吧,因为如果ByteBuffer中有多个类型相同的Table数据时,这样可以节省存储空间。

05_vtable复用

调用endObject()后就表示一个Table数据写入完毕,但是如果这个Table是整个schema的root_table,那么还需要调用FlatBuilder.finish()方法来在底层的ByteBuffer的开头写入这个Table的偏移,这样就能够在反序列化的时候通过读取ByteBuffer的前4个字节就能够确定root_table的位置并顺次解析数据。调用完FlatBuilder.finish()方法后就不能再往ByteBuffer中添加任何数据。

/**
 * Finalize a buffer, pointing to the given `root_table`.
 *
 * @param root_table An offset to be added to the buffer.
 */
public void finish(int root_table) {
    prep(minalign, SIZEOF_INT);
    addOffset(root_table);  // 写入root_table的table_data的偏移
    bb.position(space);     // 修改bytebuffer的指针位置,以指明ByteBuffer中的有效数据的开端
    finished = true;
}

在解析Table数据的时候,FlatBuffers都是通过偏移先找到table_data的位置,然后再根据table_data开始的4个直接找到vtable,然后根据vtalbe来辅助table_data中的数据解析。例如:

public static Monster getRootAsMonster(ByteBuffer _bb) {
    return getRootAsMonster(_bb, new Monster());
}

public static Monster getRootAsMonster(ByteBuffer _bb, Monster obj) {
    _bb.order(ByteOrder.LITTLE_ENDIAN);
    return (obj.__init(_bb.getInt(_bb.position()) + _bb.position(), _bb));  // _bb.getInt(_bb.position()) + _bb.position()得到的就是table_data在bytebuffer中的position
}

public Monster __init(int _i, ByteBuffer _bb) {
    bb_pos = _i;
    bb = _bb;
    return this;
} 请注意,上面的代码中*_bb.getInt(_bb.position())+_bb.position()*得到的就是table_data的偏移,因为*FlatBuilder.endObject()*返回的是table_data的偏移,*FlatBuilder.finish()*又将这个偏移计算成了相对偏移并记录。因此Table中的bb_pos就是这个Table中的table_data字段在ByteBuffer中的偏移地址。 到这里基本上就将FlatBuffers中的Table讲解完毕了,只剩下最后一个小的细节,那就是:*对于简单数据,如果指定写入的数据和默认值相同,FlatBuffers是不会将此数据写入到底层ByteBuffer中的*,这又是FlatBuffers节省数据长度的一个优化。大家可以从下面的代码看到这个特点:

/******* Monster.java *********/

public static void addHp(FlatBufferBuilder builder, short hp) {
    builder.addShort(2, hp, 100);
}

public short hp() {
    int o = __offset(8);
    return o != 0 ? bb.getShort(o + bb_pos) : 100;
}

/******** FlatBuilder.java ********/
/**
 * Add a `short` to a table at `o` into its vtable, with value `x` and default `d`.
 *
 * @param o The index into the vtable.
 * @param x A `short` to put into the buffer, depending on how defaults are handled. If
 *          `force_defaults` is `false`, compare `x` against the default value `d`. If `x` contains the
 *          default value, it can be skipped.
 * @param d A `short` default value to compare against when `force_defaults` is `false`.
 */
public void addShort(int o, short x, int d) {
    if (force_defaults || x != d) {
        addShort(x);
        slot(o);
    }
}

/**
 * Set the current vtable at `voffset` to the current location in the buffer.<br>
 * 使vtable的第vtableIndex记录当前bytebuffer的偏移。一般用于vtable记录table中字段的位置
 *
 * @param vtableIndex The index into the vtable to store the offset relative to the end of the
 *                buffer.<br>
 *                table中成员的索引
 */
public void slot(int vtableIndex) {
    vtable[vtableIndex] = offset();
}

以上代码FlatBuilder.addShort()方法在force_defaults为false的情况下,如果写入的值和当前值相同,那么并不会将数据写入到table_data中,相应的vtable[i]就为0,并且后面通过FlatBuilder.endObject()写入到ByteBuffer中的vtable的第i个成员也为0。此后,调用Monster.hp()的时候__offset()函数返回的值就是0,在这种情况下,FlatBuffers不会再去table_data中去寻找成员的值或者偏移,而是直接返回了schema中规定的默认值。同理,复杂类型数据也有和简单数据类型类似的处理。

结束

本文结合demo和源码对FlatBuffers进行了剖析,在解释原理的时候为了方便省略了其中的一些细节,对于省略的内容并不是不重要,比如说其中的对齐操作和相对偏移的计算都是至关重要的,大家可以参考本文大概把握FlatBuffers的原理,然后对其中的一些细节使用自己的方法来理解。本文的内容是我个人总结,如有偏颇,烦请指正!

参考

FlatBuffer Overview

29 Feb 2016

java enum brief

一、Java enum简介

注意enum不是Enum,有Java基础的同学们应该都不会把二者混淆了。简单来说,enum只是jdk1.5引入的语法糖,它不是java中的新增类型,编译器在编译阶段会自动将它转换成一个继承于Enum的子类,例如如下的代码:

public enum GenderEnum {
	MALE,
	FEMALE
} 编译成class文件后,通过javap GenderEnum.class得到的简单的编译后的结构为:

public final class com.yuanxz.example.GenderEnum extends java.lang.Enum<com.yuanxz.example.GenderEnum> {
  		public static final com.yuanxz.example.GenderEnum MALE;
  		public static final com.yuanxz.example.GenderEnum FEMALE;
  		static {};
  		public static com.yuanxz.example.GenderEnum[] values();
  		public static com.yuanxz.example.GenderEnum valueOf(java.lang.String);
} 从这里可以看到编译器已经将我们的GenderEnum处理成了Enum的子类了。

二、Enum和enum

2.1 Enum基本结构

既然是Enum的子类,那么我们就先来了解一下这个Enum类。这个类的基本结构还是很简单的,如下:

Enum基本结构

Enum类的成员变量只有name和ordinal,实现了Serilizable和Comparable接口。看到这个结构的时候很多人可能会有疑惑,如果只是简单继承Enum,怎么能够实现enum的功能的呢?确实,我们之前用javap GenderEnum.class得到的结构只是一个很简单的结构,实际上编译器为我们做了更多的工作,下面是使用javap -verbose GenderEnum.class得到的字节码内容:

public final class com.yuanxz.example.GenderEnum extends java.lang.Enum<com.yuanxz.example.GenderEnum>
  SourceFile: "GenderEnum.java"
  Signature: #45                          // Ljava/lang/Enum<Lcom/yuanxz/example/GenderEnum;>;
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM
Constant pool:

   ....							// 省略了常量池中的内容,不影响这里分析

{
  public static final com.yuanxz.example.GenderEnum MALE;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static final com.yuanxz.example.GenderEnum FEMALEE;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  static {};
    flags: ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: new           #1                  // class com/yuanxz/example/GenderEnum
         3: dup           
         4: ldc           #13                 // String MALE
         6: iconst_0      					  // ordinal参数的值为0
         7: invokespecial #14                 // Method "<init>":(Ljava/lang/String;I)	构造GenderEnum对象
        10: putstatic     #18                 // Field MALE:Lcom/yuanxz/example/GenderEnum;  赋值给MALE变量
        13: new           #1                  // class com/yuanxz/example/GenderEnum
        16: dup           
        17: ldc           #20                 // String FEMALEE
        19: iconst_1      					  // ordinal参数的值为1
        20: invokespecial #14                 // Method "<init>":(Ljava/lang/String;I) 	构造GenderEnum对象
        23: putstatic     #21                 // Field FEMALEE:Lcom/yuanxz/example/GenderEnum;  赋值给FEMALEE变量
        26: iconst_2      					  // 数组的长度为2
        27: anewarray     #1                  // class com/yuanxz/example/GenderEnum
        30: dup           
        31: iconst_0      
        32: getstatic     #18                 // Field MALE:Lcom/yuanxz/example/GenderEnum;
        35: aastore       
        36: dup           
        37: iconst_1      
        38: getstatic     #21                 // Field FEMALEE:Lcom/yuanxz/example/GenderEnum;
        41: aastore       
        42: putstatic     #23                 // Field ENUM$VALUES:[Lcom/yuanxz/example/GenderEnum;	// 将MALE和FEMALE赋值给VALUES数组
        45: return        
      LineNumberTable:
        line 12: 0
        line 13: 13
        line 11: 26
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

  public static com.yuanxz.example.GenderEnum[] values();	// 拷贝返回VALUES数组
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=5, locals=3, args_size=0
         0: getstatic     #23                 // Field ENUM$VALUES:[Lcom/yuanxz/example/GenderEnum;
         3: dup           
         4: astore_0      
         5: iconst_0      
         6: aload_0       
         7: arraylength   
         8: dup           
         9: istore_1      
        10: anewarray     #1                  // class com/yuanxz/example/GenderEnum
        13: dup           
        14: astore_2      
        15: iconst_0      
        16: iload_1       
        17: invokestatic  #31                 // Method java/lang/System.arraycopy:(Ljava/lang/Object;ILjava/lang/Object;II)V
        20: aload_2       
        21: areturn       
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature

  public static com.yuanxz.example.GenderEnum valueOf(java.lang.String);		//调用Enum的静态方法valueOf,第一个参数传递的是当前类对象
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc           #1                  // class com/yuanxz/example/GenderEnum
         2: aload_0       
         3: invokestatic  #39                 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         6: checkcast     #1                  // class com/yuanxz/example/GenderEnum
         9: areturn       
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
}yuanxz 如果要翻译成java代码的话基本如下:

public final class com.yuanxz.example.GenderEnum extends java.lang.Enum<com.yuanxz.example.GenderEnum> {


	public static final com.yuanxz.example.GenderEnum MALE;
	public static final com.yuanxz.example.GenderEnum FEMALE;

	private static final com.yuanxz.example.GenderEnum[] VALUES;

	static {

		MALE = new com.yuanxz.example.GenderEnum("MALE", 0);
		FEMALE = new com.yuanxz.example.GenderEnum("FEMALE", 1);

		VALUES = new GenderEnum[2];
		VALUES[0] = MALE;
		VALUES[1] = FEMALE;
	}

	public static com.yuanxz.example.GenderEnum[] values() {
		com.yuanxz.example.GenderEnum[] values = new com.yuanxz.example.GenderEnum[VALUES.length];
		System.arraycopy(VALUES, 0, values, 0, VALUES.length);
		return values;
	}


	public static com.yuanxz.example.GenderEnum valueOf(java.lang.String name) {
		return Enum.valueOf(com.yuanxz.example.GenderEnum, name);
	}

}

从生成的字节码中可以看到,编译器为我们生成的类的static模块实际上为我们完成了构造MALE和FEMALE变量并且赋值给VALUES数组的工作,因此当GenderEnum被classloader加载进来的时候就完成了这些工作,之后我们就可以通过GenderEnum.MALE这种方式来直接使用枚举了。

2.2 enum的其他特性

不要仅仅只将enum当做一个枚举来使用,除了枚举的特性之外它还能做一些其他工作,包括可以使用成员变量和声明自己的方法等,例如:

public enum EnumInstance {

    INSTANCE;
    
    private EnumInstance() {
        mInstaceVar = INIT_VAR_VALUE;   // 给变量赋初值
    }
    
    private EnumInstance(int var) {
        mInstaceVar = var;   // 给变量赋初值
    }
    
    private static final int INIT_VAR_VALUE = 10;
    
    private int mInstaceVar = 0;
    
    public void printSelf() {
        System.out.println("当前enum是: " + this);
    }
    
    public void addVar(int increment) {
        mInstaceVar += increment;
    }
    
    public int getVar() {
        return mInstaceVar;
    }


} 声明以上的枚举后,我们可以通过INSTANCE来调用它内部声明的addVar和getVar等方法,所有的这些都和普通类没有什么区别,除了它的构造函数。   enum不允许声明public的构造函数,目的是防止程序中随意地实例化枚举对象,但是它可是声明private的构造函数,比如上面那段代码中构造函数:

private EnumInstance(int var) {
        mInstaceVar = var;   // 给变量赋初值
} 我们可以通过如下的实现方式来指明enum调用这个构造函数:

public enum EnumInstance {

    INSTANCE(100);	// 指明调用private EnumInstance(int var)
    
    private EnumInstance() {
        mInstaceVar = INIT_VAR_VALUE;   // 给变量赋初值
    }
    
    private EnumInstance(int var) {
        mInstaceVar = var;   // 给变量赋初值
    }
    
    ...	// 省略其他代码


} 需要注意的是,由于enum本身只是一个语法糖,因此声明的构造函数相关的代码实际上经过编译器的处理会变成Enum构造函数中的一部分,例如EnumInstance经过编译器处理后代码就基本如下:

public final class com.yuanxz.example.EnumInstance extends java.lang.Enum<com.yuanxz.example.EnumInstance> {
	  public static final com.yuanxz.example.EnumInstance INSTANCE;
	  static {
	  		INSTANCE = new com.yuanxz.example.GenderEnum("MALE", 0);
			VALUES = new EnumInstance[1];
			VALUES[0] = INSTANCE;
	  };
	
	  private EnumInstance(String name, int ordinal) {  // Enum的构造函数
	  		super(name, ordinal);	// 先调用Enum的构造函数
	  		// 以下是我们声明的构造函数中的内容
	  		mInstaceVar = 100;
	  }
	
	  // 下面的函数省略描述
	  public void printSelf();
	  public void addVar(int);
	  public int getVar();
	  public static com.yuanxz.example.EnumInstance[] values();
	  public static com.yuanxz.example.EnumInstance valueOf(java.lang.String);
}

除此之外,由于编译器需要将enum最终翻译成Enum的子类,而Java中又不允许多继承,因此enum只支持实现接口,而不支持声明继承。

三、Java中的枚举单例

枚举类的基本使用这里就不再多说了,但有必要在这里简单介绍一下枚举单例。单例模式要求在同一个进程当中,类对象始终只有一份实例,单例模式有很多种实现方案,例如如下的两种:

/**
 * 方法一
 */
public final class SingleInstance {

	private static final SingleInstance sInstance = new SingleInstance();

	private SingleInstance() {

	}

	public static SingleInstance getInstance() {
		return sInstance;
	}

}

/**
 * 方法二
 */
public final class LazySingleInstance {

	private static SingleInstance sInstance;

	private SingleInstance() {

	}

	public synchronized static SingleInstance getInstance() {
		if (null == sInstance) {
			sInstance = new SingleInstance();
		}
		return sInstance;
	}

	/**
	// 或者使用double check方法
	public static SingleInstance getInstance() {
		if (null == sInstance) {
			synchronized(SingleInstance.class) {
				if (null == sInstance) {
					sInstance = new SingleInstance();
				}
			}
		}
		return sInstance;
	}
	*/

}

上面的两种方法基本上已经能够满足平时的使用场景的需求,即保证通过getInstance方法在同一个进程中只能获取到一个SingleInstance的实例,同时还使用了classloader加载Class时的线程安全性和synchronized来保证多线程竞争环境下也只能获取到一个单例。相比较于上面大段代码的实现,如果使用enum来实现单例只需要一句:

public enum SingleInstance {

	INSTANCE;

}

只需要这样就能保证在我们代码中使用SingleInstance.INSTANCE在同一进程下获取到的一定是同一个单例对象。是不是很方便很神奇!但是还不仅仅如此,enum还为单例的实现提供了更多的的天然特性,下面就来简单总结一下:

3.1 多线程环境下的安全性

这个特性上面已经介绍了,因为INSTANCE最后会被编译器处理成static final的,并且在static模块中进行的初始化,因此它的实例化是在class被加载阶段完成,是线程安全的。这个特性也决定了枚举单例不是lazy的,如果你的单例初始化比较费时且大多数情况下只会被引用但是不会被真正调用的话,你需要使用lazy的单例模式(上面传统单例的方法二实现),而不要选择枚举单例。

3.2 不可被反射实例化

在Java中,不仅通过限制enum只能声明private的构造方法来防止Enum被使用new进行实例化,而且还限制了使用反射的方法不能通过Constructor来newInstance一个枚举实例。在你尝试使用反射得到的Constructor来调用其newInstance方法来实例化enum时,回得到一个exception:

java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:521)

这一点,使用传统的单例模式是不好来保证的。注意,虽然Enum不能被反射实例化,但是它的其他方法是可以被反射调用的,例如Enum的valueof方法实际上就是通过Class类的getEnumConstantsShared方法反射调用Enum类的values方法来实现的。

3.3 序列化的唯一性

传统的单例模式要防止序列化/反序列化的攻击必须要手动来实现readObject或者readResolve方法,这一点Enum已经我们保证了,官方对于Enum序列化处理的表述如下:

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream. The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

这说明了在序列化和反序列化的时候Enum的处理是和其他类不同的,在序列化的时候实际上只写入了Enum的name成员,而没有保存ordinal成员;在反序列化的时候从ObjectInputStream中读取Enum的name成员,同时调用Enum的valueof方法传入读取的name得到对应的ordinal值。同时Enum是不支持自定义序列化和反序列化的,一些序列化和反序列化对应的函数例如readObject和writeObject及serialVersionUID等属性在Enum的序列化过程中都是被忽略的。
Enum在序列化和反序列化上的特性保证了使用Enum来实现单例是经受得起序列化的攻击的。

四、enum的其他使用方法

4.1 使用enum来设计模板类

由于enum既有枚举的特性,又包含基本类的特性,因此结合他的特点能够巧妙地设计一些代码。例如,如果你的代码中存在这样的结构:

if ("soldier".equals(toy)) {
	System.out.println("I'm a soldier."); 
} else if ("doll".equals(toy)) {
	System.out.println("I'm a doll."); 
} else {
	...
}

那么不妨考虑使用enum来实现:

public enum Toy {

	SOLDIER {

		@Override 
		public void execute() { 
        	System.out.println("I'm a doll."); 
        }
	},
	DOLL {
		
		@Override 
		public void execute() { 
        	System.out.println("I'm a doll."); 
        }
	};
	
	// template method
	public abstract void execute();

}

这样,直接使用toy.execute就能优雅地完成上面的功能,这是使用enum实现模板方法的典型方法。

4.2 使用value值反向查找enum对象

在Enum中提供了使用静态方法valuesof来根据name查询enum对象的方法,如果在程序中还有根据value来反向查找enum的需求,可以参考下面的代码来实现:

public enum Status {
     WAITING(0),
     READY(1),
     SKIPPED(-1),
     COMPLETED(5);

     private static final Map<Integer,Status> lookup 
          = new HashMap<Integer,Status>();

     static {
          for(Status s : EnumSet.allOf(Status.class))
               lookup.put(s.getCode(), s);
     }

     private int code;

     private Status(int code) {
          this.code = code;
     }

     public int getCode() { return code; }

     public static Status get(int code) { 
          return lookup.get(code); 
     }
} 这样通过调用get方法传入value值就可以反向查找对应的Status枚举对象。注意这里使用了EnumSet,它相对于普通的HashSet,针对enum的执行效率更高,类似结构的还有EnumMap,在这里就不逐一介绍了。

五、参考:

Making the Most of Java 5.0: Enum Tricks
What are enums and why are they useful?
Java中Enum类型的序列化

12 Jan 2016

CFR Java Decompiler

java反编译,JAD & CFR

目前我们开发中大都使用JAD进行java反编译。这个工具已经过于陈旧,最突出的问题就是经常反编译出错。

使用CFR反编译工具能够很好的解决这个问题,并且支持java8,这个工具更活跃。

CFR

附件是Java反编译工具CFR,支持java7,java8的反编译,能解决jd-gui部分代码不能反编译的问题,尤其是匿名类,内部类的一些逻辑。使用方法如下:

D:\>java -jar cfr_0_110.jar D:\example.jar –outputdir D:\data\example

输入可以是jar,class文件,也可以是在classpath里的类名,其他的高级选项可以参考—help,或者官网说明,最新版本也可以去官网获取,目前最新的是110版本。

官网地址:http://www.benf.org/other/cfr/

12 Dec 2015

Android 下拉刷新控件原理解析

#一、下拉刷新控件#

    下拉刷新效果由Atebits公司创始人Loren Brichter发明并申请“下拉刷新”专利,Twitter在2010年收购Atebits公司并获得此专利,同时Twitter签订“创新者专利协议”此协议是约束Twitter只能使用此专利用于防御目的,所以在项目中使用此效果完全不用考虑侵权的问题。

    目前手机百度、微信、FaceBook、新浪微博等大量应用都有此效果,最早的开源项目是由johannilsson在2011年1月9日发布的android-pulltorefresh不过此开源项目已经不维护,且作者推荐使用v4 support library中的SwipeRefreshLayout。另外由chrisbanes发布的开源项目Android-PullToRefresh 不仅支持ListView下拉刷新,还支持GrdiView等其他控件支持下拉刷新效果。本篇分析下这两个框架的使用与原理。

#二、下拉刷新交互# 下图来自johannilsson GitHub首页:

下拉刷新主要流程:

    上面介绍的交互都是最常见的流程,如果对交互细节与提升感兴趣可以看下有趣的下拉刷新快来QQ空间玩小鸟!~这两篇文章讨论如何把下拉刷新更趣味。

    此控件最早是在IOS平台,当移植到Andorid平台是并不是一片赞誉,感兴趣可以看下Cyril Mottier的评论“Pull-to-refresh”: An Anti UI Pattern on Android ,还有来自著名博客Android UI Patterns不一样的观点Pull-to-refresh, or not?Google’s first pull-to-refresh - a good first try

#三、chrisbanes Android-PullToRefresh开源项目#

项目地址: Android-PullToRefresh

##1.控件特点##

  • 支持顶部下拉、底部上拉、左侧向右滑动、右侧向左滑动刷新
  • 在2.3以上手机支持Over Scroll
  • 支持控件
        ListView、ExpandableListView、GridView、WebView、ScrollView、HorizontalScrollView、ViewPager、ListFragment
  • 提供滚动到列表底部的监听
  • 大量定制选项

##2.控件使用##

  • 布局文件

      <com.handmark.pulltorefresh.library.PullToRefreshListView      
          android:id="@+id/pull_refresh_list"      
          android:layout_width="fill_parent"      
          android:layout_height="fill_parent"      
          android:cacheColorHint="#00000000"      
          android:divider="#19000000"      
          android:dividerHeight="4dp"      
          android:fadingEdge="none"      
          android:fastScrollEnabled="false"      
          android:footerDividersEnabled="false"      
          android:headerDividersEnabled="false"      
          android:smoothScrollbar="true" />      
    
  • 使用

      mPullRefreshListView = (PullToRefreshListView) findViewById(R.id.pull_refresh_list);
    
      // Set a listener to be invoked when the list should be refreshed.
      mPullRefreshListView.setOnRefreshListener(new OnRefreshListener<ListView>() {
          @Override
          public void onRefresh(PullToRefreshBase<ListView> refreshView) {
              String label = DateUtils.formatDateTime(getApplicationContext(), System.currentTimeMillis(),
                      DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_ALL);
    			
              // Update the LastUpdatedLabel
              refreshView.getLoadingLayoutProxy().setLastUpdatedLabel(label);
    
              // Do work to refresh the list here.
              new GetDataTask().execute();
          }
      });
    

##3. 源码分析##

类图

    由类图可以看出此项目的主要功能类结构,此处仅分析最核心的PullToRefreshBase,通过ListView下拉刷新的一种情形分析整个流程。分析主要分为4块,布局、下拉手势判断、视图随手指移动与松手后自动回滚。

###1 布局###

private void init(Context context, AttributeSet attrs) {
	// 因为是LinearLayout的子类,可以根据需要设置横纵向展示
	switch (getPullToRefreshScrollDirection()) {
		case HORIZONTAL:
			setOrientation(LinearLayout.HORIZONTAL);
			break;
		case VERTICAL:
		default:
			setOrientation(LinearLayout.VERTICAL);
			break;
	}

	// ..... 读取一些参数

	// 创建与添加需要下拉刷新的视图
	mRefreshableView = createRefreshableView(context, attrs);
	addRefreshableView(context, mRefreshableView);

    // 仅创建顶部与底部视图,并未添加视图到视图树中
	mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a);
	mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a);

	// ..... 读取一些参数
	
	// 刷新UI
	updateUIForMode();
}

protected void updateUIForMode() {
	// We need to use the correct LayoutParam values, based on scroll
	// direction
	final LinearLayout.LayoutParams lp = getLoadingLayoutLayoutParams();
	
	// 添加Header View到顶部
	if (this == mHeaderLayout.getParent()) {
		removeView(mHeaderLayout);
	}
	if (mMode.showHeaderLoadingLayout()) {
		addViewInternal(mHeaderLayout, 0, lp);
	}

	// 添加Header View到底部
	if (this == mFooterLayout.getParent()) {
		removeView(mFooterLayout);
	}
	if (mMode.showFooterLoadingLayout()) {
		addViewInternal(mFooterLayout, lp);
	}

	// Hide Loading Views
	refreshLoadingViewsSize();

	// If we're not using Mode.BOTH, set mCurrentMode to mMode, otherwise
	// set it to pull down
	mCurrentMode = (mMode != Mode.BOTH) ? mMode : Mode.PULL_FROM_START;
}


protected final void refreshLoadingViewsSize() {
	final int maximumPullScroll = (int) (getMaximumPullScroll() * 1.2f);

	int pLeft = getPaddingLeft();
	int pTop = getPaddingTop();
	int pRight = getPaddingRight();
	int pBottom = getPaddingBottom();

	switch (getPullToRefreshScrollDirection()) {
		case HORIZONTAL:
			if (mMode.showHeaderLoadingLayout()) {
				mHeaderLayout.setWidth(maximumPullScroll);
				pLeft = -maximumPullScroll;
			} else {
				pLeft = 0;
			}

			if (mMode.showFooterLoadingLayout()) {
				mFooterLayout.setWidth(maximumPullScroll);
				pRight = -maximumPullScroll;
			} else {
				pRight = 0;
			}
			break;

		case VERTICAL:
			if (mMode.showHeaderLoadingLayout()) {
				mHeaderLayout.setHeight(maximumPullScroll);
				pTop = -maximumPullScroll;
			} else {
				pTop = 0;
			}

			if (mMode.showFooterLoadingLayout()) {
				mFooterLayout.setHeight(maximumPullScroll);
				pBottom = -maximumPullScroll;
			} else {
				pBottom = 0;
			}
			break;
	}

	if (DEBUG) {
		Log.d(LOG_TAG, String.format("Setting Padding. L: %d, T: %d, R: %d, B: %d", pLeft, pTop, pRight, pBottom));
	}
	
	// 通过把相应padding设置被负数,隐藏Header与Footer
	setPadding(pLeft, pTop, pRight, pBottom);
}

    PullToRefreshBase继承自LinearLayout,好处在于此效果视图都是横向或者纵向依次排布,完全可以复用LinearLayout排布视图的逻辑,不用自己再覆写onMeasure, onLayout去测量与排布视图,只需要设置Orientation属性并依次添加Header、RefreshableView、FooterView3个视图。

    HeaderView在refreshLoadingViewsSize函数中通过设置-paddingTop达到此默认状态不展示顶部视图的效果。布局已经完成接下来看下第2块,控件是如果进行手势判断的。

###2 下拉手势判断###

@Override
public final boolean onInterceptTouchEvent(MotionEvent event) {
	if (!isPullToRefreshEnabled()) {
		return false;
	}

	final int action = event.getAction();

	// 手指抬起与取消操作,不把事件拦截给onTouchEvent
	if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
		mIsBeingDragged = false;
		return false;
	}

	// 已经符合下拉刷新条件,果断拦截
	if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
		return true;
	}

	switch (action) {
		case MotionEvent.ACTION_MOVE: {
			// If we're refreshing, and the flag is set. Eat all MOVE events
			if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
				return true;
			}

			if (isReadyForPull()) {
				final float y = event.getY(), x = event.getX();
				final float diff, oppositeDiff, absDiff;

				// We need to use the correct values, based on scroll
				// direction
				switch (getPullToRefreshScrollDirection()) {
					case HORIZONTAL:
						diff = x - mLastMotionX;
						oppositeDiff = y - mLastMotionY;
						break;
					case VERTICAL:
					default:
						diff = y - mLastMotionY;
						oppositeDiff = x - mLastMotionX;
						break;
				}
				// 滑动的绝对值,仅用于获取移动长度,后续有单独的方向判断
				absDiff = Math.abs(diff);

				// 手指在屏幕上的移动距离已经满足滚动条件,但是移动距离小于TouchSlop时ListView ItemView会处于tap按下状态,此时并不拦截
				// 需要纵轴的移动距离大于横轴的移动距离,目的是斜着在屏幕上滑动时不会触发。
				if (absDiff > mTouchSlop && (!mFilterTouchEvents || absDiff > Math.abs(oppositeDiff))) {
					// diff >= 1f 方向判断,说明是向下或者向右
					if (mMode.showHeaderLoadingLayout() && diff >= 1f && isReadyForPullStart()) {
						mLastMotionY = y;
						mLastMotionX = x;
						mIsBeingDragged = true;
						if (mMode == Mode.BOTH) {
							mCurrentMode = Mode.PULL_FROM_START;
						}
					} else if (mMode.showFooterLoadingLayout() && diff <= -1f && isReadyForPullEnd()) {
						mLastMotionY = y;
						mLastMotionX = x;
						mIsBeingDragged = true;
						if (mMode == Mode.BOTH) {
							mCurrentMode = Mode.PULL_FROM_END;
						}
					}
				}
			}
			break;
		}
		case MotionEvent.ACTION_DOWN: {
			if (isReadyForPull()) {
				mLastMotionY = mInitialMotionY = event.getY();
				mLastMotionX = mInitialMotionX = event.getX();
				mIsBeingDragged = false;
			}
			break;
		}
	}

	// 返回true说明已经触发拖住刷新
	return mIsBeingDragged;
}

// 来自PullToRefreshAdapterViewBase,用于判断ListView与GridView是否满足下拉条件
protected boolean isReadyForPullStart() {
	return isFirstItemVisible();
}

private boolean isFirstItemVisible() {
	final Adapter adapter = mRefreshableView.getAdapter();

	if (null == adapter || adapter.isEmpty()) {
		if (DEBUG) {
			Log.d(LOG_TAG, "isFirstItemVisible. Empty View.");
		}
		return true;

	} else {

		/**
		 * This check should really just be:
		 * mRefreshableView.getFirstVisiblePosition() == 0, but PtRListView
		 * internally use a HeaderView which messes the positions up. For
		 * now we'll just add one to account for it and rely on the inner
		 * condition which checks getTop().
		 */
		if (mRefreshableView.getFirstVisiblePosition() <= 1) {
			final View firstVisibleChild = mRefreshableView.getChildAt(0);
			if (firstVisibleChild != null) {
				return firstVisibleChild.getTop() >= mRefreshableView.getTop();
			}
		}
	}

	return false;
}

    此控件在刷新视图外添加一层LinearLayout,然后通过onInterceptTouchEvent函数判断如何满足下拉刷新条件进行拦截手势处理,不继续派发给刷新视图,因为这两个条件此控件可以支持任意视图,例如ListView、Gridview,仅需要这些视图然后告知PullToRefreshBase何时满足下拉刷新条件即可(ListView下拉刷新是告知已到达ListView顶部)。

###Android 事件传递流程###

  • 1)传递流程
    传递: ViewGroup/View.dispatchTouchEvent(MotionEvent)
    拦截: ViewGroup.onInterceptTouchEvent(MotionEvent)
    处理: ViewGroup/View.onTouchEvent(MotionEvent)

    Android事件每隔几毫秒派发一次,在View层传递主要涉及以上3个函数,由上图的ViewRoot向下逐层传递,每层仅有一个视图满足传递条件,通过调用满足条件子视图的dispatchTouchEvent向下传递事件,如果有视图消耗此事件再向上返回true,表示此次事件已经被处理。

  • 2)单次传递规律
    向下传递:

      ViewGroup.dispatchTouchEvent()      
          通过当前所有子视图添加顺序(addView)的反序遍历,是否满足以下条件      
          判断子视图是否显示,如果不显示肯定不需要向此子视图派发         
          检查位置是否在当前子视图内。     
          以上是最常见判断,如果满足调用此视图dispatchTouchEvent传递事件            
    	      
      ViewGroup.onInterceptTouchEvent()      
          如果传递到当前视图,通过覆写此函数返回true,拦截此事件并派发给当前视图onTouchEvent函数    
          通用用于手势冲突            
    	
      View.onTouchEvent()      
          通用用于手势处理,例如控制视图移动,处理视图点击行为等。
     			此函数中会调用的回调:
          setOnClickListener         
          setOnLongClickListener       
          setOnTouchListener      
          setOnItemClickListener    
    

向上传递:

	有视图处理,onTouchEvent return true(消耗),View.dispatchTouchEvent()逐层返回true。            
	最底层子视图未处理,会返回上层,父视图是否处理。      
  • 4)基础知识 MotionEvent

      getX(),getY() 获取的是当前视图针对当前父视图的x,y轴距离      
      getRawX(),getRawY() 获取的是针对屏幕左上角的距离。      
      时间、历史记录,多点            
      事件类型ACTION_DOWN, ACTION_UP, ACTION_MOVE, ACTION_CANCEL, ACTION_POINTER_DOWN, ACTION_POINTER_UP            
    
  • 5)手势识别:
    单点:GestureDetector
    多点缩放:ScaleGestureDetector

    以上比较简单的总结Touch事件,详细可查看文档Mastering the Android Touch System

###3 视图随手指移动###

@Override
public final boolean onTouchEvent(MotionEvent event) {
    
	if (!isPullToRefreshEnabled()) {
		return false;
	}

	// If we're refreshing, and the flag is set. Eat the event
	if (!mScrollingWhileRefreshingEnabled && isRefreshing()) {
		return true;
	}

	// 按下的时候已经到当前视图边界,已经出范围所以不是拖拽刷新
	if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) {
		return false;
	}

	switch (event.getAction()) {
		case MotionEvent.ACTION_MOVE: {
			if (mIsBeingDragged) {
				mLastMotionY = event.getY();
				mLastMotionX = event.getX();
				// 处理视图拖拽操作
				pullEvent();
				return true;
			}
			break;
		}

		case MotionEvent.ACTION_DOWN: {
		    // 如果手指触及的当前类的子视图未处理onTouch,此时当前onInterceptTouchEvent函数
		    // 还未满足判断是否为mIsBeingDragged的条件,所以此处需要判断是否满足滚动前的边界条件
			if (isReadyForPull()) {
				mLastMotionY = mInitialMotionY = event.getY();
				mLastMotionX = mInitialMotionX = event.getX();
				return true;
			}
			break;
		}

		case MotionEvent.ACTION_CANCEL:
		case MotionEvent.ACTION_UP: {
			if (mIsBeingDragged) {
				mIsBeingDragged = false;

				if (mState == State.RELEASE_TO_REFRESH
						&& (null != mOnRefreshListener || null != mOnRefreshListener2)) {
					setState(State.REFRESHING, true);
					return true;
				}

				// If we're already refreshing, just scroll back to the top
				if (isRefreshing()) {
					smoothScrollTo(0);
					return true;
				}

				// If we haven't returned by here, then we're not in a state
				// to pull, so just reset
				setState(State.RESET);

				return true;
			}
			break;
		}
	}

	return false;
}

onTouchEvent函数中通过pullEvent处理视图跟随手指移动,通过smoothScrollTo处理视图自动滚动。

private void pullEvent() {
	final int newScrollValue;
	final int itemDimension;
	final float initialMotionValue, lastMotionValue;

	switch (getPullToRefreshScrollDirection()) {
		case HORIZONTAL:
			initialMotionValue = mInitialMotionX;
			lastMotionValue = mLastMotionX;
			break;
		case VERTICAL:
		default:
			initialMotionValue = mInitialMotionY;
			lastMotionValue = mLastMotionY;
			break;
	}
	
	switch (mCurrentMode) {
		case PULL_FROM_END:
		    // 向上或者向左拖拽不能为正值
			newScrollValue = Math.round(Math.max(initialMotionValue - lastMotionValue, 0) / FRICTION);
			itemDimension = getFooterSize();
			break;
		case PULL_FROM_START:
		default:
		    // 向下或者向右如果满足拖拽条件,移动的值肯定是大于0。
		    // 移动的值/2是摩察系数,最明显的就是屏幕顶部向下滚动到底部,但是其中被拖拽视图仅向下移动屏幕的一半
		    // 如果不添加Math.round,手指在屏幕上一个像素的速度移动,此处算出的float值会永远不变,视图也不会移动
			newScrollValue = Math.round(Math.min(initialMotionValue - lastMotionValue, 0) / FRICTION);
			itemDimension = getHeaderSize();
			break;
	}

	// 视图移动使用scrollTo,所以传入的是手指在屏幕上移动的距离
	// 如果减去mTouchSlop,刚开始滚动的时候就不会有一个跳动的感觉
	setHeaderScroll(newScrollValue);

	if (newScrollValue != 0 && !isRefreshing()) {
		float scale = Math.abs(newScrollValue) / (float) itemDimension;
		switch (mCurrentMode) {
			case PULL_FROM_END:
				mFooterLayout.onPull(scale);
				break;
			case PULL_FROM_START:
			default:
			    // 通知顶部视图刷新
				mHeaderLayout.onPull(scale);
				break;
		}

		if (mState != State.PULL_TO_REFRESH && itemDimension >= Math.abs(newScrollValue)) {
			// 向下滚动的距离已经超出顶部视图高度,认为是下拉刷新状态
			setState(State.PULL_TO_REFRESH);
		} else if (mState == State.PULL_TO_REFRESH && itemDimension < Math.abs(newScrollValue)) {
		    // 向下滑动距离大于顶部视图高度,现在如果松开手已经满足刷新数据的条件
			setState(State.RELEASE_TO_REFRESH);
		}
	}
}

protected final void setHeaderScroll(int value) {
	if (DEBUG) {
		Log.d(LOG_TAG, "setHeaderScroll: " + value);
	}

	// -max ~ max
	// Clamp value to with pull scroll range
	final int maximumPullScroll = getMaximumPullScroll();
	value = Math.min(maximumPullScroll, Math.max(-maximumPullScroll, value));

	if (mLayoutVisibilityChangesEnabled) {
	    // 移动方向正确,且有移动距离才展示
		if (value < 0) {
			mHeaderLayout.setVisibility(View.VISIBLE);
		} else if (value > 0) {
			mFooterLayout.setVisibility(View.VISIBLE);
		} else {
			mHeaderLayout.setVisibility(View.INVISIBLE);
			mFooterLayout.setVisibility(View.INVISIBLE);
		}
	}

	if (USE_HW_LAYERS) {
		/**
		 * Use a Hardware Layer on the Refreshable View if we've scrolled at
		 * all. We don't use them on the Header/Footer Views as they change
		 * often, which would negate any HW layer performance boost.
		 */
		ViewCompat.setLayerType(mRefreshableViewWrapper, value != 0 ? View.LAYER_TYPE_HARDWARE
				: View.LAYER_TYPE_NONE);
	}

	// 移动到指定位置
	switch (getPullToRefreshScrollDirection()) {
		case VERTICAL:
			scrollTo(0, value);
			break;
		case HORIZONTAL:
			scrollTo(value, 0);
			break;
	}
}

###视图移动方法###

    此控件通过scrollTo函数来移动视图,目前已知有4种实现视图移动的方法:

方法 修改值 效果
1 mScrollX,mScrollY view.scrollTo(int x, int y)、scrollBy(int x, int y),视图大小与位置(x, y)都为发未改变,移动过程中不会触发onMeasure,onLayout函数,仅触发onDraw。
    在视图上调用此函数,当前视图不会移动而是移动其所有子视图。
2 x,y 修改left,top,right,bottom移动视图,通过view.layout或者view.offsetTopAndBottom、view.offsetLeftAndRight函数达到效果,ListView控制Item移动使用的是后者。
3 padding 最早的johannilsson实现的下拉刷新就是基于这种,不过需要每次都重新measure、layout才能生效。
4 margin 从来没见过哪个开源控件使用此种方式实现,不过也是一种使视图位置改变的一种办法。

###4 视图自动滚动 ###

final void setState(State state, final boolean... params) {
	mState = state;
	if (DEBUG) {
		Log.d(LOG_TAG, "State: " + mState.name());
	}

	switch (mState) {
		case RESET:
			// 列表滚动到顶部,顶部视图也重置为默认状态
			onReset();
			break;
		case PULL_TO_REFRESH:
			// 通知顶部视图下拉中
			onPullToRefresh();
			break;
		case RELEASE_TO_REFRESH:
			// 通知顶部视图手指释放刷新中
			onReleaseToRefresh();
			break;
		case REFRESHING:
		case MANUAL_REFRESHING:
			// 自动滚动到漏出顶部视图区域
			onRefreshing(params[0]); 
			break;
		case OVERSCROLLING:
			// NO-OP
			break;
	}

	// Call OnPullEventListener
	if (null != mOnPullEventListener) {
		mOnPullEventListener.onPullEvent(this, mState, mCurrentMode);
	}
}

protected void onRefreshing(final boolean doScroll) {
    if (mMode.showHeaderLoadingLayout()) {
        mHeaderLayout.refreshing();
    }
    if (mMode.showFooterLoadingLayout()) {
        mFooterLayout.refreshing();
    }
 
    if (doScroll) {
        if (mShowViewWhileRefreshing) {
 
            // Call Refresh Listener when the Scroll has finished
            OnSmoothScrollFinishedListener listener = new OnSmoothScrollFinishedListener() {
                @Override
                public void onSmoothScrollFinished() {
                    callRefreshListener();
                }
            };
 
            switch (mCurrentMode) {
                case MANUAL_REFRESH_ONLY:
                case PULL_FROM_END:
                    smoothScrollTo(getFooterSize(), listener);
                    break;
                default:
                case PULL_FROM_START:
					// 注意是负值,scrollY向下是负数,向上相反
					// 向上滚动到HeaderSize高度的位置
                    smoothScrollTo(-getHeaderSize(), listener);
                    break;
            }
        } else {
			// 回滚到初始状态
            smoothScrollTo(0);
        }
    } else {
        // We're not scrolling, so just call Refresh Listener now
        callRefreshListener();
    }
}

private final void smoothScrollTo(int newScrollValue, long duration, long delayMillis,
		OnSmoothScrollFinishedListener listener) {
	// 停止自动滚动动画
	if (null != mCurrentSmoothScrollRunnable) {
		mCurrentSmoothScrollRunnable.stop();
	}

	// 当前位置
	final int oldScrollValue;
	switch (getPullToRefreshScrollDirection()) {
		case HORIZONTAL:
			oldScrollValue = getScrollX();
			break;
		case VERTICAL:
		default:
			oldScrollValue = getScrollY();
			break;
	}

	if (oldScrollValue != newScrollValue) {
		if (null == mScrollAnimationInterpolator) {
			// Default interpolator is a Decelerate Interpolator
			mScrollAnimationInterpolator = new DecelerateInterpolator();
		}
		mCurrentSmoothScrollRunnable = new SmoothScrollRunnable(oldScrollValue, newScrollValue, duration, listener);

		// 执行子线程
		if (delayMillis > 0) {
			postDelayed(mCurrentSmoothScrollRunnable, delayMillis);
		} else {
			post(mCurrentSmoothScrollRunnable);
		}
	}
}


final class SmoothScrollRunnable implements Runnable {

	@Override
	public void run() {

		/**
		 * Only set mStartTime if this is the first time we're starting,
		 * else actually calculate the Y delta
		 */
		if (mStartTime == -1) {
			mStartTime = System.currentTimeMillis();
		} else {

			/**
			 * We do do all calculations in long to reduce software float
			 * calculations. We use 1000 as it gives us good accuracy and
			 * small rounding errors
			 */
			// 时间消耗比例
			long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime)) / mDuration;
			normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0);
			
			final int deltaY = Math.round((mScrollFromY - mScrollToY)
					* mInterpolator.getInterpolation(normalizedTime / 1000f));
			mCurrentY = mScrollFromY - deltaY;
			setHeaderScroll(mCurrentY);
		}

		// If we're not at the target Y, keep going...
		if (mContinueRunning && mScrollToY != mCurrentY) {
			ViewCompat.postOnAnimation(PullToRefreshBase.this, this);
		} else {
			if (null != mListener) {
				mListener.onSmoothScrollFinished();
			}
		}
	}

	public void stop() {
		mContinueRunning = false;
		removeCallbacks(this);
	}
}

public class ViewCompat {

	public static void postOnAnimation(View view, Runnable runnable) {
		if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
			SDK16.postOnAnimation(view, runnable);
		} else {
			view.postDelayed(runnable, 16);
		}
	}
}

###视图自动滚动方法### * 自动滚动的循环方式:

  • 1.使用Handler

      class ScrollRunnable implements Runnable {
          @Override
          public void run() {
              if (currentY < toY) {
                  offsetTopAndBottom(offsetY);
                  // ViewCompat.postOnAnimation
                  mHandler().postDelayed(this, 16);	            
              }
          }
      };
    

    Handler发出一个消息,执行此消息时如果满足判断,改变位置再发出一个Handler消息。

  • 2.利用系统机制

      @Override
      public void computeScroll() {
    	    
          if (currentY < toY) {
              offsetTopAndBottom(offsetY);
              invalidate();
          }
      }
    

    调用invalidate()函数后,最终会执行onDraw,onDraw中会调用computeScroll()函数。如果未到指定位置,再次出发刷新,达到循环的效果。

  • 3.使用动画

      ObjectAnimator yAnimator = ObjectAnimator.ofFloat(view, "translationY", fromY, toY);
    

    这种可以实现效果,在Android 3.0以下需要使用nineoldanimation.jar开源库,框架通过修改视图的Matix达到在Android 3.0以下视图视觉上发生移动,但是视图的位置并未发生改变导致点击视图并不一定触发视图的点击事件。

  • 自动滚动循环过程中获取当前位置

      // 需要执行自动滚动处调用      
      mScroller.startScroll(startX, startY, dx, dy, duration);
    
      private class ScrollerRunnable implements Runnable {  
          @Override  
          public void run() {  
              final Scroller scroller = mScroller;  
    			
              if (scroller.computeScrollOffset()) {  
                  final int currentY = scroller.getCurrY();
    
                  ......
    
                  offsetTopAndBottom(offsetY);
    				
                  invalidate();  
                  mHandler.postDelayed(this, DELAY_MILLIS);  
              }  
          }  
      }  
    

    Scroller本身并不控制视图的移动,仅仅是提供数值。通过当前消耗时间占总时间的比例乘以总长度,算出当前移动的距离。 如果希望减速、加速滚动等可以使用Interpolator 插值器,详见:android动画(一)Interpolator

#四、Android support v4 SwipeRefreshLayout#     Android V4 在19.1与20分别提供两种样式的下拉刷新效果

    Android support v4 19.1的效果如下,下拉时ListView可以跟随手指移动,但是加载视图并不是在ListView的上面,而是叠在ListView顶部。

    Android support v4 20的效果如下,下拉时ListView不会跟随手指移动。

    以上主要针对chrisbanes开源项目分析下拉刷新实现原理并对一些场景进行讨论。

14 Oct 2015

Android 64位兼容方式运行32位分析

关于Android L 64位系统兼容32位应用的实现的简单分析。

Android L 的zygote进程的实现不同于之前的版本,除了有zygote进程之外还有zygote64进程。 在init.zygote32_64.rc中有明确指出:

service zygote /system/bin/app_process32 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
...
service zygote_secondary /system/bin/app_process64 -Xzygote /system/bin --zygote --socket-name=zygote_secondary
...

其中app_process32 和app_process64 就是zygote进程的可执行程序,启动后会改名成zygote。

顾名思义,zygote32即app_process32是一个运行在32位的进程,它所连接的库也都是32位的。而zygote64就是运行在64位的进程,它所连接的库都是64位的。

在不考虑有32/64兼容库的情况下,一个进程如果要正确运行,就必须从可执行程序入口开始到所有使用的库都保持32/64位的一致性。

因为zygote进程是所有第三方应用程序的父进程,所以可以认为,如果应用程序是32位的,那没他的父进程也肯定是32位,换句话说,如果需要启动某个32位的应用,那么肯定是通过32位的zygote进程fork出来的。

这个一点可以在ActivityManagerService上得到验证。

ActivityManagerService中startProcessLocked 方法实现启动应用,主要通过Process中的startViaZygote方法,这个方法最终是向相应的zygote进程发出fork的请求 zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);

其中openZygoteSocketIfNeeded(abi)会根据abi的类型,选择不同的zygote的socket监听的端口,在之前的init文件中可以看到

zygote32位监听的端口就是–socket-name=zygote

另外一个就是–socket-name=zygote_secondary

因此可以证实,之前的猜测,即32应用进由32位zygote进程fork出来,64位应用进程由64zygote进程fork出来。

那么之前说的abi参数就是决定应用是32还是64位的关键所在,跟踪这个参数,发现这个参数在ApplicationInfo的primaryCpuAbi中决定,

这个值由PackageManagerService在做scanPackageLI的时候决定,具体这个值的得出有一个公式化的过程,主要就是判断这个apk有没有使用native的库,如果使用了,那就看使用了的是32位的还是64位的,另外还要看系统支持的是32位还是64位的。

在64位设备上,如果app的 lib 目录下 存在armeabi,则以32位兼容方式运行。如果存在arm64-v8a 则已64位运行。如果没有任何 so,则 primaryCpuAbi 为空,按照系统的默认配置决定,也就是64位运行。

根据这些因素就可以决定这个apk是应该是32位的还是64位的。

以上就是Android L 64位系统兼容32位应用的基本实现过程。

另外记录一点,在源码环境下如果要PREBUILT第三方的so,如果是32位的需要专门标注 LOCAL_MULTILIB := 32

以此告诉编译系统so位32位,防止编译到64位下去。

来源 http://stackoverflow.com/questions/27712921/how-to-use-32bit-native-libraries-on-64-bit-android-l-platform

本文转自 http://blog.csdn.net/louyong0571/article/details/44223481

Older Posts

Android monkey OOM 问题分析
Android 5.1 Webview 内存泄漏新场景
LeakCanary原理分析
Android aidl backward & forward compatible
通过Accessibility自动安装与卸载应用
mainDexClasses脚本分析
SVN分支间Merge注意事项
MultiDex
Compile MAT Source Code
MAT内存分析之-查看android内存中的bitmap