ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

《离线AI安卓APP》附录 A 零基础补课:读这本书之前,你需要知道的

《离线AI安卓APP》附录 A 零基础补课:读这本书之前,你需要知道的

这个附录是给谁的
如果你看到下面这些话里有任何一个不懂,就该读这个附录:

  • “变量”、“函数”、"循环"是什么?
  • 命令行怎么用?cd是什么意思?为什么路径有D:\和/两种写法?
  • 为什么书里说0x47?24 字节、f16(2 字节)是什么意思?
  • “一个汉字占 3 个字节”——为什么?乱码方块是怎么产生的?
  • JSON 是什么?为什么要"序列化"?
  • CPU、内存、进程、线程、虚拟机——这些词听过但说不清。
  • Git 是什么?为什么要clone?

如果你已经懂这些,可以跳过本附录,直接读 ch01。
本附录不涉及 Android,只讲"读这本书需要的最小常识"。

怎么用本附录:不用从头背。遇到不懂的词,回这里查对应小节。
每节末尾都有"在本项目哪里用到",告诉你去哪一章看它真正的样子。


A.1 编程的最基本概念

这一节假设你从没写过代码。如果你写过任何一门语言,可以快速浏览。

A.1.1 程序是什么

程序 = 给计算机的、一步一步的操作说明。

💡类比
程序像菜谱:先切菜、再热锅、再下油、再翻炒……
计算机(电脑/手机)只会严格按你写的顺序执行,不会"猜到你想干什么"。
所以顺序、每一步是否说清楚,比"聪明"重要得多。

一个程序通常包含几种最基本的"动作":

动作作用类比
变量记住一个值给抽屉贴标签,把东西放进去
条件判断根据情况走不同路“如果下雨,带伞”
循环重复做同一件事“把碗一个个洗,直到洗完”
函数把一组操作打包,起个名字菜谱里的"炒个底料"(可复用)

A.1.2 变量与类型

变量就是一个"有名字的盒子",里面放一个值。

score = 100 ← 把 100 放进名叫 score 的盒子 name = "离线AI" ← 把文字放进名叫 name 的盒子

类型= 这个盒子里装的是"哪一类东西"。常见类型:

类型装什么例子
整数整数42、-7、8999
浮点数带小数的数0.3、3.14159
字符串一段文字"你好"、"Q4_K_M"
布尔值真 / 假true、false
列表一串东西[1, 2, 3]、["a", "b"]

⚠️为什么要有类型?
因为计算机对"数字 42"和"文字 "42""的处理完全不同:
数字可以做加法,文字不行。类型告诉计算机该怎么处理。

在本项目哪里用到:ch03 讲 Kotlin 的val/var与类型;ch14 讲 GGUF 元数据的 13 种类型。

A.1.3 函数

函数 = 一段"可以反复使用"的操作,有一个名字。

函数:算两个数的和 输入:a, b 输出:a + b 用的时候: 算两个数的和(3, 4) → 得到 7
术语含义
参数函数需要的输入(上例的a、b)
返回值函数给出的结果(上例的7)
调用"使用"一个函数这个动作

💡类比:函数像洗衣机上的按钮。
你按"标准洗"(调用),它内部有一大堆步骤(洗、漂、脱水),
但你不需要知道细节,只要按按钮。
这就是函数的两个好处:复用(按多次)+封装(不用管内部)。

在本项目哪里用到:ch03 讲 Kotlin 函数;ch13 讲"Kotlin 怎么调用 C++ 函数"。

A.1.4 条件判断与循环

条件判断(走不同的路):

如果 温度 > 30: 开空调 否则: 开窗

循环(重复做):

对列表里的每一个 消息: 显示它

在本项目哪里用到:ch16 的生成循环(while (!_cancelGeneration))、
ch18 的分批解码(for循环)。

A.1.5 数据结构(容器)

现实程序里,"一个值"很少够用,往往要装一堆值:

结构特点类比
列表 / 数组有顺序,可按下标取排队的队伍
字典 / Map键 → 值,按键查字典(按词查释义)
对象 / 类一组相关字段打包一张表格(姓名、年龄、地址)

