Android6.0之App中的资源Rsources.arsc详解

Apk中的resources.arsc是aapt工具编译资源时生成的一个重要文件。App资源能根据配置的变化,索引到相应的资源都要依赖它。例如Android设备语言,屏幕设备尺寸不同时,app通过同样的ID但却能找到不同的资源进行显示。

资源打包过程简述

开发app时,需要代码和资源。最终生成的apk中代码转换为了dex文件,那么apk文件中的资源是否还是app开发时那些资源文件呢?或者说这些资源文件是否发生了什么变化?

引用老罗一张关于资源打包过程以及查找的图:

资源打包.jpg

从上图可以看出:

  1. 除了assets和res/raw资源被原装不动地打包进APK之外,其它的资源都会被编译或者处理.xml文件会被编译为二进制的xml,所以解压apk后,无法直接打开xml文件。

  2. 除了assets资源之外,其它的资源都会被赋予一个资源ID。

  3. 打包工具负责编译和打包资源,编译完成之后,会生成一个resources.arsc文件和一个R.java,前者保存的是一个资源索引表,后者定义了各个资源ID常量,供在代码中索引资源。

  4. 应用程序配置文件AndroidManifest.xml同样会被编译成二进制的XML文件,然后再打包到APK里面去。

  5. 应用程序在运行时最终是通过AssetManager来访问资源,或通过资源ID来访问,或通过文件名来访问。

在生成的apk中,只有assets和res/raw资源被原装不动地打包进apk。其它的资源都会被编译或者处理。可以使用如下命令查看apk中的文件列表:

aapt l -v apkfile

将apk直接解压后,会发现xml都打不开,提示格式不对,因为其已经变为二进制xml了。另外PNG等图片也会进行相应的优化。还有就是多了一个resources.arsc文件。

需要准备的东西

分析resources.arsc文件,肯定要现有它了。利用Android studio创建一个ResourceDemo的工程,

资源从取值上来分,可分为两类:bag类型资源和非bag类型的资源。

bag资源:通俗的说,就是这类资源在赋值的时候,不能随便赋值,只能从事先定义好的值中选取一个赋值。很像枚举。

类型为values的资源除了是string之外,还有其它很多类型的资源,其中有一些比较特殊,如bag、style、plurals和array类的资源。这些资源会给自己定义一些专用的值,这些带有专用值的资源就统称为Bag资源。

例如,Android系统提供的android:orientation属性的取值范围为{“vertical”、“horizontal”},就相当于是定义了vertical和horizontal两个Bag。

在res/values中创建attrs.xml文件,在其中自定一个bag类型的属性资源。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="custom_orientation">
        <enum name="custom_vertical" value="100" />
        <enum name="custom_horizontal" value="200" />
    </attr>
</resources>

这个文件定义了一个名称为“custom_orientation”的属性资源,它是一个枚举格式(也可理解为枚举类型)的属性,可以取值为“custom_vertical”或者“custom_horizontal”。

custom_vertical和custom_horizontal是custom_orientation的两个bag,我们可以将custom_vertical和custom_horizontal看成是custom_orientation的两个元数据,用来描述custom_orientation的取值集合。

“custom_orientation”是一个枚举类型的attr属性资源,也要使用一个内部元数据来描述其属性类型,这个元数据也使用一个bag来表示。

也就是说custom_orientation是由三个bag构成的:

第一个bag:名称是“^type”,值是TYPE_ENUM(TYPE_ENUM = 1<<16)

第二个bag:名称是“custom_vertical”,值是100

第三个bag: 名称是“custom_horizontal”,值是200

另外还要给这个bag分配资源ID,因为这些枚举值是通过名称,例如custom_vertical被引用使用的,所以也要给其分配资源ID,

资源ID的格式是PPTTEEEE,其中TT代表资源类型。那么:

名称是“^type”的bag其分配的资源ID是attr类型的,而“custom_vertical”和“custom_horizontal”被分配到的资源ID是id类型的,所以在代码中可以通过下面的形式引用其值:

     R.attr.custom_orientation;
     R.id.custom_horizontal;
     R.id.custom_vertical;

非bag资源:通俗的说,就是这类资源赋值的时候,很随意,可以任意指定。

以res/values/strings.xml为例:

<resources>
    <string name="app_name">ResourceDemo</string>
</resources>

该文件中定义了一个名字为“app_name”的string类型的资源,资源值为ResourceDemo。

将这个ResourceDemo工程编译之后,解压APK,就可以得到resources.arsc文件了。

要做什么

接下来就是分析resources.arsc,看看前面我们指出的那三个bag资源和一个非bag资源是以什么样的形式存储在resources.arsc的什么位置的。

只要搞清楚了这个,那么就沉底搞清楚resources.arsc文件的格式了。

resources.arsc

resources.arsc文件的作用就是通过一样的ID,根据不同的配置索引到最佳的资源显示在UI中。

从整体上来看,其结构为:资源索引表头部+字符串资源池+N个Package数据块。文件格式:

AMS-33.png

这张神图在网上广为流传,但是其下半部分很容易让人产生误解,因为实际上Type Spec和 Config List是交替出现的,而且一个Type Spec通常有不止一个config list.

不想在画图了,所以仍旧以此神图为模板分析resources.arsc文件吧。

这里不去纠结这个文件是如何生成的,咱们逆其道而行,从文件本身窥探它是有什么组成的。

resources.arsc文件的结构分割符

从上面所示的神图中可以看到arsc文件是由若干种chunk组成的,而每一种chunk都是由一个头部来记录一些相关信息,例如该部分是什么,占多大空间等。

而每种chunk的头部又是在一个基础头部上扩展而来的的,这个基础头部是strcut Resheader:

源码路径:

 AOSP-6.0/frameworks/base/include/androidfw/ResourceTypes.h
struct Resheader
{
    //表示这是一个什么chunk
    uint16_t type;
    //chunk header 大小
    uint16_t headerSize;
    // chunk headr + chunk data,也就是 chunk的总大小
    uint32_t size;
};

每一部分的头部也是一个结构体,这个结构体继承自Resheader(按照C语言来理解,就是结构体的首元素是Resheader)。

在resources.arsc中type的取值有:

RES_NULL_TYPE               = 0x0000,
RES_STRING_POOL_TYPE        = 0x0001,
RES_TABLE_TYPE              = 0x0002,
RES_XML_TYPE                = 0x0003,
// Chunk types in RES_TABLE_TYPE
RES_TABLE_PACKAGE_TYPE      = 0x0200,
RES_TABLE_TYPE_TYPE         = 0x0201,
RES_TABLE_TYPE_SPEC_TYPE    = 0x0202,
RES_TABLE_LIBRARY_TYPE      = 0x0203

resources.arsc头部

resources.arsc头部,即索引表头部,其结构如下:

struct ResTable_header
{
    struct Resheader header;
    // 该resources.arsc文件中包含几个package资源包,
    // 通常一个app只会包一个package资源包,就是自己
    uint32_t packageCount;
};

这里header.headerSize就是这个struct ResTable_header的大小,header.size是这个resources.arsc文件的大小。而pacakageCount为1.

resources.arsc的文件是一个索引表,是RES_TABLE_TYPE,也就是说header.type为RES_TABLE_TYPE。

代码验证:

struct stat buf;
stat("./resources.arsc", &buf);
int fd = open("./resources.arsc",0644);
uint8_t *data = (uint8_t*)mmap(NULL,buf.st_size,PROT_READ,MAP_PRIVATE,fd,0);
printf("################# res 文件头部信息 #################\n");
ResTable_header *resHd = (ResTable_header*)data;
printf("res type           = %p\n",resHd->header.type);
printf("res chunk hd size  = %p\n",resHd->header.headerSize);
printf("res chunk    size  = %p\n",resHd->header.size);
printf("res packages count = %p\n",resHd->packageCount);

结果:

################# res 文件头部信息 #################
res type           = 0x2
res chunk hd size  = 0xc
res chunk    size  = 0x2f268
res packages count = 0x1

0x2与RES_TABLE_TYPE相等,packages count为1都与预期相一致。

字符串资源值池

这一部分的存储的字符串,都是资源的值,而且值是字符串类型。

以res/values/strings.xml为例:

<resources>
    <string name="app_name">ResourceDemo</string>
</resources>

该文件中定义了一个名字为“app_name”的string类型的资源,资源值为ResourceDemo。

ResourceDemo就存在这一部分,而"app_name"与"string"并没有存储在这里。

这一字符串池也包含一个头部:

struct ResStringPool_header
{
    struct Resheader header;

    // 字符串个数
    uint32_t stringCount;

    //字符串样式个数
    uint32_t styleCount;

    // Flags
    enum {
        // If set, the string index is sorted by the string values (based
        // on strcmp16()).
        SORTED_FLAG = 1<<0,

        // String pool is encoded in UTF-8
        UTF8_FLAG = 1<<8
    };
    // 该字符串是string16还是string8类型
    uint32_t flags;

    //字符数组相对头部的位置
    uint32_t stringsStart;

    //样式数组相对头部的位置
    uint32_t stylesStart;
}

简单介绍这个字符串池如何存储和索引字符串。

从神图中可以看出紧跟着头部的后面是两个uint32类型的数组:字符串偏移数组和样式偏移数组,数组元素个数分别为stringCount和styleCount.

这两个数组后面之后还有两个字符数组:字符串字符数组和与样式字符数组。这两个字符数组都很大很大。。。。

字符串偏移数组中的元素,就是一个字符串在字符串字符数组中的索引,而且根据索引得到的字符串的前面两个字节表示其长度,而且是以NULL结尾的,所以不会索引到其他内容。

样式偏移数组中的元素,就是一个样式在样式字符数组中的索引。这里不考虑样式的情况。有兴趣的可以参考老罗的博客。

现在已经搞清楚这个字符串池的结构了,而且也知道"ResourceDemo"这个字符串应该就这个字符串池里,只不过在这个字符串前面加了两个字节,表示其长度。

这两个用于存储长度的字节,并不是单纯的把长度存储在这两个字节中,而是有规则的:

  1. 与字符串格式是string8还是string16相关

  2. string8类型的长度解码如下:

static inline size_t
decodeLength(const uint8_t** str)
{
    size_t len = **str;
    if ((len & 0x80) != 0) {
        (*str)++;
        len = ((len & 0x7F) << 8) | **str;
    }
    (*str)++;
    return len;
}

传入的参数是从字符串字符数组中以字符串偏移数组中的偏移为索引的字符串(前两个字节是长度)。

  1. string16类型长度解码如下:
static inline size_t
decodeLength(const uint16_t** str)
{
    size_t len = **str;
    if ((len & 0x8000) != 0) {
        (*str)++;
        len = ((len & 0x7FFF) << 16) | **str;
    }
    (*str)++;
    return len;
}

那么来验证一下:

  printf("################# resStringPool(资源项的值字符串资源池)头部信息 #################\n");
  ResStringPool_header *resStrPoolHd = (ResStringPool_header *)((uint8_t*)resHd+resHd->header.headerSize);
  printf("type           = %p\n",resStrPoolHd->header.type);
  // 这个chunk数据块头部的大小
  printf("chunk hd size  = %p\n",resStrPoolHd->header.headerSize);
  // 这个chunk数据块的大小
  printf("chunk    size  = %p\n",resStrPoolHd->header.size);
  printf("stringCount    = %p\n",resStrPoolHd->stringCount);
  printf("styleCount     = %p\n",resStrPoolHd->styleCount);
  printf("flags          = %p\n",resStrPoolHd->flags);
  printf("stringsStart   = %p\n",resStrPoolHd->stringsStart);
  printf("stylesStart    = %p\n",resStrPoolHd->stylesStart);
  // header后面紧接着是两个偏移数组,之后才是数据
  // 字符串偏移数组,数组元素个数是stringCount
  const uint32_t*  mEntries = (const uint32_t*)(data+sizeof(ResTable_header)+resStrPoolHd->header.headerSize);
  const uint32_t*  mEntryStyles;
  uint32_t         mStylePoolSize;
  // 字符串池地址
  const void * mString = (const void *)((const uint8_t*)resStrPoolHd+resStrPoolHd->stringsStart);
  const void * mStyleString;
  if(resStrPoolHd->styleCount>0){
    // 字符串样式偏移数组,数组个数是styleCount,样式数组和字符串数组一一对应
    // 也就是说在字符串偏移数组中所以为N的字符串,其样式在样式数组中的索引也为N
    mEntryStyles = mEntries + resStrPoolHd->stringCount;
    mStyleString = (void *)((const uint8_t*)resStrPoolHd+resStrPoolHd->stylesStart);
    mStylePoolSize = (resStrPoolHd->header.size-resStrPoolHd->stylesStart)/sizeof(uint32_t);
  }
  for(int i=0;i<resStrPoolHd->stringCount;i++){
    // 加2是跳过长度
    if(strcmp(((char*)mString)+mEntries[i]+2,"ResourceDemo")==0){
        const uint8_t * str = (((uint8_t*)mString)+mEntries[i]);
        // 字符串长度
        int len = decodeLength(&str);
        printf("-->len = %p\n",len);
        printf("-->idx = %p\n",i);
        // 前两字节是长度
        printf("-->%s\n",(((uint8_t*)mString)+mEntries[i])+2);
    }
  }

