相见恨晚的二进制序列化库:binrw

binrw 是一个针对二进制文件格式的(反)序列化库。不同于 serde 等其他的序列化库 —— serde 关注的是如何更轻松准确地把 Rust structs 映射为 serde 的内部数据模型,而序列化和反序列化这一步(即从 serde 的数据模型到文件格式的映射)需要自行编写后端,当然通用格式的后端基本都是有的,比如 JSON 和 XML。而 binrw 的针对的恰好是序列化/反序列化这一步:它能够让你更轻松地表达 Rust struct 与二进制文件格式里每个字节之间的映射关系,而无需手工编写 Serializer/Deserializer。

我一开始试图找过类似的库,也许是关键词不对、或者是看的不仔细而错过了,导致没有 binrw 可用的时候,我只能手写反序列化代码,比如:

pub fn read_pol(reader: &mut dyn Read) -> Result<PolFile, Box<dyn Error>> {
    let mut magic = [0u8; 4];

    // 读取四字节 magic number
    reader.read_exact(&mut magic)?;

    match magic {
        [0x50, 0x4f, 0x4c, 0x59] => (), // "POLY"
        _ => panic!("Not a valid pol file"),
    }

    // 序列化、反序列化时需要注意字节序
    let some_flag = reader.read_u32::<LittleEndian>()?;
    let mesh_count = reader.read_u32::<LittleEndian>()?;
    let mut geom_node_descs = vec![];

    // 读进一个数组
    for _i in 0..mesh_count {
        let unknown = reader.read_w_vec(26)?;
        geom_node_descs.push(GeomNodeDesc { unknown });
    }

    let mut unknown_count = 0;
    let mut unknown_data = vec![];
    if some_flag > 100 {
        // 只有版本大于某个值时才会有这部分数据
    }

    let mut meshes = vec![];
    for _i in 0..mesh_count {
        meshes.push(read_pol_mesh(reader)?);
    }

    Ok(...)
}

一开始对于我来说倒是问题不大,一是文件格式有限,几种格式写个遍也还好;再就是这些格式的含义都是逆向分析得出的,本身搞清楚他们的意思就很慢,写反序列化代码这部分暂时算不上瓶颈。

到了后来,问题的重要性才开始提高:

  1. 项目预计的领域扩大了。现在不光需要解析读取仙剑三的各类文件,还需要读仙剑四、五、轩辕剑、古剑等诸多游戏的各类文件,每个文件类型都手写一下,会很难受。
  2. 现在只写了反序列化的代码,序列化代码需要再写一份。
  3. 代码不够简洁。现在的代码其实还算清晰、复杂度也不高,但是 boilerplate 很多,维护起来虽不费劲但也有一些成本。文件格式增多会加重这一点。

直到解析古剑奇谭的 GameBryo NIF 格式的模型时,又去网上冲浪了一圈,果然被我找到了 binrw。有了 binrw,我就可以这样说话了:

#[binrw]
#[brw(little)]
#[derive(Debug)]
pub struct NiObjectNET {
    name: u32,
    num_extra_data: u32,

    #[br(count = num_extra_data)]
    extra_data: Vec<u32>,

    controller: u32,
}

首先是指定字节序这件事,用 [brw(little)] 就可以了。数组也不在话下,可以直接指定结构体里的成员作为数组长度。再比如:

#[binrw]
#[brw(little)]
#[derive(Debug, Serialize)]
pub struct HAnimPlugin {
    pub header: HAnimHeader,

    #[br(if(header.bone_count > 0))]
    pub unknown: Option<HAnimUnknown>,

    #[br(count = header.bone_count)]
    pub bones: Vec<HAnimBone>,
}

可选字段也可以轻松应对—— 只有在 bone_count > 0 的时候,二进制文件中才会有一段区域描述这个 unknown 结构,否则会直接进入 bones 字段的内容。再如:

#[binrw]
#[brw(little)]
#[brw(import{half_float: bool = false})]
#[derive(Debug, Serialize)]
pub struct Vec3f {
    #[br(parse_with = float_parser)]
    #[br(args(half_float))]
    pub x: f32,

    #[br(parse_with = float_parser)]
    #[br(args(half_float))]
    pub y: f32,

    #[br(parse_with = float_parser)]
    #[br(args(half_float))]
    pub z: f32,
}

