深入浅出Android Chrome浏览器源代码 - 书签功能

写在前面

很久没更博了,自从16年国庆节后开始着手书签助手APP的开发,更新了二十余个版本后,正逢年底,工作一下子紧张起来,暂时将APP的开发工作搁置了近一个月。

问题描述

最近几天有了时间来继续开发,计划17年的第一版更新,完成23款浏览器的书签合并到Android Chrome浏览器,尽最大努力,保持合并后的Chrome浏览器不出现FC、ANR等问题,最好能支持使用Android Google Accout同步到云端。而书签功能作为浏览器的底层基础,很多上层功能需要以此为基点,什么样的方案才能避免蝴蝶效应引起牵一发而动全身呢?

可行性分析

前提说明:以下内容,大量使用fastjson api,请提前温习。

Bookmarks文件

我们都知道,Android Chrome浏览器的书签数据,存储在“/data/data/com.android.chrome/app_chrome/Default/Bookmarks”文件中,下文简称“Bookmarks文件”。文件大小视书签数据量而定,300条书签,接近250KB,将这个文件拷贝出来,简单的使用EditPlus软件打开。一眼看出是经 pretty formatter 处理的json文件,序列化时,为方便阅读,适当加上换行和空格,以体现视觉层次感。

稍加留意,可以知道Bookmarks文件中,书签与文件夹的关系被处理为一棵树形结构。事实上,我在开发Chrome浏览器提取书签,合并到Via浏览器时,就是读取了Bookmarks文件,使用递归将每条书签的文件夹完整路径提取出来。但这毕竟是read-only操作,不涉及到写入。

那么是不是将书签数据,按照既定的json格式组织起来,写入到Bookmarks文件就可以了呢?先暂且认为是可以的。那么如何安全的写入呢?我考虑到有如下几个关键点:

1.写入时,确保Chrome浏览器安全退出,避免多进程同时写,结果相互覆盖。由于GAPPS很有可能同Chrome一起安装,需要写入时监测GMS,避免唤醒Chrome浏览器,比如定期唤醒Chrome同步浏览器数据到云端。

2.读取时,确保读取完整已有书签数据,避免在回写时数据丢失。

3.写入时,将已有Bookmarks文件做一次backup,避免程序bug或崩溃(OS Crash)时,可以回退到写入前(restore)。

针对第1点,分为两种情况。

1.如果没有安装其他的GAPPS套件,我们可以在书签合并之前,提示用户保存好浏览数据,然后手动退出(可以是强制停止),也可以由书签助手APP利用Root权限强行杀进程。

2.如果有安装其他的GAPPS套件,这种情况就复杂了,在不卸载的前提下,是否可以调用相关API对GAPPS套件进行完整启动和停止,以避免唤醒Chrome浏览器,可行性是未知的,理论上Google不会提供这样的API。以借用Root权限杀进程(kill -9 ?)的方式强制退出,会引发数据丢失的问题,何况完成书签合并后,还需要将GAPPS启动回来,那将有一段时间的运行中断,例如GMS通知等重要业务将没办法使用。强制退出的GAPPS会在书签合并中途重新启动吗?broadcast receiver 接收到系统event而启动主进程,这是可能的。这个方案目前问题太多,几乎不可行。也许有其他方式可以解决,先搁置这个问题,思考在可以解决的前提下,第2点和第3点该如何应对?

针对第2点,在下一小节里说明。

针对第3点,目前的程序逻辑,已经有了backup策略,但是restore是缺失的,这个作为健壮性设计在下一版本迭代中完成。

读取完整书签数据

针对上文中的第2点,确保读取完整已有书签数据。Bookmarks文件作为json格式的文件,完整读取,是需要将所有key-value对 都转化为java object,并保持好key-value对 之间的逻辑依赖关系,比如名为folderA的文件夹,共有5个field,包含了2条书签,分别是bookmarkB和bookmarkC,而bookmarkB有12个field,bookmarkC有6个field。别惊讶,下文中我将说明为何field数量出现不一致,这是真实存在的。想要完整读取,就需要这种描述信息由json格式,转化为java object。

在之前的读取时,只需要针对基本的4个field进行解析就可以:name,url,type,children。而且这些field的排列规则是非常固定的。我们可以抽象出来,组成Node对象,更加方便后续的程序逻辑解析。

那为了读取完整的书签数据,这一点也需要调整,整理出全新的Node对象,包含9个field。可以留意到,为了保持序列化的写入顺序,加入了注解:“@JSONField(ordinal = 5)”,这很重要,最小化影响Chrome浏览器功能,经观察Bookmarks文件中的key是以当前深度下的key字母正向顺序写入的。