测试结果:

################# resStringPool(资源项的值字符串资源池)头部信息 #################
type           = 0x1
chunk hd size  = 0x1c
chunk    size  = 0xcfb8
stringCount    = 0x610
styleCount     = (nil)
flags          = 0x100
stringsStart   = 0x185c
stylesStart    = (nil)
-->len = 0xc
-->idx = 0x148
-->ResourceDemo

type为RES_STRING_POOL_TYPE正确。

flags为0x100,表明是string8,也就是utf-8字符串。

len为0xc,即12,而ResourceDemo长度为12,也正确。也找到了ResourceDemo。

package数据部分

这一部分最为复杂。索引表头部中的packageCount记录了索引表中有多少各package数据部分。通常只有一个。

同样这一部分的开头也是一个头部,结构如下:

struct ResTable_package
{
    struct Resheader header;

    // 包ID
    uint32_t id;

    //package名字,string16形式存储
    uint16_t name[128];

    // 类型字符串池,相对package头部的偏移
    uint32_t typeStrings;

    // 包中共有资源类型的种数
    uint32_t lastPublicType;

    // 资源项名称字符串池,相对package头部的偏移
    uint32_t keyStrings;

    // 资源项的数量
    uint32_t lastPublicKey;

    uint32_t typeIdOffset;
};

其中ID是由命名规则的,系统资源包id为0x1,而app的资源包ID为0x7f,0x1-0x7f的都是合法的。

从神图中可以看到紧跟着这个头部的是两个字符串池,都和前面介绍的资源项字符串池结构一样。

那么这两个字符串池用来存储什么东东呢?

仍以res/values/strings.xml为例:

<resources>
    <string name="app_name">ResourceDemo</string>
</resources>

该文件中定义了一个名字为“app_name”的string类型的资源项,资源值为ResourceDemo。

ResourceDemo就存在在前面介绍的资源项字符串池中,"string"存储在类型字符串池中,“app_name”存储在资源项名称字符串池中。

代码验证下:

printf("################# ResTablePackage头部信息 #################\n");

  ResTablePackage_header *resTablePackageHd = (ResTablePackage_header *)(data+sizeof(ResTable_header)+resStrPoolHd->header.size);
  printf("resTablePackage type           = %p\n",resTablePackageHd->header.type);
  printf("resTablePackage chunk hd size  = %p\n",resTablePackageHd->header.headerSize);
  printf("resTablePackage chunk    size  = %p\n",resTablePackageHd->header.size);

  printf("resTablePackage id             = %p\n",resTablePackageHd->id);
  char *name = allocFromUTF16(resTablePackageHd->name,128);
  printf("resTablePackage name           = %s\n",name);
  free(name);
  name = NULL;
  printf("resTablePackage typeStrings    = %p\n",resTablePackageHd->typeStrings);
  //目前这个值设置为类型字符串资源池的元素个数
  printf("resTablePackage lastPublicType = %p\n",resTablePackageHd->lastPublicType);

  printf("resTablePackage keyStrings     = %p\n",resTablePackageHd->keyStrings);
  //目前这个值设置为资源项名称字符串资源池的元素个数
  printf("resTablePackage lastPublicKey  = %p\n",resTablePackageHd->lastPublicKey);
  printf("resTablePackage typeIdOffset   = %p\n",resTablePackageHd->typeIdOffset);

  printf("################# Type String pool信息 #################\n");
  ResStringPool_header *typeStringPoolHd = (ResStringPool_header *)((uint8_t*)resTablePackageHd+resTablePackageHd->header.headerSize);

  printf("typeStringPoolHd type           = %p\n",typeStringPoolHd->header.type);
  printf("typeStringPoolHd chunk hd size  = %p\n",typeStringPoolHd->header.headerSize);
  printf("typeStringPoolHd chunk    size  = %p\n",typeStringPoolHd->header.size);
  printf("stringCount    = %p\n",typeStringPoolHd->stringCount);
  printf("styleCount     = %p\n",typeStringPoolHd->styleCount);
  printf("flags          = %p\n",typeStringPoolHd->flags);
  printf("stringsStart   = %p\n",typeStringPoolHd->stringsStart);
  printf("stylesStart    = %p\n",typeStringPoolHd->stylesStart);
  // header后面紧接着是两个偏移数组,之后才是数据
  // 字符串偏移数组,数组元素个数是stringCount
  const uint32_t*  mTypeEntries = (const uint32_t*)((uint8_t*)typeStringPoolHd+typeStringPoolHd->header.headerSize);
  const uint32_t*  mTypeEntryStyles;
  uint32_t         mTypeStylePoolSize;
  // 字符串池地址
  const void * mTypeString = (const void *)((const uint8_t*)typeStringPoolHd+typeStringPoolHd->stringsStart);
  const void * mTypeStyleString;
  if(typeStringPoolHd->styleCount>0){
    // 字符串样式偏移数组,数组个数是styleCount,样式数组和字符串数组一一对应
    // 也就是说在字符串偏移数组中所以为N的字符串,其样式在样式数组中的索引也为N
    mTypeEntryStyles = mTypeEntries + typeStringPoolHd->stringCount;
    mTypeStyleString = (void *)((const uint8_t*)typeStringPoolHd+typeStringPoolHd->stylesStart);
    mTypeStylePoolSize = (typeStringPoolHd->header.size-typeStringPoolHd->stylesStart)/sizeof(uint32_t);
  }

  printf("---->res type: \n");
  for(int i=0;i<typeStringPoolHd->stringCount;i++){

        const uint8_t * str = (((uint8_t*)mTypeString)+mTypeEntries[i]);
        int len = decodeLength(&str);
        //printf("-->len = %p\n",decodeLength(&str));
        //printf("-->idx = %p\n",i);
        printf("-->%s\n",(((uint8_t*)mTypeString)+mTypeEntries[i])+2);

  }
  printf("################# key String pool信息 #################\n");
  ResStringPool_header *keyStringPoolHd = (ResStringPool_header *)((uint8_t*)typeStringPoolHd+typeStringPoolHd->header.size);

  printf("keyStringPoolHd type           = %p\n",keyStringPoolHd->header.type);
  printf("keyStringPoolHd chunk hd size  = %p\n",keyStringPoolHd->header.headerSize);
  printf("keyStringPoolHd chunk    size  = %p\n",keyStringPoolHd->header.size);
  printf("stringCount    = %p\n",keyStringPoolHd->stringCount);
  printf("styleCount     = %p\n",keyStringPoolHd->styleCount);
  printf("flags          = %p\n",keyStringPoolHd->flags);
  printf("stringsStart   = %p\n",keyStringPoolHd->stringsStart);
  printf("stylesStart    = %p\n",keyStringPoolHd->stylesStart);

  // header后面紧接着是两个偏移数组,之后才是数据
  // 字符串偏移数组,数组元素个数是stringCount
  const uint32_t*  mKeyEntries = (const uint32_t*)((uint8_t*)keyStringPoolHd+keyStringPoolHd->header.headerSize);
  const uint32_t*  mKeyEntryStyles;
  uint32_t         mKeyStylePoolSize;
  // 字符串池地址
  const void * mKeyString = (const void *)((const uint8_t*)keyStringPoolHd+keyStringPoolHd->stringsStart);
  const void * mKeyStyleString;
  if(keyStringPoolHd->styleCount>0){
    // 字符串样式偏移数组,数组个数是styleCount,样式数组和字符串数组一一对应
    // 也就是说在字符串偏移数组中所以为N的字符串,其样式在样式数组中的索引也为N
    mKeyEntryStyles = mKeyEntries + keyStringPoolHd->stringCount;
    mKeyStyleString = (void *)((const uint8_t*)keyStringPoolHd+keyStringPoolHd->stylesStart);
    mKeyStylePoolSize = (keyStringPoolHd->header.size-keyStringPoolHd->stylesStart)/sizeof(uint32_t);
  }

  printf("---->res key: \n");
  for(int i=0;i<keyStringPoolHd->stringCount;i++){
      if(strcmp(((char*)mKeyString)+mKeyEntries[i]+2,"app_name")==0){
        const uint8_t * str = (((uint8_t*)mKeyString)+mKeyEntries[i]);
        int len = decodeLength(&str);
        printf("-->%s\n",(((uint8_t*)mKeyString)+mKeyEntries[i])+2);
      }
  }

结果:

################# ResTablePackage头部信息 #################
resTablePackage type           = 0x200
resTablePackage chunk hd size  = 0x120
resTablePackage chunk    size  = 0x222a4
resTablePackage id             = 0x7f
resTablePackage name           = com.godin.resourcedemo
resTablePackage typeStrings    = 0x120
resTablePackage lastPublicType = 0xc
resTablePackage keyStrings     = 0x1d0
resTablePackage lastPublicKey  = 0x3a8
resTablePackage typeIdOffset   = (nil)
################# Type String pool信息 #################
typeStringPoolHd type           = 0x1
typeStringPoolHd chunk hd size  = 0x1c
typeStringPoolHd chunk    size  = 0xb0
stringCount    = 0xc
styleCount     = (nil)
flags          = 0x100
stringsStart   = 0x4c
stylesStart    = (nil)
---->res type:
-->attr
-->drawable
-->mipmap
-->layout
-->anim
-->string
-->bool
-->dimen
-->style
-->integer
-->color
-->id
################# key String pool信息 #################
keyStringPoolHd type           = 0x1
keyStringPoolHd chunk hd size  = 0x1c
keyStringPoolHd chunk    size  = 0x7f38
stringCount    = 0x3a8
styleCount     = (nil)
flags          = 0x100
stringsStart   = 0xebc
stylesStart    = (nil)
---->res key:
-->app_name

resTablePackage type 是RES_TABLE_PACKAGE_TYPE 正确。

也打印出该资源包中的所有资源类型和找到了"app_name"这个资源项。

Type Spec与Config List

神图中的Type Spec 和 Config List 仍然归属在package数据部分.

这块内容是资源索引表中最重要的部分,但也是神图没能表达清楚的地方。这一部分也是同一个资源ID在不同配置下,找到不同资源文件的关键。所以这里先对这部分结构进行补充。

该部分的整体结构以资源类型Type分段,每段的数据结构相似,都是以ResTable_typeSpec开头,后面紧跟着一个spec数组,若干ResTable_type,每个ResTable_type之后紧跟着ResTable_entry偏移数组和若干ResTable_entry。

要注意:ResTable_typeSpec中的chunk header的size包括了其后面紧跟的spec数组所占空间大小,ResTable_type中的chunk header的size同样包括了跟随在其后面的数据大小。

然后又以一个资源type的ResTable_typeSpec开头,后面还跟这上面说的那些结构。直到所有的资源Type都存放完毕。

也就是说一个resources.arsc中的资源type有多少,就会有多少个。ResTable_typeSpec结构。

Type Spec数结构定义如下:

struct ResTable_typeSpec
{
    struct Resheader header;

    // 类型ID
    uint8_t id;

    // Must be 0.
    uint8_t res0;
    // Must be 0.
    uint16_t res1;

    // 该类型资源项的数量
    uint32_t entryCount;