一个坐标点,里面维度的数值可能是 f32,也可能是 f16。解析每个 field 时可以指定一个自定义的解析函数,同时支持传入一些参数。在这个例子中,传给 float_parser 的参数 half_float 本身是这个结构体的序列化参数,由 #[brw(import{half_float: bool = false})] 指定。使用这个 Vec3f 时,可以在外部传入这个参数:

#[binread]
#[brw(little)]
#[br(import(kf_type: u32))]
#[derive(Debug, Serialize)]
pub struct AnmKeyFrame {
    pub ts: f32,

    #[br(args{half_float: kf_type == 2})]
    pub rot: Vec4f,

    #[br(args{half_float: kf_type == 2})]
    pub pos: Vec3f,

    pub pref_frame_off: u32,

    #[br(if(kf_type == 2))]
    pub kf_offset: Option<Vec3f>,

    #[br(if(kf_type == 2))]
    pub kf_offset_scalar: Option<Vec3f>,
}

每个 binrw 了的结构体会自动生成 read、write 函数,可以直接调用进行反序列化/序列化:

NifModel::read(&mut Cursor::new(...))

现在除了一些 corner case,几乎不需要再手写任何序列化代码了。描述序列化的各种参数都跟字段本身放在一起,一眼望过去就大概知道文件格式是什么样的,序列化/反序列化时做了什么事情,查错维护很方便。

总之世界简洁了不少,绝对算得上相见恨晚。现在已经离不开了。

node-ipc:再谈开源软件与政治

这是我一直反对在自由开源软件中夹带政治立场的原因,只不过 node-ipc 的作者更进一步,将纯粹的意识宣传转变为了物理斗争。

在我之前的一篇文章里:https://dontpanic.blog/notepadpp-rust-tech-and-politics/ 也有提到,有人就离不开政治。甚至自由软件、开源软件本身也是一场政治运动[1],但这并不代表通过自由软件宣传无关的政治立场是合理的。我一般把这种行为叫做夹带私货。而这次 node-ipc 夹带的私货,有点多。

当然,开源界也有分歧、也搞政治:自由软件与开源软件之间存在思想争论[2]。因此我在开放代码时选择什么样的开源协议,其实也是一种政治。但他们只是表现为对当今资本与商业社会的接纳方式不同,与谁当总统、谁来执政没有有半点关系。

自由与开源软件所代表的精神,是人人为我、我为人人的精神,是世界大同、天下一家的精神。从道理上讲,我认为将自由与开源软件与政治立场绑定的做法,违背了自由与开源软件运动的初衷。它通过政治立场隐式地对用户社区做了限制。

那一个自由开源软件是否可以通过使用某种开源协议、或是一些附加条款,显式地撤销某些特定用户的权利呢?作为软件的作者,当然有权利自行选择协议;但需要注意的是,不论是开源软件还是自由软件 ,对协议本身是有要求的。也就是说,不是所有的协议都能叫做“开源协议”,也不是开放了源代码就可以叫开源软件。例如,开源定义[3]的第五条指出,不得歧视任何个人或群体:

5. No Discrimination Against Persons or Groups
The license must not discriminate against any person or group of persons.

再比如,GPLv3[4] 中的第七条明确指出,

……
尽管已存在本协议的其他条款,对你添加到受保护作品的材料,你可以(如果你获得该材料版权持有人的授权)以如下条款补充本协议:
a)表示不提供品质担保或有超出十五、十六条的责任。
b)要求在此材料中或在适当的法律声明中保留特定的合理法律声明或创作印记。
c)禁止误传材料的起源,或要求合理标示修改以别于原版。
d)限制以宣传为目的使用该材料的作者或授权人的名号。
e)降低约束以便赋予在商标法下使用商品名、商品标识及服务标识。
f)要求任何转发该材料(或其修改版)并对接收者提供契约性责任许诺的人,保证这种许诺不会给作者或授权人带来连带责任。
此外的非许可性附加条款都被视作第十条所说的“进一步的限制”。如果你接收到的程序或其部分,声称受本协议约束,却补充了这种进一步的限制条款,你可以去掉它们。……

我不是律师,但“某些群体不得使用”,应当不包含在上面允许的范畴中。也就是说,你可以移除它们。