参差不齐的JSON

参差不齐的JSON是指统一深度下的节点对象,出现field数量变化。这一点体现在Node对象的meta_info field。

1
2
3
4
@Setter
@Getter
@JSONField(ordinal = 5)
private MetaInfo meta_info;

经过仔细观察,遍历一个比较大和复杂的Bookmarks文件,我们发现field数量变化也出现规律性。1.MetaInfo的自身field交叉,field数量不相同的MetaInfo对象,存在交叉关系,部分或完全重叠。2.有的Node对象不存在meta_info field,例如新添加的书签数据,这点在下文中说明。

MetaInfo也是一个对象,整理出可能的field后,得到以下pojo。

你可以看出来,又有了新的注解:“@JSONField(ordinal = 10, name = synonymsPrefix + “userEdit”)”,由于java对于field的定义规范,不允许“.”。我们将key与field name的映射关系,做了特别处理。

仅MetaInfo对象就有11个field,数量不是问题,关键是无法确定,是否完整罗列了所有field ?

不能接受数据丢失,差之毫厘谬以千里。

JSON.parseObject

在上一小节,除了GAPPS的问题外,还遇到了meta_info对象不确定的问题,接下来solve it。

有没有可能在不定义pojo或不完整定义pojo的前提下,解析json string呢?在此基础上经过修改,还能在序列化为string不丢失数据呢?

这有点像在原有的json string上,再合适的位置,打开一道口子,放进新的书签数据段,然后重新缝合起来,变成一个全新的json string,就像是给计算机多加一个内存条,增加的不影响已有的,并且能协作一起运行。

多方尝试发现是可以的,整理出如下代码段,可以完成在“bookmark_bar”节点下,添加一个书签,进行序列化时,没有数据丢失,原有的格式被完整保留。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//将Bookmarks文件,完整读取到内存
String content = FileUtil.read(tmpFilePath);
if (content == null || content.isEmpty()) {
LogHelper.e(TAG + ".appendBookmark:content is null or empty.");
throw new ConverterException(ContextUtil.buildAppendBookmarksErrorMessage(this.getName()));
}
//解析为JSONObject,保持原有key排列顺序
JSONObject top = JSON.parseObject(content, Feature.OrderedField);
if (top == null) {
LogHelper.e(TAG + ".appendBookmark:top is null.");
throw new ConverterException(ContextUtil.buildAppendBookmarksErrorMessage(this.getName()));
}
//获取名为roots的子节点
JSONObject roots = top.getJSONObject("roots");
if (roots == null) {
LogHelper.e(TAG + ".appendBookmark:roots is null.");
throw new ConverterException(ContextUtil.buildAppendBookmarksErrorMessage(this.getName()));
}
//获取名为bookmark_bar的子节点
JSONObject bookmarkBar = roots.getJSONObject("bookmark_bar");
if (bookmarkBar == null) {
LogHelper.e(TAG + ".appendBookmark:bookmarkBar is null.");
throw new ConverterException(ContextUtil.buildAppendBookmarksErrorMessage(this.getName()));
}
//获取名为children的子节点
JSONArray children = bookmarkBar.getJSONArray("children");
if (children == null) {
LogHelper.e(TAG + ".appendBookmark:children is null.");
throw new ConverterException(ContextUtil.buildAppendBookmarksErrorMessage(this.getName()));
}
LogHelper.v("children origin size:" + children.size());
Node node = new Node();
node.setName("test01");
node.setUrl("http://www.github.com");
node.setType(MetaData.FOLDER_TYPE_DEFAULT_NAME);
node.setId("99999999999999999");
//添加一个书签,名为:“test01”
children.add(node);
//覆盖原有的children节点
JsonUtil.replace(bookmarkBar, "children", children);
//关于checksum的处理下文说明
JsonUtil.update(top, "checksum", "");
//获取添加书签后的完整json数据
String finalContent = JsonUtil.toJson(top, true);
if (finalContent == null) {
LogHelper.e(TAG + ".appendBookmark:finalContent is null.");
throw new ConverterException(ContextUtil.buildAppendBookmarksErrorMessage(this.getName()));
}

效果图:

字段的分析

按上一小节的代码段逻辑,我们对原有的书签json string,是不做修改的,这样是否可以被chrome浏览器识别为正常完整的新Bookmarks文件呢?经模拟器(Genymotion:V2.8.1)环境测试(完整安装GAPPS),是不可行的。chrome浏览器的书签页面,不显示新加的书签“test01”,但在Bookmarks文件中确实是存在的,这说明chrome浏览器不认可这一数据段。