在本项目哪里用到:

  • 列表:List<Message>(消息列表,ch03);
  • Map:GGUF 元数据是Map<String, MetadataValue>(ch14);
  • 对象:Message/ChatSession/ModelInfo都是(ch03)。

A.1.6 类和对象(面向对象)

类 = 一张"设计图纸";对象 = 按图纸造出来的具体东西。

类(图纸):Message 字段:id、content、isUser 对象(具体实例): 消息1 = Message(id="a1", content="你好", isUser=true) 消息2 = Message(id="b2", content="你好呀", isUser=false)
术语含义
字段 / 属性类里存的"数据"
方法类里能做的"操作"(相当于属于这个类的函数)
实例化按类造出一个对象(也叫"创建对象")

在本项目哪里用到:ch03 讲data class;MainScreenState是一个"类"(ch09)。

A.1.7 库、框架、依赖

词含义类比
库(Library)别人写好的一堆函数,你拿来用五金店里买现成的螺丝
框架(Framework)别人定好的"程序骨架",你在指定位置填代码买好的毛坯房(水电都通了,你只管装修)
依赖(Dependency)你的项目需要的别人的库菜谱里说"需要现成的酱油"

在本项目哪里用到:

  • 库:Gson(转 JSON,ch11)、llama.cpp(推理引擎,ch14);
  • 框架:Jetpack Compose(界面框架,ch06)、Gradle(构建框架,ch20);
  • 依赖:全部列在gradle/libs.versions.toml(ch02)。

A.1.8 SDK、API、IDE

词全称 / 含义一句话
SDKSoftware Development Kit一整套开发工具(编译器、工具、文档)
IDEIntegrated Development Environment写代码的软件(如 Android Studio)
APIApplication Programming Interface别人提供的"函数清单",你照它调用
编译Compile把源码"翻译"成机器能执行的形式
运行Run让程序真正执行

在本项目哪里用到:

  • SDK:Android SDK(ch02);
  • API:llama_decode、llama_model_load_from_file都是 llama.cpp 提供的 API(ch15);
  • 编译:Kotlin 编译、C++ 编译(ch20)。

A.2 命令行与文件路径

为什么必须懂命令行:本书大量操作用命令行完成(构建、校验、看日志)。
比"会用鼠标点"更重要的是理解路径——因为大部分报错都和路径有关。

A.2.1 什么是命令行

命令行(CLI)= 用打字给计算机下命令的界面(对比"用鼠标点"的图形界面)。

为什么开发常用命令行:

  • 可复制、可分享(把命令贴给同事就行,不用截图说明点了哪里);
  • 可脚本化(写进脚本,一次跑一批);
  • 信息更全(报错原文完整显示,不被界面省略)。

打开方式(Windows):按Win + R,输入powershell或cmd,回车。

A.2.2 最常用的几条命令

目的Windows (PowerShell)说明
看当前在哪pwd显示当前目录
进入某个目录cd C:\dev\llamacpptestchange directory
返回上一级cd ....表示"上一级"
列出目录内容ls(或dir)list
新建目录mkdir 新名字
复制文件copy 源 目标/Copy-Item
看文件内容type 文件/Get-Content小文件可以,大文件会刷屏
清屏cls

⚠️本书所有gradlew命令,都必须在llama.android目录下执行——
因为gradlew是那个目录里的文件。"在哪个目录执行"是新手最常见的错误来源之一。

A.2.3 路径:绝对路径 vs 相对路径

类型长什么样含义
绝对路径C:\dev\llamacpptest\llama.android从盘符(或根)开始,完整
相对路径llama.android、..\llama.cpp相对当前位置

两个特殊符号:

符号含义
.当前目录
..上一级目录

所以..\..\llama.cpp的意思是"上两级,再进 llama.cpp"。

在本项目哪里用到:ch20 讲的LLAMA_SRC就是
../../../../../llama.cpp(上溯 5 级)——数错层数是最常见的构建报错之一。

A.2.4 斜杠还是反斜杠?

场合写法例子
Windows 命令行的路径反斜杠\C:\dev\...
绝大多数编程语言/配置文件正斜杠/C:/dev/...、llama.android/app/...
Windows 其实也认正斜杠✅C:/dev也能用