    enum {
        // Additional flag indicating an entry is public.
        SPEC_PUBLIC = 0x40000000
    };
};

紧跟着这个结构后面的是一个uint32_t类型的数组,该数组元素数量为entryCount。

数组中的uint32_t数据位图表示资源的配置。而且如果这个资源是可以导出的资源,那么其SPEC_PUBLICbit位置1.其余的配置的bit位如下:

    ACONFIGURATION_MCC = 0x0001,
    ACONFIGURATION_MNC = 0x0002,
    ACONFIGURATION_LOCALE = 0x0004,
    ACONFIGURATION_TOUCHSCREEN = 0x0008,
    ACONFIGURATION_KEYBOARD = 0x0010,
    ACONFIGURATION_KEYBOARD_HIDDEN = 0x0020,
    ACONFIGURATION_NAVIGATION = 0x0040,
    ACONFIGURATION_ORIENTATION = 0x0080,
    ACONFIGURATION_DENSITY = 0x0100,
    ACONFIGURATION_SCREEN_SIZE = 0x0200,
    ACONFIGURATION_VERSION = 0x0400,
    ACONFIGURATION_SCREEN_LAYOUT = 0x0800,
    ACONFIGURATION_UI_MODE = 0x1000,
    ACONFIGURATION_SMALLEST_SCREEN_SIZE = 0x2000,
    ACONFIGURATION_LAYOUTDIR = 0x4000,
    ACONFIGURATION_SCREEN_ROUND = 0x8000,
enum {
    CONFIG_MCC = ACONFIGURATION_MCC,
    CONFIG_MNC = ACONFIGURATION_MNC,
    CONFIG_LOCALE = ACONFIGURATION_LOCALE,
    CONFIG_TOUCHSCREEN = ACONFIGURATION_TOUCHSCREEN,
    CONFIG_KEYBOARD = ACONFIGURATION_KEYBOARD,
    CONFIG_KEYBOARD_HIDDEN = ACONFIGURATION_KEYBOARD_HIDDEN,
    CONFIG_NAVIGATION = ACONFIGURATION_NAVIGATION,
    CONFIG_ORIENTATION = ACONFIGURATION_ORIENTATION,
    CONFIG_DENSITY = ACONFIGURATION_DENSITY,
    CONFIG_SCREEN_SIZE = ACONFIGURATION_SCREEN_SIZE,
    CONFIG_SMALLEST_SCREEN_SIZE = ACONFIGURATION_SMALLEST_SCREEN_SIZE,
    CONFIG_VERSION = ACONFIGURATION_VERSION,
    CONFIG_SCREEN_LAYOUT = ACONFIGURATION_SCREEN_LAYOUT,
    CONFIG_UI_MODE = ACONFIGURATION_UI_MODE,
    CONFIG_LAYOUTDIR = ACONFIGURATION_LAYOUTDIR,
    CONFIG_SCREEN_ROUND = ACONFIGURATION_SCREEN_ROUND,
};

以mipmap类型的资源(存储app icon)为例:

├── mipmap-hdpi-v4
│   └── ic_launcher.png
├── mipmap-mdpi-v4
│   └── ic_launcher.png
├── mipmap-xhdpi-v4
│   └── ic_launcher.png
├── mipmap-xxhdpi-v4
│   └── ic_launcher.png
└── mipmap-xxxhdpi-v4
    └── ic_launcher.png

mipmap类型的资源,提供了五种配置,以适应mdpi,hdpi,xhdpi,xxhdpi,xxxhpdi等不同屏幕尺寸。

mipmap类型的资源项名称为ic_launcher(注意不包括文件的后缀),对应着五个文件,app运行时,会根据当时的系统配置选择最佳的文件来显示。

以此为例的话,entryCount为1.配置数组spec元素数量也是为1了。

每种资源类型,在resources.arsc中只会存在一个ResTable_typeSpec数据结构,用来规范这个资源类型,比如这个资源类型中的资源项是否有配置(即可选资源),有哪些配置等。

紧跟在配置数组后面的是Config list,其对于的数据结构是:

struct ResTable_type
{
    struct Resheader header;

    enum {
        NO_ENTRY = 0xFFFFFFFF
    };
    // 类型ID
    uint8_t id;

    // Must be 0.
    uint8_t res0;
    // Must be 0.
    uint16_t res1;

    // 该类型资源项的数量
    uint32_t entryCount;

    // 该资源项值在 ResTable_entry数据部分的中的偏移
    uint32_t entriesStart;

    // 该资源的配置.
    ResTable_config config;
};

ResTable_type这个数据结构的数量,与配置的种类有关,比如这个例子中的ic_launcher有五中配置,那么就会存在五个这样的数据结构。

假设res/mipmap-hdpi-v4中除了ic_launcher.png外还有一个名为test.png,而其他配置的mipmap文件夹中没有这个文件,那么也还是只有五中配置,也就是五个ResTable_type数据结构。

也就是说ResTable_type数据结构的数量,由某一种类型资源中配置最多的资源项来决定,其数量等于该资源项的配置数量。

接下来希望在代码中验证mipmap类型的entryCount是1,有五个ResTable_type数据结构。

printf("#################### type spec信息 ####################\n");
 ResTable_typeSpec *resTableTypeSpecHd     = NULL;
 ResTable_type     *resTableTypeHd         = NULL;
 // 得到第一个type的ResTable_typeSpec结构
 Resheader   *chunk_hd               = (Resheader *)((uint8_t*)keyStringPoolHd+keyStringPoolHd->header.size);
 int num = 0;
 // 资源包中有几个资源类型,就有几个ResTable_typeSpec结构
 for(int i=0;i<resTablePackageHd->lastPublicType;i++){

    // ResTable_typeSpec后面有若干ResTable_type,
    // 通过 chunk header的type来区分
    // 说明是TYPE_TYPE
    while(chunk_hd->type == RES_TABLE_TYPE_TYPE){
      resTableTypeHd = (ResTable_type *)chunk_hd;
      // 统计mipmap类型的的ResTable_type结构有多少
      if(strcmp((((uint8_t*)mTypeString)+mTypeEntries[resTableTypeHd->id-1]+2),"mipmap")==0){
         num +=1;
      }
      chunk_hd             = (Resheader *)((uint8_t*)chunk_hd+chunk_hd->size);
    }
 //这是一个新的ResTable_typeSpec
 resTableTypeSpecHd = (ResTable_typeSpec*)chunk_hd;
 printf("resTableTypeSpecHd type           = %p\n",resTableTypeSpecHd->header.type);
 printf("resTableTypeSpecHd chunk hd size  = %p\n",resTableTypeSpecHd->header.headerSize);
 printf("resTableTypeSpecHd chunk    size  = %p\n",resTableTypeSpecHd->header.size);
 printf("resTableTypeSpecHd id             = %p\n",resTableTypeSpecHd->id);
 // 头部后面紧跟着数组元素个数为entryCount的uint32_t数组,每一个数组元素都用来描述一个资源项的配置差异性的。
 printf("resTableTypeSpecHd entryCount     = %p\n",resTableTypeSpecHd->entryCount);
 chunk_hd             = (Resheader *)((uint8_t*)chunk_hd+chunk_hd->size);
 }
 printf("-mipmap ResTable_type count is:%d\n",num);