经查证,修改完Bookmarks文件后,恢复启动chrome浏览器,新书签“test01”数据段被删除,这说明chrome浏览器在启动时,有verify机制和rollback机制,检测到不正常的书签数据hack,自动恢复到上一个savepoint,那这些机制的运作细节是怎样的呢?如何应对?

同时发现,chrome浏览器页面中,无法再添加新书签了,提示:“无法添加书签。”,这很可能是在异常修改后,chrome浏览器的一种自我防御机制,还只是猜想。

也许完整分析Bookmarks文件的字段会有一些灵感?在字面上来分析业务含义,很容易知道大部分字段的功能性和算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
String id;//【猜测】唯一主键,数据来源不明,经观察,存在自增特点,步长不确定。同级路径从上到下,存在顺序性,值大于内层目录id值
String name;//书签或者文件夹名称
String url;//书签的url,文件夹无此field
String type;//类型,区分是书签还是文件夹,固定枚举值
String date_added;//数据被添加时的时间戳,固定存在
String date_modified;//数据被修改时的时间戳,不一定存在。如果修改,则该书签或文件夹新增该field,同时上级路径也会有该field
String checksum;//【猜测】roots节点才有,书签完整性和有效性校验值,具体算法不明。
Integer version;//【猜测】roots节点才有,版本?具体产生算法不明。
String sync_transaction_version;//【猜测】chrome浏览器的书签想要同步到云端,就需要这个字段控制版本
Object meta_info;//【猜测】观察仅书签才有,可能记录了书签的元数据。
Object[] children;//文件夹才有,描述该文件夹所包含的书签数据

未知的字段,大概占了50%左右,仅仅从字面含义观察,已经不能满足分析要求了。

可以看出来,chrome浏览器对于运行稳定性以及健壮性,有很多的考虑,如何不破坏这些机制,完成书签添加呢?

黑盒测试验证

在静态分析meta_info field 阶段时,意外的留意到,不是每个字段都会出现,呈现不整齐的现象。那在实际的chrome浏览器中,新增书签或文件夹时是否也存在呢?如果存在,意味着有些字段的产生规则和业务含义可以不必理会,这需要在模拟器环境来验证猜想。

新书签

在模拟器环境,完整GAPPS套件安装,新安装最新版chrome浏览器,完成Google account 同步【FQ】,包括浏览器信息同步。此时正常完整退出chrome浏览器,保存一个Bookmarks文件副本。打开chrome浏览器,在“书签栏”路径下,新加一个书签,名为“test02”(新书签,之前是不存在的),正常完整的退出chrome浏览器,回过头来再看Bookmarks文件的变化。

添加书签:

Bookmarks文件变化:

第1部分:

第2部分:

我们可以发现,新的书签,不仅仅影响局部数据,像原有的date_modified、checksum等值也会跟随变化。好消息是,观察新书签数据结构,发现新书签数据段,字段结构并不复杂,大部分字段可以由外部程序提供,但还是存在疑点:“id”、“sync_transaction_version”、“checksum”等产生规则是什么呢?

新文件夹

已经知道新书签的添加,对Bookmarks文件的影响,继续来观察新文件夹对Bookmarks文件的影响。按照上一节的流程添加,只是添加时改为添加文件夹,名为“新建文件夹10”,同时为这个新文件夹,加入一个书签,名为:“test03”。

添加文件夹:

Bookmarks文件变化:

第1部分:

第2部分:

同上一小节,“id”、“sync_transaction_version”、“checksum”等字段的产生规则还不明确。

SyncData.sqlite3 不确定问题

在观察新加书签和新加文件夹的时候,观察的重点对象是Bookmarks文件。经过上文的观察结果,有一些疑问不能仅仅通过Bookmarks文件来得到答案。在观察Bookmarks文件的同时,我们使用文件系统自身的watch机制,来感知其他数据文件的变化,得到了另一重点对象:SyncData.sqlite3,它的路径位于:“/data/data/com.android.chrome/app_chrome/Default/Sync Data/SyncData.sqlite3”,经分析,这是一个典型的sqlite3数据库。

将这个文件拷贝出来,用Navicat Premium(11.1.12)打开,果然是不需要密码,可以直接打开查看。

5张表的结构也出来了