💡建议:写代码/配置时一律用正斜杠/**。
它在 Windows、macOS、Linux 上都通用,能避免一类转义问题。

📌但有个例外:Java 的.properties文件里,反斜杠是转义字符,
所以写C:\dev\Android\Sdk会被解析错,必须写成C:\\dev\\Android\\Sdk或改用C:/dev/Android/Sdk。
这正是 ch02 讲的local.properties那个坑。

A.2.5 环境变量

环境变量 = 系统里的一些"全局设置",程序可以读它们。

例子:

  • JAVA_HOME:Java 装在哪;
  • PATH:系统去哪找可执行程序(所以你才能直接敲java)。

怎么读(PowerShell):

echo$env:JAVA_HOME

在本项目哪里用到:ch02 讲"为什么本项目不依赖 PATH,而是在gradle.properties里写死 JDK 路径"。


A.3 数字在计算机里怎么存

为什么必须懂:本书里反复出现0x47、24 字节、8192、f16(2 字节)、
小端序——不理解这些,ch14/ch16/ch18 会读不下去。

A.3.1 位(bit)与字节(byte)

单位含义
位(bit)最小的单位,只能是0或1
字节(byte)8 个位= 一个字节

换算:

1 字节(byte) = 8 位(bit) 1 KB = 1024 字节 1 MB = 1024 KB 1 GB = 1024 MB

💡为什么是 1024 而不是 1000?
因为计算机用二进制,1024 = 2¹⁰。
所以"2.0 GB 的模型"其实是 2 × 1024 × 1024 × 1024 ≈ 21.5 亿字节。

在本项目哪里用到:

  • ch01 讲"7B Q4 量化后约 3.5–4 GB";
  • ch10 讲"导入中途失败残留 GB 级半成品";
  • ch15 算"KV cache 约 4 GB"(2 × 32 × 8192 × 4096 × 2 字节)。

A.3.2 二进制与十六进制

二进制:只有0和1。计算机内部全是二进制。

十六进制:用0–9和A–F共 16 个符号表示,每 1 位十六进制 = 正好 4 位二进制,
所以它比二进制短,写起来方便。

十进制二进制十六进制
000000
101010A
151111F
161000010
710100 011147
2551111 1111FF

写法约定:0x开头表示十六进制。所以0x47= 十进制的 71。

在本项目哪里用到:
ch14 讲 GGUF 的 magic 是0x47 0x47 0x55 0x46——
翻成 ASCII 就是G G U F(见 A.4)。

A.3.3 “小端序”:多字节数字的存放顺序

一个数字如果超过 1 字节(比如 4 字节的整数),这几个字节谁在前?

方式规则
小端序(little-endian)低位字节放前面(低地址)
大端序(big-endian)高位字节放前面

例子:数字1用 4 字节存:

小端序:01 00 00 00 ← 数值 1 在最前面 大端序:00 00 00 01 ← 要看到最后才看到 1

这就是为什么 GGUF 的 version = 3 存成03 00 00 00。

在本项目哪里用到:ch14 的readLEUInt32(LE就是 little-endian);
读错字节序 = 全盘皆错。

A.3.4 浮点数与"精度"

浮点数= 带小数的数。计算机用"科学计数法"的形式存它。

关键问题:精度不同、占的字节数不同。

类型占字节大概能表示的位数常见称呼
FP32(单精度)4约 7 位有效数字float
FP16(半精度)2约 3–4 位f16、half
INT81整数 −128~127int8
INT40.5(“半个字节”)16 个取值Q4

📌这就是 ch01 讲的"量化":
把 FP16(2 字节)压成 INT4(0.5 字节),体积缩 4 倍。
所以"7B 模型 FP16 约 14 GB → Q4 约 3.5–4 GB"。

也解释了 ch15 的 KV cache 计算:KV cache 默认用 f16,所以每个数值算2 字节。


A.4 文本编码:为什么"一个汉字 3 字节"、为什么会乱码

为什么必须懂:ch16 讲了一个核心机制——“汉字被 token 切成两半导致乱码”,
以及 33 行的 UTF-8 校验函数。不懂编码就读不懂那一章。

A.4.1 计算机只认数字

字符(A、中、😀)在计算机里都是数字。
"编码"就是"字符 ↔ 数字"的对照规则。

A.4.2 ASCII:最早的编码

ASCII 只覆盖英文字母、数字、常用符号,每个字符用1 字节。

字符ASCII 码(十六进制)
G0x47
U0x55
F0x46
A0x41
00x30

所以 GGUF 文件的头 4 字节47 47 55 46就是 “GGUF”(✅ ch14 讲的就是这个)。

⚠️ASCII 的问题:只有 128 个字符,装不下中文、日文、emoji。

A.4.3 UTF-8:现代通用编码

UTF-8 的特点:变长编码——不同字符占不同字节数。

字符类型占字节例子
英文/数字(ASCII 范围)1a=61
拉丁扩展、希腊文等2é=C3 A9
中日韩汉字3中=E4 B8 AD
emoji 等4😀=F0 9F 98 80

这就是"一个汉字 3 个字节"的由来。

A.4.4 UTF-8 的字节结构(关键)

UTF-8 用字节的高几位来表示"这个字符总共几字节":

首字节模式表示后续字节数
0xxxxxxx单字节(ASCII)0
110xxxxx双字节1
1110xxxx三字节(汉字)2
11110xxx四字节(emoji)3
其它❌ 非法—

后续字节必须以10xxxxxx开头。

A.4.5 乱码是怎么产生的

如果一段字节不满足上面的规则,就不是合法的 UTF-8,
显示时通常变成�(乱码方块)。

在本项目里,这个"非法"是怎么产生的?
因为模型是按 token 输出的,而token 边界可能落在字符的中间:

"中" = E4 B8 AD 模型可能这样输出: token A → E4 B8 ← 只有 2 个字节,不是完整的 UTF-8 token B → AD ← 补上第 3 个字节

如果你在 token A 之后就把E4 B8转成字符串 → 非法 → 乱码方块。

✅ 本工程的解法(ch16.5):先攒着,等凑成合法 UTF-8 再发。
那个 33 行的is_valid_utf8函数做的就是"按上面的规则逐字节检查"。

💡一个记住它的办法:
“半个字不是字”—— 宁可晚一点显示,也不能显示半个字。


A.5 JSON:程序之间传数据的"通用格式"

为什么必须懂:ch11 讲持久化时,会话列表就是存成 JSON 的。

A.5.1 它长什么样

JSON 是一种文本格式,用来表示结构化数据:

{"id":"a1b2c3","title":"新对话","messages":[{"id":"m1","content":"你好","isUser":true},{"id":"m2","content":"你好呀","isUser":false}],"createdAt":1757000000000}

结构要素:

符号含义
{ }一个"对象"(一组键值对)
[ ]一个"列表"
"键": 值键值对(键必须是带引号的字符串)
值是字符串要带双引号
值是数字不用引号
值是真假true/false

A.5.2 序列化与反序列化

术语方向含义
序列化对象 → 文本把内存里的对象"拍扁"成 JSON 字符串
反序列化文本 → 对象把 JSON 字符串"复原"成对象

为什么需要它?
因为"存到磁盘"和"网络传输"都只能处理文本/字节,
而程序内部用的是对象。所以两边要转换。

在本项目哪里用到(ch11):

  • gson.toJson(list)→ 序列化(存盘);
  • gson.fromJson(json, ...)→ 反序列化(读盘)。

📌ch11 讲的"坏 JSON 兜底":因为 JSON 是纯文本,
可能被改坏、被截断。反序列化失败时必须兜住,否则整个列表读不出来。


A.6 CPU、内存、进程、线程

为什么必须懂:ch01 讲"为什么手机能跑大模型"、ch08 讲"为什么不能用 Thread"、
ch10 讲"ANR"——都建立在这些概念上。

A.6.1 CPU:负责"算"

CPU(中央处理器)= 计算机里负责执行指令、做运算的部件。

相关词含义
核心(core)CPU 里能独立干活的小单元(8 核 = 能同时做 8 件事)
主频每秒能做多少次操作(越高越快)
指令集CPU 认识哪些"指令"(不同 CPU 不一样)

在本项目里:

  • ch01 讲"本工程跑在 CPU 上"(不是 GPU);
  • ch15 讲"线程数 = 核数 − 2,最多 4";
  • ch20 讲的ABI(arm64-v8a / x86_64)就是"CPU 的架构/指令集":
    手机几乎都是 arm64,电脑/模拟器常见 x86_64。

A.6.2 内存(RAM):负责"记"

内存 = 程序运行时的"工作台",程序的数据要放在这里才能被 CPU 使用。

特点说明
快比磁盘快非常多
断电即失关机就没了(所以需要"存盘")
有限手机常见 8–16 GB

在本项目里:

  • ch01 讲"一个 4 GB 的模型在 8 GB 内存手机上基本跑不起来";
  • ch15 讲"内存占用 = 模型权重 + KV cache";
  • ch04/ch09 讲"切后台要释放模型腾内存"。

💡"存内存"和"存磁盘"的区别(本书反复出现):

内存(RAM)磁盘(存储)
速度极快慢很多
关机后没了还在
本项目例子聊天记录(ViewModel)聊天记录(DataStore)

这就是 ch04 讲的"旋转屏幕记录不丢(在内存里)“vs"退出重开还在(在磁盘里)”。

💡什么是"缓存(cache)"?
缓存 = “算过一次的东西先放着,下次直接用,不重算”。
它通常放在快的存储(内存)里,因为重算慢、重读快。
本书最大的缓存就是KV cache(ch15/ch18):
历史对话的计算结果存起来,每新说一句话只算新的,不用把整段历史重算一遍。

A.6.3 进程与线程

概念含义类比
进程(Process)一个正在运行的程序一家公司的办事处
线程(Thread)进程内的一条执行流办事处里的一个员工

关键点:

事实说明
一个进程可以有多个线程可以"同时"做几件事
同进程的线程共享内存所以能互相看到对方的数据(也所以要小心"抢"数据)
不同进程的内存是隔离的所以要通信必须走特殊机制(见 A.6.4)

在本项目里:

  • ch01 讲"整个 App 从头到尾只有一个InferenceEngine实例"(进程内单例);
  • ch08 讲"主线程不能被阻塞"、“用后台线程跑推理”;
  • ch12 讲"TTS 回调跑在 Binder 线程"——和主线程不同的线程,所以要加锁。

A.6.4 跨进程通信(IPC)与 Binder

因为不同进程的内存隔离,一个 App 想用系统或别的 App的能力,
就得通过一套通信机制——Android 上叫Binder。

Binder 的一个关键限制:一次能传的数据有大小上限(约 1MB)。

在本项目里:ch11/ch12 讲的"导出对话不能直接塞EXTRA_TEXT"——
就是因为长对话会超过 Binder 上限(TransactionTooLargeException),
所以要走"写文件 + 给一个content://URI"。

💡这也解释了为什么叫"跨进程":
“把文件分享给别的 App” = 两个不同的进程在通信。


A.7 "编译"是怎么把代码变成 App 的

为什么必须懂:ch01/ch20 讲构建流程,ch13 讲"为什么要有 JNI"。

A.7.1 三种执行方式

方式过程例子
编译型源码 →机器码→ 直接给 CPU 执行C / C++
解释型源码 → 逐行解释执行Python
混合型(虚拟机)源码 → 中间码 → 虚拟机执行Java / Kotlin(跑在 ART 上)

在本项目里:

  • Kotlin跑在 Android 的ART 虚拟机上(ch01);
  • C++编成机器码直接跑(ch13);
  • 两者"语言、内存、调用规则都不一样",所以要JNI 桥(ch13)。

A.7.2 本项目的两条编译线

Kotlin 代码 → Kotlin 编译器 → .class → D8/R8 → classes.dex ┐ ├→ APK C++ 代码 → CMake + clang++ → .so(动态库) ┘ 资源(图标) → aapt2 → 编译后的资源 ┘

这就是 ch20 讲的三条构建线。

A.7.3 什么是 APK

APK = Android 的安装包,本质是个zip 压缩包,里面装:

内容是什么
classes*.dex编译后的 Kotlin/Java 代码
lib/<架构>/*.so编译后的 C++ 库
res/资源(图标、文字等)
resources.arsc资源索引表
AndroidManifest.xml那个"身份证"(ch04)

📌所以unzip -l xxx.apk能看到它的内部——
ch20/ch21 讲"判断 ABI 只看 APK",用的就是这条。

A.7.4 什么是 Gradle

Gradle = 构建工具:管"按什么顺序、用什么工具、把代码变成 APK"。

它做的事:

  • 下载依赖;
  • 调用编译器;
  • 打包资源;
  • 生成 APK;
  • 签名。

在本项目里:gradlew就是"用工程自带版本的 Gradle"(ch02)。


A.8 Git 最小必需

为什么必须懂:本项目用 Git 管代码;书里也建议"每次只改一个变量"(ch22),
而 Git 正是那个"能精确回退"的工具。

A.8.1 Git 是什么

Git = 版本控制工具:记录"每一次改动",让你能:

能力有什么用
看历史三天前改了什么
对比差异这次改动了哪些行
回退改坏了,退回上一个能用的状态
分支同时试验多个方案

💡类比:Git 像游戏的"存档点"。
每到一个里程碑就存一次档,玩崩了能读回去。
"不能回退"是新手最大的恐惧来源——有了 Git,这个恐惧就没了。

A.8.2 四条最常用的命令

命令作用
git status看当前有哪些改动(最常用,能救命)
git diff看具体改了什么内容
git add .+git commit -m "说明"把当前改动"存一个档"
git log --oneline看历史存档列表

注意:commit只存在本地,不会自动上传。

A.8.3 关于clone

git clone <仓库地址>= 把一个远程仓库完整复制到本地(含所有历史)。

为什么很多教程让你先clone?
因为这样你拿到的不只是代码,还有完整历史——
能看到"这个文件是怎么一步步变成现在这样的"。这是学习真实项目的最好材料。

📌本项目的一个相关约定(ch21 强调过):
密钥文件(*.jks)和keystore.properties绝对不能提交到 Git。
因为它们一旦进了仓库,就等于"把公章复印件发给了所有能看到仓库的人"。


A.9 读这本书的建议姿势

A.9.1 遇到不懂的词怎么办

① 先查本附录(A.1–A.8) ② 再查《附录 B 术语表》(中英对照 + 一句话 + 出处章节) ③ 还没懂 → 去看那一章(术语表会写明"在哪一章详细讲") ④ 还是不懂 → 看《附录 C 常见问题》

A.9.2 三条"不要"

不要因为
不要试图一次记住所有词术语是在使用中记住的,不是背出来的
不要跳过"动手验证"那才是"学会"和"看懂"的分界线(ch23 讲的)
不要怕报错报错是信息,不是失败。本书每章都有"如果失败了怎么办"

A.9.3 一条"要"

要尽早让项目在你机器上跑起来。
哪怕那时还看不懂代码。
因为"跑起来"会给你正反馈,而且后面每次调试都有个可用的基线。

(这一条在 ch01 也强调过,值得再提一次。)


A.10 小结:这个附录讲了多少个概念

小节概念关键结论
A.1变量/函数/条件/循环/数据结构/类/库框架/SDK·API·IDE程序是"按顺序的说明书"
A.2命令行/路径/斜杠/环境变量"在哪个目录执行"很重要
A.3bit/byte/二进制/十六进制/小端序/浮点精度1 字节 = 8 位;0x47=G
A.4ASCII/UTF-8/乱码一个汉字 3 字节,“半个字不是字”
A.5JSON/序列化存盘和传输只能处理文本
A.6CPU/内存/进程/线程/Binder内存快但易失;Binder 有 1MB 上限
A.7编译/APK/GradleAPK 是个 zip
A.8Git存档点,让你敢改代码
A.9学习姿势别背术语、别跳验证、别怕报错

读懂了这个附录,你就可以开始读 ch01 了。

最后提醒:这个附录不是"读完就过"的。
读正文时遇到不懂的词,回来查——
用的次数多了,自然就记住了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进