结果:

#################### type spec信息 ####################
resTableTypeSpecHd type           = 0x202
resTableTypeSpecHd chunk hd size  = 0x10
resTableTypeSpecHd chunk    size  = 0x368
resTableTypeSpecHd id             = 0x1
resTableTypeSpecHd entryCount     = 0xd6
resTableTypeSpecHd type           = 0x202
resTableTypeSpecHd chunk hd size  = 0x10
resTableTypeSpecHd chunk    size  = 0x144
resTableTypeSpecHd id             = 0x2
resTableTypeSpecHd entryCount     = 0x4d
resTableTypeSpecHd type           = 0x202
resTableTypeSpecHd chunk hd size  = 0x10
resTableTypeSpecHd chunk    size  = 0x14
resTableTypeSpecHd id             = 0x3
resTableTypeSpecHd entryCount     = 0x1
.................
-mipmap ResTable_type count is:5

从结果中看出entryCount为1,mipmap类型的ResTable_type数量也为5,与其配置种类一样。

然后按照前面假设的做法,在res/mipmap-hdpi-v4放置一个名为test.png的文件。重新编译生成新的resources.arsc文件,再次运行测试(加入了ResTable_type.entryCount):

if(strcmp((((uint8_t*)mTypeString)+mTypeEntries[resTableTypeHd->id-1]+2),"mipmap")==0){
         num +=1;
         printf("mipmap ResTable_type.entryCount = %d\n",resTableTypeHd->entryCount);
..........
........
resTableTypeSpecHd type           = 0x202
resTableTypeSpecHd chunk hd size  = 0x10
resTableTypeSpecHd chunk    size  = 0x18
resTableTypeSpecHd id             = 0x3
resTableTypeSpecHd entryCount     = 0x2
mipmap ResTable_type.entryCount = 2
mipmap ResTable_type.entryCount = 2
mipmap ResTable_type.entryCount = 2
mipmap ResTable_type.entryCount = 2
mipmap ResTable_type.entryCount = 2
..........
-mipmap ResTable_type count is:5

看到了吧,虽然只是在在res/mipmap-hdpi-v4放置一个名为test.png的文件,没有在其他mipmap中放置,但是每个ResTable_type.entryCount同样由1变为了2。

也就是说ResTable_type.entryCount和ResTable_typeSpec.entryCount应该保持一致,都为该类型资源项的个数,如果一个类型的某个资源项只存在某一个配置文件夹下,那也算一个资源项。

ResTable_entry

到这里就只剩下ResTable_type结构中的ResTable_entry了。这里面存储了资源项的值和资源项的资源ID,可以理解为资源项的数据块。

每一个ResTable_type结构后面都会有若干ResTable_entry,至于ResTable_entry的数量,每个ResTable_type可能回有所不同。

还以前面res/mipmap-hdpi-v4放置一个名为test.png的文件,其它mipmap文件夹中不放为例,那么与mipmap-hdpi-v4对应的ResTable_type结构后面的ResTable_entry就有两个,其他mipmap对应的ResTable_type后面只有一个ResTable_entry。但是所有的ResTable_type结构后面跟着的ResTable_entry偏移数组元素数都是一样的,还是为2.

再来看一次ResTable_type:

struct ResTable_type
{
    struct Resheader header;

    enum {
        NO_ENTRY = 0xFFFFFFFF
    };
    // 类型ID
    uint8_t id;

    // Must be 0.
    uint8_t res0;
    // Must be 0.
    uint16_t res1;

    // 该类型资源项的数量
    uint32_t entryCount;

    // 该资源项值在 ResTable_entry数据部分的中的偏移
    uint32_t entriesStart;

    // 该资源的配置.
    ResTable_config config;
};

ResTable_type结构后面紧跟一个大小为entryCount的uint32_t数组ResTable_entry偏移数组,每一个数组元素都用来描述一个资源项数据块的偏移位置。

ResTable_type结构中的entriesStart指明了ResTable_type后面的一系列的ResTable_entry的起始位置。

这里还有一个十分重要的的地方,前面说了资源项值分为两大类:bag类值和非bag类值。

非bag类值的资源项数据块是ResTable_entry,而bag类值资源项数据块由ResTable_map_entry描述。

struct ResTable_entry
{
    // Number of bytes in this structure.
    uint16_t size;

    enum {  
        FLAG_COMPLEX = 0x0001,  
        FLAG_PUBLIC = 0x0002,
        FLAG_WEAK = 0x0004
    };
    //根据flags的不同,后面跟随的数据也不相同:bag资源和非bag资源
    //flags为1,则ResTable_entry是ResTable_map_entry
    //资源项标志位。如果是一个Bag资源项,那么FLAG_COMPLEX位就等于1,并且在ResTable_entry后面跟有一个ResTable_map数组,
    //否则的话,在ResTable_entry后面跟的是一个Res_value。如果是一个可以被引用的资源项,那么FLAG_PUBLIC位就等于1。/
    uint16_t flags;

    //对应的资源项名称 资源项名称字符串池中的偏移数组的索引
    struct ResStringPool_ref key;
};
struct ResTable_map_entry : public ResTable_entry
{
    ResTable_ref parent;
    //bag类可取值的数量
    uint32_t count;
};
struct ResStringPool_ref
{
    uint32_t index;
};

对应mipmap类型的ic_launcher来说,其值也不是事先就定义好的,所以是一个非bag类值。可以通过flags来验证。

对于非bag类值来说,ResTable_entry后面紧跟着一个Res_value结构:


/**
 * Representation of a value in a resource, supplying type
 * information.
 */
struct Res_value
{
    // Number of bytes in this structure.
    uint16_t size;

    // Always set to 0.
    uint8_t res0;
    //数据的类型,可以从上面的枚举类型中获取
    uint8_t dataType;
    // The data for this item, as interpreted according to dataType.
    // 对于bag类值来说,data就是其值
    // 对于非bag类值来说,其值是在资源项值字符串池中偏移数组的索引
    typedef uint32_t data_type;
    data_type data;

    void copyFrom_dtoh(const Res_value& src);
};

现在以代码获取ic_launcher的值来验证:

printf("#################### type spec信息 ####################\n");
  ResTable_typeSpec *resTableTypeSpecHd     = NULL;
  ResTable_type     *resTableTypeHd         = NULL;
  Resheader   *chunk_hd               = (Resheader *)((uint8_t*)keyStringPoolHd+keyStringPoolHd->header.size);
  int num = 0;
  for(int i=0;i<resTablePackageHd->lastPublicType;i++){

     // 说明是TYPE_TYPE
     while(chunk_hd->type == RES_TABLE_TYPE_TYPE){
       resTableTypeHd = (ResTable_type *)chunk_hd;

       if(strcmp((((uint8_t*)mTypeString)+mTypeEntries[resTableTypeHd->id-1]+2),"mipmap")==0){
          num +=1;
          printf("mipmap ResTable_type.entryCount = %d\n",resTableTypeHd->entryCount);
          // 得到数组
          uint32_t * su =(uint32_t *)(resTableTypeHd->header.headerSize+(uint8_t*)resTableTypeHd);

          //ResTable_entry data starts
          // 得到ResTable_entry起始位置
          uint8_t * addr = (uint8_t*)((uint8_t*)resTableTypeHd+resTableTypeHd->header.headerSize+resTableTypeHd->entryCount*sizeof(uint32_t));
         for(int i=0;i<resTableTypeHd->entryCount;i++){
           // 因为偏移数组中元素数量可能比其后面的ResTable_entry数量多,对于没有对应ResTable_entry结构的偏移数组中元素,其值为0xffffffff.
           if(su[i]!=0xffffffff){
              // 依次加上偏移得到对应的ResTable_entry
              ResTable_entry * entry = (ResTable_entry *)(addr+su[i]);
              // 如果flags是bag值类型的话,flags最低bit位为1
               printf("entry flags:0x%x\n",entry->flags);
              if(strstr(((char*)mKeyString)+mKeyEntries[entry->key.index],"ic_launcher")){
                  //ResTable_entry后面紧跟着Res_Value
                Res_value* value = (Res_value*)((uint8_t*)entry+entry->size);
                printf("value is:%s\n",((char*)mString)+mEntries[value->data]+2);
              }
           }
         }
       }
       chunk_hd             = (Resheader *)((uint8_t*)chunk_hd+chunk_hd->size);
     }

  resTableTypeSpecHd = (ResTable_typeSpec*)chunk_hd;
  printf("resTableTypeSpecHd type           = %p\n",resTableTypeSpecHd->header.type);
  printf("resTableTypeSpecHd chunk hd size  = %p\n",resTableTypeSpecHd->header.headerSize);
  printf("resTableTypeSpecHd chunk    size  = %p\n",resTableTypeSpecHd->header.size);
  printf("resTableTypeSpecHd id             = %p\n",resTableTypeSpecHd->id);
  // 头部后面紧跟着数组元素个数为entryCount的uint32_t数组,每一个数组元素都用来描述一个资源项的配置差异性的。
  printf("resTableTypeSpecHd entryCount     = %p\n",resTableTypeSpecHd->entryCount);
  chunk_hd             = (Resheader *)((uint8_t*)chunk_hd+chunk_hd->size);
  }
 printf("-mipmap ResTable_type count is:%d\n",num);

结果:

...........
resTableTypeSpecHd type           = 0x202
resTableTypeSpecHd chunk hd size  = 0x10
resTableTypeSpecHd chunk    size  = 0x18
resTableTypeSpecHd id             = 0x3
resTableTypeSpecHd entryCount     = 0x2
mipmap ResTable_type.entryCount = 2
entry flags:0
value is:res/mipmap-mdpi-v4/ic_launcher.png
mipmap ResTable_type.entryCount = 2
entry flags:0
value is:res/mipmap-hdpi-v4/ic_launcher.png
entry flags:0
mipmap ResTable_type.entryCount = 2
entry flags:0
value is:res/mipmap-xhdpi-v4/ic_launcher.png
mipmap ResTable_type.entryCount = 2
entry flags:0
value is:res/mipmap-xxhdpi-v4/ic_launcher.png
mipmap ResTable_type.entryCount = 2
entry flags:0
value is:res/mipmap-xxxhdpi-v4/ic_launcher.png
.........
-mipmap ResTable_type count is:5

那么对于bag值类型,以前面介绍bag时列举的类型:

在res/values中创建attrs.xml文件,在其中自定一个bag类型的属性资源。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="custom_orientation">
        <enum name="custom_vertical" value="100" />
        <enum name="custom_horizontal" value="200" />
    </attr>
</resources>

前面已经知道了custom_orientation是由三个bag构成的:

第一个bag:名称是“^type”,值是TYPE_ENUM(TYPE_ENUM = 1<<16)

第二个bag:名称是“custom_vertical”,值是100

第三个bag: 名称是“custom_horizontal”,值是200

另外还要给这个bag分配资源ID:

名称是“^type”的bag其分配的资源ID是attr类型的,而“custom_vertical”和“custom_horizontal”被分配到的资源ID是id类型的。

对于bag值的资源项数据块是ResTable_map_entry:

struct ResTable_map_entry : public ResTable_entry
{
    ResTable_ref parent;
    //bag类可取值的数量
    uint32_t count;
};

ResTable_map_entry后面跟着若干ResTable_map,确切来说一个Bag资源项有N个bag,那么在ResTable_map_entry后面就有N个ResTable_map。


/**
 * A single name/value mapping that is part of a complex resource
 * entry.
 */
struct ResTable_map
{
    //等于bag的资源项ID。
    ResTable_ref name;
    // 等于bag的资源项值。
    // 确切来说是Res_value.data
    Res_value value;
};

代码验证:

printf("#################### type spec信息 ####################\n");
ResTable_typeSpec *resTableTypeSpecHd     = NULL;
ResTable_type     *resTableTypeHd         = NULL;
ResChunk_header   *chunk_hd               = (ResChunk_header *)((uint8_t*)keyStringPoolHd+keyStringPoolHd->chunk_header.size);
int num = 0;
for(int i=0;i<resTablePackageHd->lastPublicType;i++){

   // 说明是TYPE_TYPE
   while(chunk_hd->type == RES_TABLE_TYPE_TYPE){
     resTableTypeHd = (ResTable_type *)chunk_hd;

     if(strcmp((((uint8_t*)mTypeString)+mTypeEntries[resTableTypeHd->id-1]+2),"mipmap")==0){
        num +=1;
        printf("mipmap ResTable_type.entryCount = %d\n",resTableTypeHd->entryCount);
        // 得到数组
        uint32_t * su =(uint32_t *)(resTableTypeHd->chunk_header.headerSize+(uint8_t*)resTableTypeHd);

        //ResTable_entry data starts
        uint8_t * addr = (uint8_t*)((uint8_t*)resTableTypeHd+resTableTypeHd->chunk_header.headerSize+resTableTypeHd->entryCount*sizeof(uint32_t));
       for(int i=0;i<resTableTypeHd->entryCount;i++){
         if(su[i]!=0xffffffff){
            ResTable_entry * entry = (ResTable_entry *)(addr+su[i]);
            printf("entry flags:0x%x\n",entry->flags);
            if(strstr(((char*)mKeyString)+mKeyEntries[entry->key.index],"ic_launcher")){
                //ResTable_entry后面紧跟着Res_Value
              Res_value* value = (Res_value*)((uint8_t*)entry+entry->size);
              printf("value is:%s\n",((char*)mString)+mEntries[value->data]+2);
            }
         }
       }
     }
     // 因为custom_orientation本质上是一个属性资源,所以以attr来判断
     if(strcmp((((uint8_t*)mTypeString)+mTypeEntries[resTableTypeHd->id-1]+2),"attr")==0){
       uint32_t * su =(uint32_t *)(resTableTypeHd->chunk_header.headerSize+(uint8_t*)resTableTypeHd);

       //ResTable_entry data starts
       uint8_t * addr = (uint8_t*)((uint8_t*)resTableTypeHd+resTableTypeHd->chunk_header.headerSize+resTableTypeHd->entryCount*sizeof(uint32_t));
       for(int i=0;i<resTableTypeHd->entryCount;i++){
         if(su[i]!=0xffffffff){
            ResTable_map_entry * map_entry = (ResTable_map_entry *)(addr+su[i]);
             if(strstr(((char*)mKeyString)+mKeyEntries[map_entry->key.index],"custom_orientation")){
                  printf("entry flags:0x%x\n",map_entry->flags);
                  // 等于本bag资源的可取值数量,也预示着后面紧跟着count个ResTable_map结构
                  printf("bag count: %d\n",map_entry->count);
                  for(int j=0;j<map_entry->count;j++){
                    ResTable_map* map_value = (ResTable_map*)((uint8_t*)map_entry+map_entry->size);
                    ResTable_map* map_value1 = map_value+j;
                    printf("bag value-->%p\n",map_value1->value.data);
                    printf("bag id-->%p\n" ,  map_value1->name);

                  }
             }
         }
       }
     }
     chunk_hd             = (ResChunk_header *)((uint8_t*)chunk_hd+chunk_hd->size);
   }

resTableTypeSpecHd = (ResTable_typeSpec*)chunk_hd;
printf("resTableTypeSpecHd type           = %p\n",resTableTypeSpecHd->chunk_header.type);
printf("resTableTypeSpecHd chunk hd size  = %p\n",resTableTypeSpecHd->chunk_header.headerSize);
printf("resTableTypeSpecHd chunk    size  = %p\n",resTableTypeSpecHd->chunk_header.size);
printf("resTableTypeSpecHd id             = %p\n",resTableTypeSpecHd->id);
// 头部后面紧跟着数组元素个数为entryCount的uint32_t数组,每一个数组元素都用来描述一个资源项的配置差异性的。
printf("resTableTypeSpecHd entryCount     = %p\n",resTableTypeSpecHd->entryCount);
chunk_hd             = (ResChunk_header *)((uint8_t*)chunk_hd+chunk_hd->size);
}
printf("-mipmap ResTable_type count is:%d\n",num);

结果:

#################### type spec信息 ####################
resTableTypeSpecHd type           = 0x202
resTableTypeSpecHd chunk hd size  = 0x10
resTableTypeSpecHd chunk    size  = 0x368
resTableTypeSpecHd id             = 0x1
resTableTypeSpecHd entryCount     = 0xd6
entry flags:0x1 //预示着是一个bag值类型
bag count: 3
bag value-->0x10000 //TYPE_ENUM = 1<<16
bag id-->0x1000000
bag value-->0xc8 //200
bag id-->0x7f0c0009
bag value-->0x64 //100
bag id-->0x7f0c000a

结果和预期都是符合的,否则使无法解析的。

这里特别指出一点,ResTable_type后面的资源项数据块可能既有ResTable_entry又有ResTable_map_entry,例如本例中资源类型attr的ResTable_type就是这种情况。

但ResTable_map_entry继承自ResTable_entry。

ResTable_entry后面跟这个的是一个Res_value,而ResTable_map_entry后面跟着的是若干ResTable_map,数量是ResTable_map_entry.count决定。每个ResTable_map中都有一个Res_value。

好了到这里位置就彻底搞清楚resources.arsc的格式了。

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

推荐阅读更多精彩内容