1
2
3
4
5
6
7
8
9
CREATE TABLE deleted_metas (metahandle bigint primary key ON CONFLICT FAIL,base_version bigint default -1,server_version bigint default 0,local_external_id bigint default 0,transaction_version bigint default 0,mtime bigint default 0,server_mtime bigint default 0,ctime bigint default 0,server_ctime bigint default 0,id varchar(255) default "r",parent_id varchar(255) default "r",server_parent_id varchar(255) default "r",is_unsynced bit default 0,is_unapplied_update bit default 0,is_del bit default 0,is_dir bit default 0,server_is_dir bit default 0,server_is_del bit default 0,non_unique_name varchar,server_non_unique_name varchar(255),unique_server_tag varchar,unique_client_tag varchar,unique_bookmark_tag varchar,specifics blob,server_specifics blob,base_server_specifics blob,server_unique_position blob,unique_position blob,attachment_metadata blob,server_attachment_metadata blob);
CREATE TABLE metas(metahandle bigint primary key ON CONFLICT FAIL,base_version bigint default -1,server_version bigint default 0,local_external_id bigint default 0,transaction_version bigint default 0,mtime bigint default 0,server_mtime bigint default 0,ctime bigint default 0,server_ctime bigint default 0,id varchar(255) default "r",parent_id varchar(255) default "r",server_parent_id varchar(255) default "r",is_unsynced bit default 0,is_unapplied_update bit default 0,is_del bit default 0,is_dir bit default 0,server_is_dir bit default 0,server_is_del bit default 0,non_unique_name varchar,server_non_unique_name varchar(255),unique_server_tag varchar,unique_client_tag varchar,unique_bookmark_tag varchar,specifics blob,server_specifics blob,base_server_specifics blob,server_unique_position blob,unique_position blob,attachment_metadata blob,server_attachment_metadata blob);
CREATE TABLE models (model_id BLOB primary key, progress_marker BLOB, transaction_version BIGINT default 0,context BLOB);
CREATE TABLE share_info (id TEXT primary key, name TEXT, store_birthday TEXT, cache_guid TEXT, bag_of_chips BLOB);
CREATE TABLE share_version (id VARCHAR(128) primary key, data INT);

经过对比分析,metas表,是主要观察对象,这个表的数据量1000+,保存了多处登录的所有设备上:浏览历史记录、当前打开的标签页、书签信息。当数据
新加书签“test02”的对应记录:

新加文件夹“test03”的对应记录:

很明显的,metas.local_external_id 字段,对应着Bookmarks文件中,id字段;metas.transaction_version 字段,对应着Bookmarks文件中,sync_transaction_version字段,checksum字段暂时没有找到映射关系。

还发现,metas表中,有10多个字段尚有疑问,

1
2
3
4
“metahandle”,“local_external_id”,“transaction_version”,
“unique_bookmark_tag”,“specifics”,“server_specifics”,
“server_unique_position”,“unique_position”,“id”,“parent_id”,
“server_parent_id”

很容易从字面上猜测到业务含义,但这些字段的产生算法是怎么样的呢?

还好在上一小节中的:“id”、“sync_transaction_version”这两个字段在SyncData.sqlite3文件中得到印证。总算没有白费功夫,但是“checksum”是什么呢?猜测的是否有误呢?

checksum字段的算法,经google找到一小部分非官方说明:http://stackoverflow.com/questions/11308603/logic-behind-creating-bookmark-checksum-in-google-chrome。关键的代码段在chromium开源项目中:https://chromium.googlesource.com/chromium/chromium/+/20f8aa123f98b2bcb0d346af0d78ad7a8ddea5d0/chrome/browser/bookmarks/bookmark_codec.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Value* BookmarkCodec::Encode(BookmarkModel* model) {
return Encode(model->bookmark_bar_node(),
model->other_node(),
model->mobile_node(),
model->root_node()->meta_info_str());
}
Value* BookmarkCodec::Encode(const BookmarkNode* bookmark_bar_node,
const BookmarkNode* other_folder_node,
const BookmarkNode* mobile_folder_node,
const std::string& model_meta_info) {
ids_reassigned_ = false;
InitializeChecksum();
DictionaryValue* roots = new DictionaryValue();
roots->Set(kRootFolderNameKey, EncodeNode(bookmark_bar_node));
roots->Set(kOtherBookmarkFolderNameKey, EncodeNode(other_folder_node));
roots->Set(kMobileBookmarkFolderNameKey, EncodeNode(mobile_folder_node));
if (!model_meta_info.empty())
roots->SetString(kMetaInfo, model_meta_info);
DictionaryValue* main = new DictionaryValue();
main->SetInteger(kVersionKey, kCurrentVersion);
FinalizeChecksum();
// We are going to store the computed checksum. So set stored checksum to be
// the same as computed checksum.
stored_checksum_ = computed_checksum_;
main->Set(kChecksumKey, new base::StringValue(computed_checksum_));
main->Set(kRootsKey, roots);
return main;
}
Value* BookmarkCodec::EncodeNode(const BookmarkNode* node) {
DictionaryValue* value = new DictionaryValue();
std::string id = base::Int64ToString(node->id());
value->SetString(kIdKey, id);
const string16& title = node->GetTitle();
value->SetString(kNameKey, title);
value->SetString(kDateAddedKey,
base::Int64ToString(node->date_added().ToInternalValue()));
if (node->is_url()) {
value->SetString(kTypeKey, kTypeURL);
std::string url = node->url().possibly_invalid_spec();
value->SetString(kURLKey, url);
UpdateChecksumWithUrlNode(id, title, url);
} else {
value->SetString(kTypeKey, kTypeFolder);
value->SetString(kDateModifiedKey,
base::Int64ToString(node->date_folder_modified().
ToInternalValue()));
UpdateChecksumWithFolderNode(id, title);
ListValue* child_values = new ListValue();
value->Set(kChildrenKey, child_values);
for (int i = 0; i < node->child_count(); ++i)
child_values->Append(EncodeNode(node->GetChild(i)));
}
if (!node->meta_info_str().empty())
value->SetString(kMetaInfo, node->meta_info_str());
return value;
}
void BookmarkCodec::FinalizeChecksum() {
base::MD5Digest digest;
base::MD5Final(&digest, &md5_context_);
computed_checksum_ = base::MD5DigestToBase16(digest);
}