因此,虽然用户社区是不受这些条款限制的,但是很显然,这种“限制某些群体融入社区”是有违开源软件和自由软件的思想和初衷的。


至于 node-ipc,就让他凉凉吧。暗暗庆幸我用的不是俄罗斯 IP。

参考

  1. Richard 对自由软件运动的看法 https://lists.gnu.org/archive/html/emacs-devel/2008-03/msg00635.html
  2. 《为什么开源错失了自由软件的重点》,Richard Stallman https://www.gnu.org/philosophy/open-source-misses-the-point.zh-cn.html
  3. The Open Source Definition https://opensource.org/osd
  4. GPL v3 非正式中文翻译 https://jxself.org/translations/gpl-3.zh.shtml

OpenPAL3 v0.3:主线剧情完结撒花🎉!

OpenPAL3 距离上次发版大概有半年的时间了,现在终于可以勉强把主线剧情推完:

景天:泥垢了

说“勉强”是在于,推进主线需要借助……内挂:

可以设置穿墙和切换地图层

以及这个:

可以调用指定脚本、切换场景、设定主线剧情进度

主要原因是一些场景机关没有实现。主线中第一个过不去的机关在蓬莱迷宫,在这之前不开内挂也没问题。梯子是可以爬的,按下互动键会瞬移上去😅

另外,OpenPAL3 现在支持手柄了!左摇杆是跑,右摇杆转动视角,B 键互动,A 或 B 键下一句对话。


这次同样可以在 GitHub Releases 和 Gitee Releases 上下载预编译好的程序:


上一个版本发布后定下的几个目标里,人物跳跃和场景机关都还没有完成。不过人物跳跃不影响主线进度,所以可以以后再做。接下来可以考虑的是:

  • 渲染:比如天空盒和光照还没做,深度缓冲也还有点问题
  • 剧情脚本:目前剧情脚本还有很多没做的指令,导致剧情演出不够润滑
  • 战斗
  • 一个酷炫的开始界面:上次的视频有人说现在的开始界面太拉跨……下次一定要做一个狂炫酷拽叼的开场!

Notepad++/Rust:技术与政治

今天偶然想起来 Notepad++ 的 Release note,又想起 rust 也有过这么一段历史:

就对裹挟自由软件夹带政治立场的行为思考了一下。我的观点是,rust core team 这么干,不妥。

rust 是个好语言,社区也不错,但没必要爱屋及乌地对 core team 的每件事都支持。users 论坛也有个帖子讨论了这件事:

帖子有点长没看完,支持和反对至少55开,甚至反对声音更多一点。

我认为不妥的首要原因是,core team 在 twitter 上公开发表政治倾向,甚至暂停了 5 周技术方面的推文更新,是一种对特权的滥用。他们在个人社交媒体上如何发言都可以,但无权代表社区发表政治言论,社区的所有人也不可能都持有统一的政治意见。

其次,rust 社区是一个欢迎所有人的社区(写在 code of conduct 第一条)。现在的潜台词就是,不支持 BLM 吗?那我不欢迎你。退一步讲,如果因为你也同样支持 BLM 所以不觉得 core team 的做法有问题,那假如将来 core team 所有人决定支持一下小菜菜呢?

有人说“有人就有江湖,技术摆脱不了政治”。本身“有人就有江湖”是一句客观正确的话,但没什么用。我做技术选型,是一种政治;社区选择使用英语(而不是世界语,例如)交流,也可以算做是政治;公司里勾心斗角搏上位,当然也是政治。但是这跟我支持民主党还是共和党有关系吗?跟我支持巴基斯坦还是印度有关系吗?先把“政治”的概念泛化扩大,说“你无法避免”,然后再具体到同属“政治”但与当前问题完全无关的点上,我们一般把这种行为叫做夹带私货。


至于 Notepad++,我认为道理是一样的。我并没有对作者的政治立场有任何意见,但很明显作为社区领导者,他对社区(包括贡献者和用户)有政治倾向的期望,客观上会限制队对立立场的贡献者和用户。而且与 Rust 不同,Notepad++ 几乎由作者一人把控。不过好在 Notepad++ 基于 GPLv3 协议开源,等有心情的时候就 Fork 一份,名字就叫 Notepad Triple Plus。Code of Conduct 第一条就写上,No politics。

OpenPAL3:仙三开源版的第二个小目标 Accomplish!

