CHM格式

转载请标明出处:http://77blogs.com/?p=283

CHM格式为CHM头,CHM头节,内容三部分组成。

 

总体格式图:

 

初始化头包含了CHM的相关信息。格式如下:

0000:char [4]’ITSF’
0004:DWORD 3(版本号)
0008:DWORD 文件头总长度,包括标题节表和
以下数据。
000C:DWORD 1(未知)
0010:DWORD 时间戳。
被认为是一个大端的DWORD,它似乎包含
秒(MSB)和小数秒(第二个字节)。
第三个和第四个字节可能包含更多的小数
位。最后一个字节中的4个最低有效位是
不变。
0014:DWORD 语言代码:英文 0X0409,简体中文:0X0804
0018:GUID {7C01FD10-7BAA-11D0-9E0C-00A0-C922-E6EC}
0028:GUID {7C01FD11-7BAA-11D0-9E0C-00A0-C922-E6EC}

 

头节记录。它是2个条目,每个条目的长度为10个字节,具有以下格式:

0000:QWORD 头节的偏移量
0008:QWORD 头节的长度

这两个头节分别指向头节0和头节1,根据这两个头节的偏移量便可以读到两个头节。

 

内容节偏移。在版本2文件中,此数据不存在,因为内容部分紧跟在目录之后:

0000:QWORD 内容节的偏移量

 

头节0。此部分包含文件的总大小,而不是其他内容。

0000:DWORD 0X01FE(未知)
0004:DWORD 0(未知)
0008:QWORD 文件大小
0010:DWORD 0(未知)
0014:DWORD 0(未知)

 

头节1。.chm文件的核心部分:它包含的文件和信息的目录。

目录头:

0000:char [4]’ITSP’
0004:DWORD 版本号1
0008:DWORD 目录头的长度
000C:DWORD 0X0a(未知)
0010:DWORD 0X1000目录块大小
0014:DWORD 快速引用块密度,通常为2。
0018:DWORD 索引树的深度
1:没有索引,2:一层索引,以此类推。
001C:DWORD 根索引块的块号,如果没有则为-1
(尽管至少有一个文件有0,尽管没有
index chunk,可能是个bug。)
0020:DWORD 第一个PMGL(列表)块的块号
0024:DWORD 最后一个PMGL(列表)块的块号
0028:DWORD -1(未知)
002C:DWORD 目录块数(总计)
0030:DWORD Windows语言ID
0034:GUID {5D02926A-212E-11D0-9DF9-00A0C922E6EC}
0044:DWORD 0X54(这又是长度),同0X1000
0048:DWORD -1(未知)
004C:DWORD -1(未知)
0050:DWORD -1(未知)

目录头之后紧跟着目录块,有两种目录块:列表块和索引块,如果只有一个列表快,那么将没有索引块。

列表块:

0000:char [4]’PMGL’
0004:DWORD 结尾处的自由空间和/或quickref区域的长度目录块
0008:DWORD 始终为0。
000C:DWORD 读取时上一个列表块的块号
顺序目录(如果这是第一个列表块,则为-1)
0010:DWORD 读取时下一个列表块的块号
顺序目录(如果这是最后一个列表块,则为-1)
0014:目录列表条目(到quickref区域)排序方式根据文件名; 排序不区分大小写。

quickref区域是从块的末尾向后写入。对于文件中的每n个条目存在一个quickref条目,其中n被计算为1 +(1 << quickref density)。因此,对于密度= 2,n = 5。

其格式从后到前为:

Chunklen-0002:整个数据块中的项数
Chunklen-0004:从0项到n项之间的偏移量
Chunklen-0006:从0项到2n项之间的偏移量
……

目录列表条目的格式如下:

ENCINT:名字长度
UTF-8:编码的名称(UTF-8编码)
ENCINT:内容部分
ENCINT:偏移量
ENCINT:长度

偏移量是从文件解压缩之后的正文段开始计算的。长度也指解压后的长度。

目录中表示的文件有两种:用户数据和格式相关文件。与格式相关的文件具有以“::”开头的名称,用户数据文件的名称以“/”开头。

 

 

索引块:

0000:char [4]’PMGI’
0004:DWORD 目录块末尾的quickref / free(空余)区域的长度
0008:目录索引项

PMGI中的quickref区域与PMGL中的相同,当索引块的层次较高时,将不再存储数据块号而是存储下一层的索引号。

目录索引项的格式如下:

ENCINT:名字长度
UTF-8:名称(UTF-8编码)
ENCINT:以此名称开始的列表块的块号

 

内容:

在版本3中,内容通常紧跟在文件头之后,并且位于文件头表之后的DWORD指示的位置。在版本2中,内容紧跟在文件头之后。目录中的所有内容部分0位置都与该点相关。其他内容部分存储在内容部分0中。而且所有此文件夹中的正文部分的第0段都放在这个位置上。其他的正文段都在这个正文段里面。

 

名单列表文件

在内容部分0和目录中存在名为“:: DataSpace / NameList”的文件。此文件包含所有内容部分的名称。格式如下:

0000:WORD 文件长度,用文字表示
0002:WORD 文件中的条目数

每个条目:
0000:WORD 单词中的名称长度,不包括终止null
0002:WORD 。以0表示所有entry的结束。名称的编码类似于UFT-16。 段的名称目前为止只有两种,Uncompressed和MSCompressed,分别表示未压缩文件和Microsoft LZX压缩算法压缩的文件。
xxxx:WORD 0

Section_data:
对于段落编号不为0的段落,还有另一个文件:DataSpace / Storage / / Content,它存储段落的压缩信息。 因此,在解析非零段落时,需要两个步骤。 第一步是获取第,并获取段落名称。 第二步是使用段落名称查找相应的段落。

 

其余与格式相关的文件: ::DataSpace/Storage//ControlData

共0x20个字节,存储关于压缩的信息:

参考链接:

https://wenku.baidu.com/view/c2f81e21aaea998fcc220e22.html?pn=1

http://www.pythonclub.org/python-files/chm-format

shrinkResources去除无用资源原理

开启shrinkResources后,打包过程会新增task transformClassesWithShrinkResFor{variant},gradle1.5之后只需要注册一个tranform就会产生一个对应的task,查看源码发现对应的tranfrom在com.android.build.gradle.internal.transforms.ShrinkResourcesTransform,此类中调用com.android.build.gradle.tasks.ResourceUsageAnalyzer的analyze方法进行分析无用资源。

 

ResourceUsageAnalyzer:

该类的开头有这么一段话:

我翻译一下这段:

这个类负责搜索一颗gradle构建树(在合并资源,编译,压缩已经完成之后,最终的apk组装之前),这颗构建树确定了那些资源是未使用的,并将其删除。

它通过检查做到这一点:

  • 合并的清单,用于查找根资源引用(例如启动图标)
  • 合并的R类(查找分配给资源的实际整数常量)
  • 混淆映射文件(找到原始名称到混淆后短名称的一个映射)
  • 合并的资源(用于查找哪些资源引用其他资源,例如包括其他other drawables的drawable state lists,或包括其他布局的布局,或引用其他drawables的styles,或包括布局文件的菜单项等)
  • 缩小的输出类(在代码中查找实际可访问的资源引用)

所有的这些,构建了一个引用图,并基于根引用(例如来自清单与剩余代码),它计算出了在app中哪些资源是实际可达的,然后将任何无法访问的内容标记为删除。

如果文件R.type.name被引用(非最终资源引用,例如在库中),或者在代码中引用相应的int值,我们通过ASM查看输出的代码来检查这一点。有一个复杂的问题是代码可以通过Resources#getIdentifier(String,String,String),通过传递资源的名称去查查找资源,为了处理这种情况,我们使用ClassVisitor来查看是否有对特定Resources#getIdentifier方法的任何调用如果没有,很好,使用分析是完全准确的。如果我们找到一个,我们检查 所有在应用程序中的任何位置找到的字符串常量,并查看是否有看起来有关。例如,如果我们找到字符串“string / foo”或“my.pkg:string / foo”,我们将标记名为foo的字符串资源(如果有)作为可能使用的字符串资源。

 

 一:

 

 SdkConstants.FN_RESOURCE_CLASS = R.java

可以看出它找到的是R.java文件,然后调用parseResourceClass(file);方法,将R.java中的资源都标记为可达,加入表中。

 

recordMapping(mProguardMapping)

解析混淆文件,将现在的把映射存于表中

 

 

 

recordManifestUsages(mMergedManifest);

分析Manifest文件,将找到的引用标记为可达,例如:启动图标

 

AndResGuard的使用

AndResGuard是何物?

AndResGuard是一个帮助你缩小APK大小的工具,他的原理类似Java Proguard,但是只针对资源。他会将原本冗长的资源路径变短,例如将res/drawable/wechat变为r/d/a。一般用于给apk瘦身。

具体请看:

https://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.md

我们解压缩apk后可以看到这个文件

resources.arsc

这个文件是编译后的二进制资源文件,里面是id-name-value的一个map,即id与资源名称的一个映射,如:

AndResGuard。它将资源的名称进行了混淆,所以可以用它对resources.arsc进行优化,只是具体优化效果与编码方式、id数量、平均减少命名长度有关。

例如:

表一:

 

表二:

表二是经过优化后的,我们一眼就可以知道表2肯定比表1存储的字符要小,所以整个文件的大小肯定也要小一些,因此就达到了瘦身的效果。

 

总的来说它可以将res/drawable/activity_advanced_setting_for_test变为r/d/a,达到混淆与减少体积的效果,也增加了反编译后阅读代码的难度,安全性高

 

那么怎么使用呢?

第一种方式:AS集成

1、在项目的build.gradle中添加依赖如下:

 

 

2、在app下建立gradle文件and_res_guard.gradle,里面的内容如下:

其中whiteList(白名单)中指定不需要进行混淆的资源路径规则,所有使用getIdentifier访问的资源都需要加入白名单,因为使用这种方式访问的资源,在代码中写死了id,比如:
int mipmapId = getResources().getIdentifier(“ic_launcher”, “mipmap”, getPackageName());,如果不加入白名单的话会报找不到资源的错误。
 