进行语义逻辑分析,可以看出来,Encode方法将bookmark_bar_node对象,other_node对象,mobile_node对象,meta_info_str对象作为入参,多维度进行摘要算法计算,字段结果就是checksum值。也就是说,Bookmarks文件中,任意书签的任意字段信息发生改变,checksum跟随变化,意思是所有书签数据的hashTag。摘要算法的细节还不清楚,在下文中说明。

思路转换

真是一波未平一波又起,chrome浏览器的复杂性远远超出预期,看来简单的通过分析android data目录文件,没办法满足需求了。换个角度思考,chrome浏览器的复杂性和稳定性是同时提供的,这意味着内部的复杂度被chrome上层程序掩盖。还知道chrome pc版是支持extension的,这是一大亮点,意味着pc版开放了一些api给extension调用,辅助chrome运行。那有没有可能,由android chrome浏览器提供一个书签管理api,开放给书签助手APP来调用呢?

content provider

通过一番Google搜索(通过google解决google的问题?),找到了可能的解决办法:Add bookmark to chrome browser on Android。原理是android chrome浏览器开放了一对权限来管理书签和历史浏览:com.android.browser.permission.READ_HISTORY_BOOKMARKS,com.android.browser.permission.WRITE_HISTORY_BOOKMARKS,app在获取这些权限后,可以通过content provider的方式,将书签数据组织为chrome浏览器需要的格式,提交给chrome浏览器来存储。

“com.android.browser.permission.READ_HISTORY_BOOKMARKS”,该权限被用作查询chrome浏览器的书签和历史浏览记录,可以按条件过滤,这个权限在6..0及以上的android版本已经被删除,意味着不再可用。

这2个的api由android chrome浏览器官方提供,可行性高了不少,附带的GAPPS问题和同步到云端的问题,也屏蔽了解构底层Bookmarks文件和SyncData.sqlite3文件。问题可以就此解决吗?

solution demo

整理出添加书签的demo代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void test(){
ContentValues values = new ContentValues();
values.put(BookmarkColumns.TITLE, "title");
values.put(BookmarkColumns.URL, "http://ss.com");
values.put(BookmarkColumns.BOOKMARK, 1);
values.put(BookmarkColumns.CREATED, 0);
values.put(BookmarkColumns.DATE, 0);
values.put("parentId", 3); // just for Chrome
getContentResolver().insert(Uri.parse("content://com.android.chrome.ChromeBrowserProvider/bookmarks"), values);
}
public static class BookmarkColumns implements BaseColumns {
public static final String URL = "url";
public static final String VISITS = "visits";
public static final String DATE = "date";
public static final String BOOKMARK = "bookmark";
public static final String TITLE = "title";
public static final String CREATED = "created";
public static final String FAVICON = "favicon";
public static final String THUMBNAIL = "thumbnail";
public static final String TOUCH_ICON = "touch_icon";
public static final String USER_ENTERED = "user_entered";
}

添加文件夹的api也是类似的写法吗?google没有找到印证。

chromium源代码