去年的时候,OpenPAL3 的第一个版本发布 之后,我给 0.2 版本设定了一个小目标:让景天能跑出永安当。当时的第一个版本还只能算是概念验证的版本,没有音乐支持、输入支持,不能直接读取仙剑三的打包文件,剧情是也在程序里面硬编码的……一年之后,景天终于跑出永安当了!✌

这次为大家提供了可运行的程序:

Gitee Releases

GitHub Releases

下载解压之后,首次运行之前记得在 openpal3.toml 文件中把《仙剑奇侠传三》的安装目录填进去。请注意反斜杠需要重复写两次哟:

之后运行openpal3.exe即可。如果运行时提示 OpenAL 出错,请下载并安装 OpenAL

目前游戏只支持键盘输入,键位为:

  • 空格键:对话框下一句
  • F键:互动
  • 方向键:跑
  • 1/2/3/4:存档至第1、2、3、4号存档位

程序还是有很多 Bug,以及有很多功能还没实现,请各位不要尝试去做奇怪的事…… 景天已经可以跑到码头那里准备上船了,但由于剧情脚本还没有 100% 支持,码头这里会用到没有实现的剧情指令而崩溃。

(以及……不要在意出现两个景天 )

另外,开发工具也全新升级!之前的模型和动画浏览器合三为一,集成在了一个工具里面:

它能够自动读取打包文件的内容,支持听歌、查看各种文件数据以及预览模型。这个工具也会读取 openpal3.toml,所以游戏路径也需要填好。


由于最近 GitHub 的访问情况不容乐观,我在 Gitee 上建了一个镜像仓库,下载也可以更快一点:

所有的开发工作还是在 GitHub 的仓库里:

以及,我们有一个新的项目主页了!还没时间仔细弄,先挂一个项目说明:


按照惯例,我们还需要确定下一个小目标。现在还没实现的功能太多了,我准备还是优先把剧情跑通,所有其他的 Bug 和系统先暂时放一下。那么第三个小目标就定为景天和雪见流浪到大渡口吧。为了实现这个目标,我们至少还需要支持:

  • 更多的剧情脚本
  • 人物跳跃事件
  • 场景机关
  • 爬梯子

希望下一个版本可以很快发布( •̀ ω •́ )y

传统的 try-catch 异常处理是否是编程语言发展中的弯路?

https://www.zhihu.com/question/425726667/answer/1525039692

我不觉得是弯路,毕竟对于多数的项目来说,写得爽比线上崩溃一两次要重要得多。

先说一下题主提到的第二点,

2) 异常处理强制了进行动态类型识别,这是一种额外的开销(虽然并不大)

题主指的应该是在异常发生时对异常对象的动态类型转换?其实这也可以看作异常处理的优势:在没有异常发生时,不需要这种额外开销。通过返回值(包括返回 Result)传递错误,由于存在更多的分支,给编译器优化、分支预测、指令 Cache 带来了负面影响;而异常通常只需要在栈上多开一点点空间(用来存放异常处理指针)。从性能上说,虽然在异常发生时它的性能要低于返回值,但在正常执行时,异常反而存在优势。

至于题主说的第一点,

许多时候面对一个子函数调用,我们可能无法知道它是否会抛出异常(当然有些语言有异常规格说明,但其实形同虚设),这导致有些时候当我们错误地假定函数不会抛出异常时,就会出现资源泄漏。写出强异常安全的代码在try-catch显得很隐晦,因而困难。

这就要说到异常的分类了。包括 C++ 和 C# 在内的很多语言,使用的都是 Unchecked Exception;而 Java 用的是 Checked Exception (除了 RuntimeException 之外)。题主谈到的,确实是 Unchecked Exception 的弊病。在我看来,Unchecked Exception 其实是最不靠谱的那种。如果一门语言使用的是 Checked Exception,那么理论上在编写强安全的代码时,难度应该与使用返回值/Result的语言没有差别。

然而,使用 Checked Exception 的 Java 被吐槽最多的,正是它的异常处理。很奇怪吧?Java设计出checked exception有必要吗?