还有一些第三方SDK,因为有些SDK的代码中也用这种方式引用到对应的资源文件,如果对其进行混淆,会导致找不到对应资源文件,出现crash,所以不能对其资源文件进行混淆。像友盟这种喜欢用反射获取资源的SDK就是一个坑(友盟的SDK就是坑王)!对于app启动图标这样的icon可以不做混淆,推荐将其放入白名单里

可以在white_list.md查看更多sdk的白名单配置,也欢迎大家PR自己的白名单

 

compressFilePattern和compressFilePattern中的通配符支持? + *


白名单机制只作用于资源的specsName,不会keep住资源的路径。如果想keep住资源原有的物理路径,可以使用mappingFile。 例如我想keep住icon所有folder,可以在mappingFile指向的文件添加:

 

注意!

  1. 如果不是对APK size有极致的需求,请不要把resource.asrc添加进compressFilePattern. (#84 #233)
  2. 对于发布于Google Play的APP,建议不要使用7Zip压缩,因为这个会导致Google Play的优化Patch算法失效. (#233)

 

3、在app的build.gradle中引入:

编译一下便可以在gradle的task任务列表里看到混淆任务

使用:

双击对应的task可以编译debug包,release包。混淆后的apk生成在build/output/apk/AndResGuard_*目录中,默认会生成4种apk,我们选择签名、压缩、对齐后的apk即可,后缀名是*_signed_7zip_aligned.apk

 

第二种方式:指令集成

 1、下载AndResGuard过程:

https://github.com/shwenzhang/AndResGuard

 

 

2、解压,可以看到这个文件,进入这个文件

 

文件夹里面有这么一些东西:

 

文件作用:

jar包:用来执行命令的时候使用

build_apk.bat :windows执行脚本

build_apk.sh:linux执行脚本,由于我使用的是windows系统,所以就。

谈谈Fragment中的onActivityResult

大家或许有遇到这个神坑,在Fragment中使用startActivityForResult能够成功,可是在Fragment中的onActivityResult却无法被调用。一不注意就让人一夜愁白了头。苦经探索(当然包括亲爱的百度和谷歌),终于总结出了一些规律。

在Fragment中使用startActivityForResult之后,onActivityResult的调用是从activity中开始的(即会先调用activity中的onActivityResult)。

一.只嵌套了一层Fragment(比如activity中使用了viewPager,viewPager中添加了几个Fragment)

在这种情况下要注意几个点:

1.在Fragment中使用startActivityForResult的时候,不要使用getActivity().startActivityForResult,而是应该直接使startActivityForResult()。

2.如果activity中重写了onActivityResult,那么activity中的onActivityResult一定要加上super.onActivityResult(requestCode, resultCode, data)。

如果违反了上面两种情况,那么onActivityResult只能够传递到activity中的,无法传递到Fragment中的。

没有违反上面两种情况的前提下,可以直接在Fragment中使用startActivityForResult和onActivityResult,和在activity中使用的一样。

 

二.嵌套多层Fragment(比如activity中使用了viewPager,viewPager中添加了几个Fragment,即第一层Fragment。其中一个Fragment又使用了一个ViewPager,这个ViewPager又加入了几个Fragment,即第二层Fragment)

在这种情况下activity中的onActivityResult调用无法传到第二层Fragment中。自己动手丰衣足食,我们只有手动传了。

让activity继承上面的MyBaseFragmentActivity 。这样就手动把onActivityResult的调用传递到每一层的每一个Fragment里面了。然后在每一个Fragment里面重写onActivityResult就行了,然后根据requestCode和resultCode来分别执行对应事件。

当然,不管是哪个Fragment调用startActivityForResult,onActivityResult的调用都会传递到每个Fragment中。

同样需要注意:

1.如果activity中重写了onActivityResult,那么activity中的onActivityResult一定要加上super.onActivityResult(requestCode, resultCode, data),否则也无法传进Fragment中。

 

不过有点值得注意的是:

在所有的Fragment中若是直接使用startActivityForResult(),那么传到activity中的onActivityResult中的requestCode就会不对,resultCode是对的,当然,这样的话传到每个Fragment中的onActivityResult的requestCode也是不对的(这里大家留意一下,评论里有人说传到Fragment中的是对的)。若是用getActivity().startActivityForResult,则传出来的requestCode和rusultCode就都是对的。

当然若是直接按返回键返回,那么会自动给一个resultCode,这个resultCode和我们设定的是不一样的,因此在重写onActivityResult的时候需要判定resultCode。

由于我们是手动将activity中的onActivityResult的调用传进每一层的每一个Fragment中去的,所以不管在Fragment中是使用startActivityForResult(),还是使用getActivity().startActivityForResult,都没关系,使用getActivity().startActivityForResult不会出现activity中onActivityResult中的调用无法传进去的情况。会影响的只有requestCode。

转载请标明出处,维权必究:http://77blogs.com/?p=11