想起来chrome是基于chromium开源项目的,那content provider这部分的代码也是开源的吗?找到chromium源代码online page:https://www.chromium.org/developers/how-tos/get-the-code,选择Android版,先不下载,翻阅相关代码段:https://chromium.googlesource.com/chromium/src/+/master/chrome/android/java/src/org/chromium/chrome/browser/provider/ChromeBrowserProvider.java,405行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public Uri insert(Uri uri, ContentValues values) {
if (!canHandleContentProviderApiCall() || !hasWriteAccess()) return null;
int match = mUriMatcher.match(uri);
Uri res = null;
long id;
switch (match) {
case URI_MATCH_BOOKMARKS:
id = addBookmark(values);
if (id == INVALID_BOOKMARK_ID) return null;
break;
case URL_MATCH_API_BOOKMARK_CONTENT:
values.put(BookmarkColumns.BOOKMARK, 1);
//$FALL-THROUGH$
case URL_MATCH_API_BOOKMARK:
case URL_MATCH_API_HISTORY_CONTENT:
id = addBookmarkFromAPI(values);
if (id == INVALID_CONTENT_PROVIDER_ID) return null;
break;
case URL_MATCH_API_SEARCHES:
id = addSearchTermFromAPI(values);
if (id == INVALID_CONTENT_PROVIDER_ID) return null;
break;
default:
throw new IllegalArgumentException(TAG + ": insert - unknown URL " + uri);
}
res = ContentUris.withAppendedId(uri, id);
notifyChange(res);
return res;
}

正好对应了content provider部分的对接部分,使用git将chromium android源代码clone下来,因为网速影响,只拉取master分支的最后一个commit版本,history change都先不下载了,约2.5GB。

1
git clone https://chromium.googlesource.com/chromium/src -b master --depth=1

android源代码,使用Android Studio浏览应该是最好的选择,因为机器配置(4核奔腾+8GB)不够强,对如此浩瀚的代码仓库,编译索引起来力不从心,一度死机。尝试缩减源代码范围,只编译“\chromium\src\chrome\android”部分,约30MB,很快就打开了,但是依赖包没有编译,无法正常浏览。

针对如此大规模的源代码,有更好的源代码浏览工具吗?可能混杂的编程语言有python、java、c系列。

Source Insight

Source Insight.它是一个面向项目开发的程序编辑器和代码浏览器。Source Insight能分析你的源代码并在你工作的同时动态维护它自己的符号数据库,并自动为你显示有用的上下文信息。 它的强大之处在于不仅仅是可编辑的源代码,还包括对于代码中的变量和类进行关联和查找。比如java语言,你可以清晰的看到一个类中的成员变量以及方法,而且source insight 还提供了类的预览,比如源码中有一个类,那么你可以解转到那个类里查看源码。

Source Insight支持的编程语言列表:

ChromeBrowserProvider.java

上一小节,已经提到ChromeBrowserProvider.java文件,这一节的就此着手,抽丝剥茧,一路挖下去吧!

当入参uri,是URI_MATCH_BOOKMARKS时,进入到addBookmark(values)代码段,URI_MATCH_BOOKMARKS定义:private static final int URI_MATCH_BOOKMARKS = 0;

addBookmark方法

接下来看addBookmark方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private long addBookmark(ContentValues values) {
String url = values.getAsString(BookmarkColumns.URL);
String title = values.getAsString(BookmarkColumns.TITLE);
boolean isFolder = false;
if (values.containsKey(BOOKMARK_IS_FOLDER_PARAM)) {
isFolder = values.getAsBoolean(BOOKMARK_IS_FOLDER_PARAM);
}
long parentId = INVALID_BOOKMARK_ID;
if (values.containsKey(BOOKMARK_PARENT_ID_PARAM)) {
parentId = values.getAsLong(BOOKMARK_PARENT_ID_PARAM);
}
long id = nativeAddBookmark(mNativeChromeBrowserProvider, url, title, isFolder, parentId);
if (id == INVALID_BOOKMARK_ID) return id;
if (isFolder) {
updateLastModifiedBookmarkFolder(id);
} else {
updateLastModifiedBookmarkFolder(parentId);
}
return id;
}

第2、3两行,分别获取传入的URL和标题字段值,不做多说。第4至11行,验证是否为文件夹,以及获取上层文件夹的id值,主要是为了第15至19行,进行文件夹的最后更新时间做计算,猜测对应前文的date_modified字段,进行级联修改。BOOKMARK_IS_FOLDER_PARAM与BOOKMARK_PARENT_ID_PARAM的定义如下。

1
2
3
4
5
/** The parameter used to specify whether this is a bookmark folder. */
public static final String BOOKMARK_IS_FOLDER_PARAM = "isFolder";
/** The parameter used to specify a bookmark parent ID in ContentValues. */
public static final String BOOKMARK_PARENT_ID_PARAM = "parentId";