但正如 Andrew 在一篇采访 The Trouble with Checked Exceptions 中所说,在某些时候(比如应用开发),大多数人并不关心如何处理异常,他们只关心当异常发生时我要处理好后事(C# 的 using),剩下的事就交给最上层的 catch 去做好了。这是大家喜欢 Unchecked Exception 的原因。

Unchecked Exception 很烂,但是大家就是喜欢,毕竟懒是人类的天性。

所以,一些项目会禁用 C++ 的异常,甚至不惜自己魔改一套不用异常的 STL,尤其是那些偏底层的、关键的、崩溃代价很高的项目。而大多数人做的并不是这样的项目,写得爽比线上崩溃一两次要重要得多。

题主说的第三点,

许多基于try-catch的编程语言,并不完全强制用户处理所有异常,这带来了便捷,但却又使得程序员会忽略一些本该处理的异常。

这也主要是 Unchecked Exception 的问题。对于不支持 Option/Result 的语言(比如 C 和 go),如果使用返回值的方式,其实同样会导致程序员忽略处理错误。


Rust 以及很多函数式语言的 Option/Result 确实也是我目前最喜欢的错误处理模型了。理论上,一门语言可以完全屏蔽它在底层对错误的处理方式,比如看起来是使用返回值,其实是抛了异常;或者看起来像是抛了异常,其实只是返回了一个值。因为它们只是两种不同的错误处理方式,所提供的能力是一致的;语言具体怎么表达,才是最影响用户编程体验的。我知道的唯一一个后者就是 Haskell,它让你有一种在使用传统的 try/catch 的错觉。

很多使用异常的语言对 try/catch 的支持还有其他的问题,比如语法不够简洁、处理粒度不够细等等。

另外再次推荐一下这篇文章(长文警告):Joe Duffy – The Error Model​

以及我的翻译:dontpanic:错误模型 – The Error Model​

Firefox 的 logo 上趴着的居然是一只小熊猫?

Firefox 的 logo 上趴着的不是 Fox,而是一只小熊猫!!

在成都熊猫基地拍到的小熊猫

…they opted for an animal that was not well known on the web at the time. It was the red panda. Unfortunately, people thought that the animal on the Mozilla Firefox logo was a fox. This “firefox” is actually a red panda which is a protected species in Asia. A mistake when translating red panda from Chinese to English is how we got firefox.

The Story Behind the Mozilla Firefox Logo (freelogodesign.org)

Firefox 官推大概是最大的小熊猫粉丝聚集地,官推经常发一些萌萌哒小熊猫吸粉(划掉)

不过从 Firefox 取名的历程来说(Phoenix 到 Firebird 再到 Firefox),Mozilla 一开始估计还是把它当作狐狸的,官方(或者是小编)对于它到底是狐狸还是小熊猫有时表现得很暧昧,比如这里又说它是狐狸:

不管了,反正小熊猫这么可爱,就钦定它是小熊猫了!

https://en.wikipedia.org/wiki/Red_panda

假期去成都熊猫基地,一直小熊猫刚好从我脚边溜过,简直萌到爆炸 (●’◡’●)


另外小熊猫不是小浣熊,它们长得不一样:

盗图来自下面的文章 作者翼狼Elang

认真你就输啦 (?ω?)ノ- ( ゜- ゜)つロ​www.acfun.cn

鸿蒙 2.0 模拟器里究竟有什么:鸿蒙 Java 运行时简析

这是我的两篇知乎文章 https://zhuanlan.zhihu.com/p/234397497https://zhuanlan.zhihu.com/p/243981657 的合辑。


最近因为个人问题一直又烦又颓,知乎一直没什么动态,连 OpenPal3 都几个月没更新了。但是从昨晚开始好几个小号想过来打我的脸,原因是我一年之前回答了这个问题:为什么很多人不相信鸿蒙系统是真的?​

姑且不说你们搞清楚我讽刺的点在哪没有,请问你们可以拿现在的剑去圆去年前年吹的 b 吗?作为一个程序员,始终对华为终端在软件上的吹逼行为十分讨厌。而且花粉们似乎已经接受了这样的现实,还觉得“大嘴吹过的皮全都实现了”。这种空手套沸腾的行为,不是华为还真不敢干。论营销,你以为华为在负一层,其实他在第五层。这些人搞得我心态爆炸真的烦,我就想看看鸿蒙 2.0 到底是个什么东西。

一开始大家说华为电视上的鸿蒙 1.0 是安卓套壳,很多人纷纷跳出来说“adb 说明不了问题”,颇有见到胳膊就想到大腿的意味;后来又改了话术,说“按照路线图,1.0 就是安卓套壳。怎么了?”。那按照路线图,请问 2.0 就应该不是安卓套壳吧?

DevEco Studio

所以我去下了 DevEco Studio。需要注意的是,DevEco Studio 并不是新做的,华为之前就有这个东西,是开发安卓的:文档中心​

DevEco 1.0 是用来开发安卓的

我一开始就找错了,这个是1.0。2.0 把安卓的支持“删掉”了,换成了鸿蒙。当然这都没什么问题,用 IDEA 二次开发也没什么问题,只是预防海军把 DevEco 拿来作为鸿蒙早就存在的论据。

工程方面现在支持新建电视、智能手表和半智能手表(个人归类,不喜你对)。电视和智能手表支持 Java 和 Javascript,半智能手表只支持 Javascript。比较关心电视的 Java 实现,所以新建了这个。

package com.example.myapplication;

import com.example.myapplication.slice.MainAbilitySlice;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.content.Intent;

public class MainAbility extends Ability {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setMainRoute(MainAbilitySlice.class.getName());
    }
}