可以很明显的感受到Google对Java代码的规范性,类名采用大驼峰;方法名、参数名、局部变量、成员变量名,采用小驼峰;常量名采用全大写蛇形,这里有一份完整的:Google Java编程风格指南

nativeAddBookmark方法

上文中的第12行代码,调用nativeAddBookmark方法后,返回long类型的id字段。这个方法对应的源码段是:

1
2
private native long nativeAddBookmark(long nativeChromeBrowserProvider,
String url, String title, boolean isFolder, long parentId);

方法定义为native,从这一步开始,书签添加的逻辑实现,从Java层交棒给了C层。第一个想到的是在同级目录下会有一个:org_chromium_chrome_browser_provider_nativeAddBookmark.h文件,但是没找到。

在“\chromium\src\chrome\android\”目录下,并没有关于jni的代码实现,我们使用EditPlus,对整个chromium src层级目录“\chromium\src\”,进行递归搜索“nativeAddBookmark”关键字,范围限定为:“.cc .h”。耗时 1 分 14 秒完成搜索,但一无所获,难道jni部分并未开源吗?或者在其他路径下开源,未囊括到chromium项目?前一种有可能,后一种可能性很低。

我们都知道,JNI函数的注册有两种方法,一种是静态方法,需要用javah为每个声明了native函数的java类编译出的class文件生成一个头文件,另一种是动态注册,通过数据结构保存关联关系实现注册。搜索“nativeAddBookmark”关键字进行jni函数查找的方式,是以静态方法为前提的。那目标jni函数可能是采用动态的方式注册的吗?

在搜索jni函数的过程中,发现android与ios(”\chromium\src\ios\chrome)共用了同一jni函数,不知道pc版是不是也会共用?一方面性能相比上层代码高,另一方面OS底层都对c代码有支持,产生了一定的跨平台性。

content provider事务性思考

——————-还没写完,持续更新中。2017年1月16日10:01:51——————-

解决方案

Reference

思路

Source Insight 使用

Chromium源代码结构

助攻

Tips

Genymotion

virtual device 启动过慢

有时候virtual device 启动过慢,花了10分钟,还停留在android黑底白字阶段,甚至启动失败。这可能是genymotion自身进程冲突,现在强制退出启动,退出其他genymotion软件,在windows任务管理器,进程列表中,找到adb、vbox相关进程,强制结束。再次启动,发现快了很多!

Linux

busybox

BusyBox 是一个集成了一百多个最常用linux命令和工具的软件。BusyBox 包含了一些简单的工具,例如ls、 cat 和 echo等等,还包含了一些更大、更复杂的工具,例如 grep、find、mount 以及 telnet。有些人将 BusyBox 称为 Linux 工具里的瑞士军刀。简单的说BusyBox就好像是个大工具箱,它集成压缩了 Linux 的许多工具和命令。也包含了 Android 系统的自带的shell。

为什么要在Android中加入busybox?

用过adb shell的人应该知道,在默认情况下,adb shell下是不能用clear,grep, find,vi等指令的,甚至连Tab链自动补全功能都不能用,对于已经习惯了使用这些指令的码农们来说,这是件比较悲摧的事情。幸运地是,我们有了busybox!

Genymotion环境下,使用DOS-ADB安装Busybox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
download link :https://busybox.net/downloads/binaries/1.21.1/busybox-armv7l
D:\Android\sdk\platform-tools>adb shell
root@vbox86p:/ # cd /data/
root@vbox86p:/data # mkdir busybox
root@vbox86p:/data # exit
D:\Android\sdk\platform-tools>adb push busybox /data/busybox/
[100%] /data/busybox/busybox
D:\Android\sdk\platform-tools>adb shell
root@vbox86p:/ # chmod 777 /data/busybox/busybox
root@vbox86p:/ # /data/busybox/busybox
BusyBox v1.26.2 (2017-01-11 08:43:16 UTC) multi-call binary.
BusyBox is copyrighted by many authors between 1998-2015.
Licensed under GPLv2. See source distribution for detailed
copyright notices.
Usage: busybox [function [arguments]...]
or: busybox --list[-full]
or: busybox --install [-s] [DIR]
or: function [arguments]...
BusyBox is a multi-call binary that combines many common Unix
utilities into a single executable. Most people will create a
link to busybox for each function they wish to use and BusyBox
will act like whatever it was invoked as.
Currently defined functions:
[, [[, acpid, add-shell, addgroup, adduser, adjtimex, arp, arping, ash,
awk, base64, basename, beep, blkdiscard, blkid, blockdev, bootchartd,
brctl, bunzip2, bzcat, bzip2, cal, cat, catv, chat, chattr, chgrp,
chmod, chown, chpasswd, chpst, chroot, chrt, chvt, cksum, clear, cmp,
comm, conspy, cp, cpio, crond, crontab, cryptpw, cttyhack, cut, date,
dc, dd, deallocvt, delgroup, deluser, depmod, devmem, df, dhcprelay,
diff, dirname, dmesg, dnsd, dnsdomainname, dos2unix, dpkg, dpkg-deb,
du, dumpkmap, dumpleases, echo, ed, egrep, eject, env, envdir,
envuidgid, ether-wake, expand, expr, fakeidentd, false, fatattr, fbset,
fbsplash, fdflush, fdformat, fdisk, fgconsole, fgrep, find, findfs,
flock, fold, free, freeramdisk, fsck, fsck.minix, fstrim, fsync, ftpd,
ftpget, ftpput, fuser, getopt, getty, grep, groups, gunzip, gzip, halt,
hd, hdparm, head, hexdump, hostid, hostname, httpd, hush, hwclock,
i2cdetect, i2cdump, i2cget, i2cset, id, ifconfig, ifdown, ifenslave,
ifplugd, ifup, inetd, init, insmod, install, ionice, iostat, ip,
ipaddr, ipcalc, ipcrm, ipcs, iplink, ipneigh, iproute, iprule,
iptunnel, kbd_mode, kill, killall, killall5, klogd, less, linux32,
linux64, linuxrc, ln, loadfont, loadkmap, logger, login, logname,
logread, losetup, lpd, lpq, lpr, ls, lsattr, lsmod, lsof, lspci, lsusb,
lzcat, lzma, lzop, lzopcat, makedevs, makemime, man, md5sum, mdev,
mesg, microcom, mkdir, mkdosfs, mke2fs, mkfifo, mkfs.ext2, mkfs.minix,
mkfs.vfat, mknod, mkpasswd, mkswap, mktemp, modinfo, modprobe, more,
mount, mountpoint, mpstat, mt, mv, nameif, nanddump, nandwrite,
nbd-client, nc, netstat, nice, nmeter, nohup, nslookup, ntpd, od,
openvt, passwd, patch, pgrep, pidof, ping, ping6, pipe_progress,
pivot_root, pkill, pmap, popmaildir, poweroff, powertop, printenv,
printf, ps, pscan, pstree, pwd, pwdx, raidautorun, rdate, rdev,
readahead, readlink, readprofile, realpath, reboot, reformime,
remove-shell, renice, reset, resize, rev, rm, rmdir, rmmod, route, rpm,
rpm2cpio, rtcwake, run-parts, runsv, runsvdir, rx, script,
scriptreplay, sed, sendmail, seq, setarch, setconsole, setfont,
setkeycodes, setlogcons, setserial, setsid, setuidgid, sh, sha1sum,
sha256sum, sha3sum, sha512sum, showkey, shuf, slattach, sleep, smemcap,
softlimit, sort, split, start-stop-daemon, stat, strings, stty, su,
sulogin, sum, sv, svc, svlogd, swapoff, swapon, switch_root, sync,
sysctl, syslogd, tac, tail, tar, tcpsvd, tee, telnet, telnetd, test,
tftp, tftpd, time, timeout, top, touch, tr, traceroute, traceroute6,
true, truncate, tty, ttysize, tunctl, ubiattach, ubidetach, ubimkvol,
ubirename, ubirmvol, ubirsvol, ubiupdatevol, udhcpc, udhcpd, udpsvd,
uevent, umount, uname, unexpand, uniq, unix2dos, unlink, unlzma,
unlzop, unxz, unzip, uptime, usleep, uudecode, uuencode, vconfig, vi,
vlock, volname, watch, watchdog, wc, wget, which, whoami, whois, xargs,
xz, xzcat, yes, zcat, zcip

FAQ:

1
2
3
Q:adb push命令出现错误:adb: error: failed to copy '***' to '/data/***': Read-only file system
A:请在模拟器内,使用RE,手动挂载为可读写,再次重试

inotify

inotify是用来监视文件系统事件的机制,在linux 2.6.13内核中引入。该机制可以用来监视文件和目录,当文件或目录发生变化时,内核会将文件或目录的变化发送给inotify文件描述符,在应用层只需调用read()就可以读取这些事件,非常的方便。更好的是,inotify文件描述符还可以使用select、poll、epoll这些接口来监听,当有事件发生是,inotify文件描述符会可读。

Genymotion模拟器也支持该特性,内核版本:root@vbox86p:/ # uname -r ——–> 3.10.0-genymotion-g08e528d

Talk is cheap,show me the code.