Emmmm…. 虽然我不做安卓,但是很久以前学过一点。这好像跟 Activity 有点像?不过也没关系,毕竟(做)好的 API 设计拿来用用,也不是不行。

下面我就想去看看 Ability 的代码。SDK 里面包含的全是 Stub,我去 OpenHarmony 上也没找到 aafwk 的源代码。只找到了 aafwk-lite,但这个明显不是电视上用的版本。各位海军请不要再随便丢一个链接过来说“鸿蒙已经开源了链接在这你还不服吗”,你们自己有去看过吗?还烦请哪位大佬找到了 aafwk 的源码的话留个言。

模拟器运行

找不到就算了,DevEco 还有一个 HVD 管理器,我想这既然模拟器都有,里面运行时肯定是全的吧?

不过这个 Manager 还需要下一些包才能运行,下载一直磕磕绊绊的出问题。这也导致我现在才把模拟器抱起来。

运行模拟器需要登录华为账号?也罢毕竟我以前用过荣耀手机,华为账号还是有的。然后实名注册了一波回来发现,模拟器运行每次限时 1 小时?大概各位也能猜到了,这模拟器其实是跑在服务器上的,界面再串流串回来!

这就堵住了我想看看模拟器里面内容的想法。也行,我先把 Hello World 跑起来吧。跑了 HelloWorld 起来之后,很快就会有 log 打出来。这个也有别人发过了,各位估计也见过:

嗯?

如果你随便下个断点,还能看到更具体的:

嗯?×2

点开 .shadow class,还可以看到:

可能有点看不清,我贴过来:

嗯?×3

dalvik 出现。以及,编译生成的 hap 是个 zip 吗?

嗯?×4

既有 dex 又有 apk,我们把 apk 再解一下,看看究竟是不是大白腿:

嗯?×5

半智能手表

上面也提到过,半智能手表只能使用 Javascript 开发,这是与电视和智能手表不同的地方。其实它们还有另一个不同之处,就是电视和智能手表支持由在服务器上串流调试,但是半智能手表不行。而且它连本地的模拟器(Emulator)都没有,只有一个用 Node 做的 Simulator:

启动 Simulator 的防火墙弹窗
本地 Node.js 做的的 Simulator

华为目前开源出来的代码,都是 Lite 版本,是基于它 2016 年就开源了的 LiteOS 做的。所以有理由相信,现在只有半智能手表会用 LiteOS 版本的鸿蒙。而且现在连完整的模拟器都没有,只能用 nodejs 顶一下。

结语

所以我觉得到这里可以大胆地做出结论了:

电视和智能手表上,完全就是安卓套壳;只有半智能手表,是华为在它 16 年开源出来的 LiteOS 的基础上做出来的。

以华为在嵌入式领域的积累,花一年多做成 LiteOS + 半智能手表这样的进度是合理的。这个答案 如何看待 9 月 10 日华为发布的鸿蒙 OS 2.0 系统,应用前景如何?

里有一段话,我想摘录在这里:

华为本来有两个选择:
1. 我们被美国制裁了,我们别无他法,我们决心从现在开始,用3到5年的时间,自主研发我们的一套体系,摆脱对美依赖;
2. 我们被美国制裁了,但是我们早就做好了准备,已经掏空安卓,随时能拿出鸿蒙来替代,引起一片沸腾;然后顶着沸腾、质疑和嘲讽暗戳戳的加班加点3到5年,终于拿出东西来。
我始终搞不懂的是,为什么华为选择了方案2,因为华为高管喜欢沸腾?
两种选择反映的其实是一家公司的做事态度,反映的是它的诚信度和责任感。

作者:世界树的影子
链接:https://www.zhihu.com/question/420404904/answer/1465210355
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

说实话,要是我的老板把牛逼吹出去了(难听点说就是造假),但我现在的代码做不到,我会脸上滚烫,生怕谁过来问我说“这个 Feature 是还有问题吗?这里是还有 Bug 吗?”。不知道华为的程序员会不会如此。如果也是这样,我能做的也只是摸摸他们受伤的心灵了。

海军还要如何吹?

我也想了一下,海军还能怎么吹呢?

  • 不管,就是打你的脸了!鸿蒙真的存在!
  • 看到 apk 就想到安卓?鸿蒙兼容安卓,这不是很正常吗?
  • 你知道做操作系统多难吗?不用一些开源的东西,你能做出来?
  • 按照路线图,鸿蒙 2.0 就是这个样子,3.0 明年见。
  • 我只能说鸿蒙是中国的希望

当然,具体鸿蒙是什么里子他们是不会管的,华为是不是吹牛逼他们也不会管的。要是华为不吹了,他们大概反而会失落吧。


距离 9 月 10 号已经过去很多天了,鸿蒙的开水终于逐渐平息。我在 上一篇文章 (饱含了对沸腾人民、沸腾现象的愤怒情绪)中最后的总结是:

电视和智能手表上,完全就是安卓套壳;只有半智能手表,是华为在它 16 年开源出来的 LiteOS 的基础上做出来的。

如果你还对现在电视上的鸿蒙心存幻想,觉得鸿蒙不是安卓、觉得鸿蒙是在兼容安卓、觉得安卓是鸿蒙的子系统,这篇文章会更加清楚明白地告诉你真相;如果你认可了鸿蒙是安卓套壳,那么这篇文章会从技术角度探究一下,这层壳到底有多厚。

HDC:鸿蒙版的 ADB

前两天刷 IT 之家,刚好刷到这篇帖子:[系统] 鸿蒙2.0:API/SDK逆向-模拟器鉴赏-LiteOS对比

原来用鸿蒙版的 adb,是可以直接连上 Shell 的… root 之后读取 build.prop,证实了系统就是安卓 10。随便看看,还有大量的安卓包、动态库、目录结构什么的,也能够佐证。

既然能登上 Shell,那也就可以把运行时拉下来,看看鸿蒙的运行时到底是什么样子的。

boot-zframework.z.vdex

直接

grep -Rl ohos /system

看下来一圈,猜测 /system/framework/boot-zframework.z.vdex 就是鸿蒙的 Java 运行时,pull 下来一看确实是。vdex 叫做 pre-validated DEX,是安卓新引进的格式。可以用下面的工具anestisb/vdexExtractor​

把 vdex 转换回普通的 dex(转换完会有两个 dex)。直接扔进 jadx 就可以反编译了:

很多在 SDK 里面不提供的类,都在这里面

如果跟 SDK 对比一下,会发现很多 SDK 中不存在的类使用了安卓的包。从设计上,大多是使用了 Delegate/Proxy/Adapter 避免了上层代码直接依赖安卓,这也是屏蔽实现细节的常用手段。不难看出,虽然华为在 SDK 剔除掉了安卓的依赖,但现在鸿蒙仍然是在安卓之上做的封装。

鸿蒙的分布式特性

在继续分析这层壳的薄厚之前,我觉得有必要先说一下鸿蒙的分布式特性。看了一圈代码下来,是能够感觉到鸿蒙有自己的设计和想法的。这一段完全来自我对反编译代码的理解,未必完全正确,也代表不了鸿蒙真正的思路。

基本想法是:把 API 做成 RPC Call。

鸿蒙的 Ability,比安卓的 Activity 概念要宽泛一些,更像是一个 Service。比如 Wifi Device 是一个 Ability,Wifi Enhancer 是一个 Ability,Wifi HotSpot 也是一个 Ability,另外还会有一个 Registra 来做服务注册和查询。具体这些 Ability 是由谁提供的不是很重要,毕竟都是 RPC Call,理论上不在本机也可以。在 SDK 这一侧,这些功能会被包装成普通的 API。

当然这些都不是新概念,老旧的 COM 就已经可以统一 RPC Call 和本地 Call、也连带提供服务注册和查询。后端上这种微服务架构也比较常见。不过,把操作系统的 API 也做成这种结构,我确实不清楚现在有其他的系统在做。

鸿蒙的壳,有厚有薄

运行时里面包含了很多的组件,完成度各不相同。我把它们大致归为四类,从上到下完成度递减:

  1. 已经按照上面的设计,做完了 RPC 封装、在 Java SDK 和 Service 一侧都不再依赖安卓组件
  2. Java SDK 一侧已经不再依赖安卓,用 JNI 调用了自己封装的 native library,但是在 native 一侧仍然需要依赖安卓
  3. Java SDK 一侧还是需要依赖安卓组件,但是也有依赖自己封装的 native library
  4. 安卓 API 映射大法

我没有全部看完所有组件。目前看的组件里,只有部分相对较轻、比较独立包能够做到第一类(比如 wifi 和 bluetooth),大多组件都是第二和第三类(包括 Ability/AbilityShell/Agp/Usb等等)。第四类也有,典型的比如 os.ProcessManager。

先举一个第一类的例子吧,比如 WifiEnhancer:

这是个单例,在新建对象的时候会把需要的 AbilityId 传进去存起来。API 都是 Message Passing:

在每个函数最后的 request 里,会使用 AbilityId 去 Registra 里面找相应的 Ability 缓存起来,然后直接对他 Rpc call。

这段代码的反编译有点问题

再举个第二/第三类的例子吧:Agp 是华为自己的自绘组件库,封装了常见的 UI 组件和自绘能力:

components 里是 UI 控件,render 里面是绘图 API

components 和 render 里面都是对自己 native library 的 wrapper。在 SDK 一侧,还是需要有 Adapter 来依赖安卓包;而在 native library 一侧,也还有对安卓的依赖:

libagp.z.so 的导入表

上面是 libagp.z.so 的导入表,对安卓的依赖不多。它还导入了很多 Skia 的方法,基本可以是确定是用 Skia 自绘。Skia 是谷歌开发的图形库,不过是跨平台的,可以不算安卓独有。

下面是 libagp.so 的导入表,还依然有大量的安卓导入函数:

libagp.so 的导入表

其实根据国内每家都有自己的安卓 UI 来看,各厂应该都有类似的库,只是未必直接作为 Java API 提供给 App 使用。所以我就好奇去下了个 DevEco 1.0,华为在 1.0 里面提供了线上的手机模拟器。开了个 P40 Pro (Emui 10),里面也有 libagp。(所以盲猜这个 agp 本来是 Emui 里面的自绘库,在鸿蒙里面做了一些新改动,暴露给了 App。)

(此处应有截图,但是华为的线上模拟器经常点不了允许调试…)

再放一个第四类的吧。不过 ProcessManager 本身足够简单,不需要过度封装:

请原谅我看到这的时候真的笑了…我不是故意的

结语

按照目前的分析,我认为鸿蒙如果真的想完全不依赖安卓,还有很长的路要走,个人乐观估计往后一到两年都会是安卓套壳/基于安卓/安卓魔改的状态。跟大嘴的 PPT 更是差得远,更谈不上已经掏空、随时替代、比安卓快 60%。而且似乎并没有要走 WSL1 的路线,这样改下去,要是未来某一天真的把安卓所有的东西都换掉了,肯定是没办法二进制兼容安卓 App 的,连源代码级别的兼容都做不到。

最后,还是想带一点私货。这几天大概看到了热心群众这样的心路历程:从“鸿蒙横空出世”,到“鸿蒙囊括了安卓”,到“鸿蒙在兼容安卓”,再到“华为是在诈美国”,最后是“你说的都对,但是华为需要群众的沸腾来支持”… 难道现在天道有轮回,再次进入亩产三万斤的时代了吗?

若真是如此,那美帝看我们,跟我们看三哥,恐怕没什么大区别。