Android A/B 系统基础入门系列《Android A/B 系统》已完结,文章列表:
Android A/B System OTA分析(一)概览
Android A/B System OTA分析(二)系统image的生成
Android A/B System OTA分析(三)主系统和bootloader的通信
Android A/B System OTA分析(四)系统的启动和升级
Android A/B System OTA分析(五)客户端参数
Android A/B System OTA分析(六)如何获取 payload 的 offset 和 size
更多关于 Android OTA 升级相关文章,请参考《Android OTA 升级系列专栏文章导读》。
0. 导读
针对群里特别多人问 update_engine_client 的 offset 和 size 参数问题,特地开辟两篇文章全面讲述 offset 和 size 参数,主要包括传入的 offset 和 size 参数在代码中是如何流转的,offset 和 size 参数最终做什么用途?以及如何计算要传入的 offset 和 size 参数?
本篇从命令行开始,一步一步向下跟踪 offset 和 size 参数的传递,由于涉及具体的代码分析,所以整个文章比较繁琐。主要流程包括,命令行参数到客户端参数解析,客户端如何将参数通过 binder 服务传递给服务端,服务端再将参数设置到 HttpFetcher,然后在具体的实现中根据是本地文件还是远程文件进行不同的处理。
不论是本地文件还是远程文件,offset 和 size 都用于指定 payload 数据读取时的偏移量,以及读取数据的大小。
如果你想了解 offset 和 size 被传递和解析过程,那么本篇文章适合你。如果你只关心大概结论,请跳转到 4.3 offset 和 size 总结 以及 5. 总结 查看相关结论。
本文涉及的Android代码版本:android‐7.1.1_r23 (NMF27D)
1. update_engine_client 客户端支持的参数
在上一篇《Android A/B System OTA 分析(四)系统的启动和升级》中,提供了一个使用 update_engine_client 升级的例子:
bcm7252ssffdr4:/ # update_engine_client \
--payload=http://stbszx-bld-5/public/android/full-ota/payload.bin \
--update \
--headers="\
FILE_HASH=ozGgyQEcnkI5ZaX+Wbjo5I/PCR7PEZka9fGd0nWa+oY=
FILE_SIZE=282164983
METADATA_HASH=GLIKfE6KRwylWMHsNadG/Q8iy5f7ENWTatvMdBlpoPg=
METADATA_SIZE=21023
"
这里传入了 --payload, --update 和 --headers 三个参数。
update_engine_client 支持的全部参数如下:
update_engine_client --help
Android Update Engine Client
--cancel (Cancel the ongoing update and exit.) type: bool default: false
--follow (Follow status update changes until a final state is reached. Exit status is 0 if the update succeeded, and 1 otherwise.) type: bool default: false
--headers (A list of key-value pairs, one element of the list per line. Used when --update is passed.) type: string default: ""
--help (Show this help message) type: bool default: false
--offset (The offset in the payload where the CrAU update starts. Used when --update is passed.) type: int64 default: 0
--payload (The URI to the update payload to use.) type: string default: "http://127.0.0.1:8080/payload"
--reset_status (Reset an already applied update and exit.) type: bool default: false
--resume (Resume a suspended update.) type: bool default: false
--size (The size of the CrAU part of the payload. If 0 is passed, it will be autodetected. Used when --update is passed.) type: int64 default: 0
--suspend (Suspend an ongoing update and exit.) type: bool default: false
--update (Start a new update, if no update in progress.) type: bool default: false
2. update_engine_client 的参数是如何解析的?
在终端调用 update_engine_client 进行升级的流程跟踪分析请参考《Android Update Engine 分析(三)客户端进程》,下面将命令行参数的流转流程总结如下:
2.1 客户端命令行传入参数进行升级
# 终端传入参数调用 update_engine_client 升级
update_engine_client \
----payload=http://192.168.1.200/ota_package/payload.bin \
--update \
--headers="\
FILE_HASH=R7FKJ+WstITiZpW2/nMIl6Duln+bwgLnTWy0W1d79cg=
FILE_SIZE=2607014024
METADATA_HASH=eW3P9td/sH6dV0es1gf5t0N1U3iE/CMzdXZpmi1gKUA=
METADATA_SIZE=180056
"
2.2 客户端解析并使用参数
客户端实际是在 UpdateEngineClientAndroid::OnInit() 函数中解析并使用参数的,如下:
// 文件: system/update_engine/update_engine_client_android.cc
int UpdateEngineClientAndroid::OnInit() {
...
// 以下定义了 FLAGS_payload, FLAGS_offset, FLAGS_size, FLAGS_headers 等接收命令行的参数
DEFINE_string(payload,
"http://127.0.0.1:8080/payload",
"The URI to the update payload to use.");
DEFINE_int64(offset, 0,
"The offset in the payload where the CrAU update starts. "
"Used when --update is passed.");
DEFINE_int64(size, 0,
"The size of the CrAU part of the payload. If 0 is passed, it "
"will be autodetected. Used when --update is passed.");
DEFINE_string(headers,
"",
"A list of key-value pairs, one element of the list per line. "
"Used when --update is passed.");
...
// 在指定了 "--update" 进行升级时,将命令行参数传递给 binder 服务的 applyPayload 函数
if (FLAGS_update) {
...
Status status = service_->applyPayload(
android::String16{FLAGS_payload.data(), FLAGS_payload.size()},
FLAGS_offset,
FLAGS_size,
and_headers);
...
}
命令行参数为什么是在这里解析,具体分析请参考:请参考《Android Update Engine 分析(三)客户端进程》
客户端将参数解析后将 payload, offset, size 和 headers 参数传递给 binder 服务。
2.3 binder 服务将参数传递给服务端
// 文件: system/update_engine/binder_service_android.cc
Status BinderUpdateEngineAndroidService::applyPayload(
const android::String16& url,
int64_t payload_offset,
int64_t payload_size,
const std::vector<android::String16>& header_kv_pairs) {
...
// Binder 服务将从客户端拿到的参数传递给服务端的 ApplyPayload 接口
if (!service_delegate_->ApplyPayload(
payload_url, payload_offset, payload_size, str_headers, &error)) {
return ErrorPtrToStatus(error);
}
return Status::ok();
}
在这里 binder 将参数转发给服务端进程。
2.4 服务端处理升级参数
服务端的 UpdateAttempterAndroid::ApplyPayload() 函数最终处理传入的 payload, offset, size 和 headers 参数:
// 文件: system/update_engine/update_attempter_android.cc
bool UpdateAttempterAndroid::ApplyPayload(
const string& payload_url,
int64_t payload_offset,
int64_t payload_size,
const vector<string>& key_value_pair_headers,
brillo::ErrorPtr* error) {
...
install_plan_.download_url = payload_url;
...
base_offset_ = payload_offset;
install_plan_.payload_size = payload_size;
if (!install_plan_.payload_size) {
if (!base::StringToUint64(headers[kPayloadPropertyFileSize/*"FILE_SIZE"*/],
&install_plan_.payload_size)) {
install_plan_.payload_size = 0;
}
}
...
BuildUpdateActions(payload_url);
...
}
从上面的代码里,我们最终看到:
* 命令行参数 --payload 在这里用于设置 install_plan_.download_url,也传递给 BuildUpdateActions(payload_url) 设置 DownloadAction。
* 命令行参数 --offset 在这里用于设置 base_offset_
* 命令行参数 --size 在这里用于设置 install_plan_.payload_size
如果没有提供 --size 参数,则会从属性值中提取 FILE_SIZE 作为 payload_size
对于上面这些参数,问得最多的有以下两个:
1. 除了使用 http 获取远程文件进行升级外,可以使用本地文件进行升级吗?
2. 如果使用 update.zip 文件,如何设置 offset 和 size 参数?
3. 使用远程文件和本地文件升级
3.1 对 download_url 的处理
// 文件: system/update_engine/update_attempter_android.cc
void UpdateAttempterAndroid::BuildUpdateActions(const string& url) {
...
// 如果是 "file:///" 这样的格式,使用 FileFetcher
if (FileFetcher::SupportedUrl(url)) {
DLOG(INFO) << "Using FileFetcher for file URL.";
download_fetcher = new FileFetcher();
} else {
// 其余的使用 LibcurlHttpFetcher,获取网络数据
LibcurlHttpFetcher* libcurl_fetcher =
new LibcurlHttpFetcher(&proxy_resolver_, hardware_);
libcurl_fetcher->set_server_to_check(ServerToCheck::kDownload);
download_fetcher = libcurl_fetcher;
}
...
}
仔细查看这个函数传入的 url 参数并没有用来传递给某些变量,而仅仅专递给函数调用 FileFetcher::SupportedUrl(url) 用于判断是是不是 file:/// 开头的文件协议。真正被用来获取数据的,还是 install_plan_.download_url。
因此,升级既可以使用本地文件(“file:///” 协议);也可以使用远程文件(“http://” 或 “https://” 协议)进行升级。
代码中支持 “file:///”, “http://”, “https://”, “file://”, 第一个走本地文件读取路径, 后三个走 curl 读取路径。
但实际上我没有使用 “https://” 和 “file://” 验证过。至于 curl 是否支持其它协议,我还没有试过,如果你有试过,欢迎群里一起讨论。
3.2 远程文件升级示例
从远程 http://192.168.1.200/full_0814.zip 获取升级数据:
update_engine_client \
--payload=http://192.168.1.200/update.zip \
--update \
--offset=7985 \
--size=1096237091 \
--headers="\
FILE_HASH=fyDltdH3RkMxjJMLKWMU8SAkeWlnp+Dxb42jQpo30zc=
FILE_SIZE=1096237091
METADATA_HASH=72+DLYstrkKDp41oTV0xMCJtAIH5YAIs4Mw/4VSUXbY=
METADATA_SIZE=125561
"
3.3 本地文件升级示例
从本地 file:///data/ota_package/payload.bin 获取升级数据:
update_engine_client \
--payload=file:///data/ota_package/payload.bin \
--update \
--headers="\
FILE_HASH=R7FKJ+WstITiZpW2/nMIl6Duln+bwgLnTWy0W1d79cg=
FILE_SIZE=2607014024
METADATA_HASH=eW3P9td/sH6dV0es1gf5t0N1U3iE/CMzdXZpmi1gKUA=
METADATA_SIZE=180056
"
4. 如何设置 offset 和 size 参数?
从第 3 节的两个例子可见,升级时既可以使用 payload.bin 文件,也可以用脚本生成的 update.zip 文件。
使用不同格式的文件,就涉及到另外两个参数 offset 和 size 的设置。
* 使用 payload.bin 该如何设置 offset 和 size?
* 使用 update.zip 又该如何设置 offset 和 size?
想要知道如何设置 offset 和 size 参数,就需要知道这两个参数是用来做什么的,包括两个问题:在哪里使用,以及如何使用?
4.1 offset 和 size 参数的使用
在前面 2.4 节我们分析过传入的 payload, offset 和 size 参数在服务端代码 UpdateAttempterAndroid::ApplyPayload() 中被使用。
我们具体看下 offset 和 size 是如何被使用的:
// 文件: system/update_engine/update_attempter_android.cc
bool UpdateAttempterAndroid::ApplyPayload(
const string& payload_url,
int64_t payload_offset,
int64_t payload_size,
const vector<string>& key_value_pair_headers,
brillo::ErrorPtr* error) {
...
install_plan_.download_url = payload_url;
...
base_offset_ = payload_offset;
install_plan_.payload_size = payload_size;
if (!install_plan_.payload_size) {
if (!base::StringToUint64(headers[kPayloadPropertyFileSize/*"FILE_SIZE"*/],
&install_plan_.payload_size)) {
install_plan_.payload_size = 0;
}
}
...
BuildUpdateActions(payload_url);
...
}
这里 payload_offset 和 payload_size 分别被设置给 base_offset_ 和 install_plan_.payload_size 变量。
代码中搜索 base_offset_,发现只在 UpdateAttempterAndroid::SetupDownload() 函数中被使用,被用来传递给 fetcher->AddRange 函数:
system/update_engine$ grep -Rnw base_offset_ .
./update_attempter_android.h:158: int64_t base_offset_{0};
./update_attempter_android.cc:146: base_offset_ = payload_offset;
./update_attempter_android.cc:495: fetcher->AddRange(base_offset_,
./update_attempter_android.cc:505: fetcher->AddRange(base_offset_ + resume_offset);
./update_attempter_android.cc:507: fetcher->AddRange(base_offset_ + resume_offset,
./update_attempter_android.cc:512: fetcher->AddRange(base_offset_, install_plan_.payload_size);
./update_attempter_android.cc:516: fetcher->AddRange(base_offset_);
grep: ./.clang-format: No such file or directory
函数 UpdateAttempterAndroid::SetupDownload() 的实现如下:
void UpdateAttempterAndroid::SetupDownload() {
MultiRangeHttpFetcher* fetcher =
static_cast<MultiRangeHttpFetcher*>(download_action_->http_fetcher());
fetcher->ClearRanges();
// 如果 install_plan_.is_resume 为 true, 即 resume 的情况
if (install_plan_.is_resume) {
// Resuming an update so fetch the update manifest metadata first.
int64_t manifest_metadata_size = 0;
int64_t manifest_signature_size = 0;
prefs_->GetInt64(kPrefsManifestMetadataSize, &manifest_metadata_size);
prefs_->GetInt64(kPrefsManifestSignatureSize, &manifest_signature_size);
fetcher->AddRange(base_offset_,
manifest_metadata_size + manifest_signature_size);
// If there're remaining unprocessed data blobs, fetch them. Be careful not
// to request data beyond the end of the payload to avoid 416 HTTP response
// error codes.
int64_t next_data_offset = 0;
prefs_->GetInt64(kPrefsUpdateStateNextDataOffset, &next_data_offset);
uint64_t resume_offset =
manifest_metadata_size + manifest_signature_size + next_data_offset;
if (!install_plan_.payload_size) {
fetcher->AddRange(base_offset_ + resume_offset);
} else if (resume_offset < install_plan_.payload_size) {
fetcher->AddRange(base_offset_ + resume_offset,
install_plan_.payload_size - resume_offset);
}
} else {
// 这里 install_plan_.is_resume 为 false,表示非 resume 情况,比如刚开始下载
if (install_plan_.payload_size) {
fetcher->AddRange(base_offset_, install_plan_.payload_size);
} else {
// If no payload size is passed we assume we read until the end of the
// stream.
fetcher->AddRange(base_offset_);
}
}
}
简单来说,上面这个函数针对第一次 download,还是暂停后恢复 download 设置提取数据的范围,包括 offset 和 size。
1. 设置第一次下载的 offset 和 size
对于第一次下载,直接用 base_offset_ 和 install_plan_.payload_size 进行设置,如果没有 payload_size,则只传递 base_offset_ 给 fetcher,意思就是告诉 fetcher 从 base_offset_ 开会,一直下载到数据结束。
if (install_plan_.payload_size) {
fetcher->AddRange(base_offset_, install_plan_.payload_size);
} else {
// If no payload size is passed we assume we read until the end of the
// stream.
fetcher->AddRange(base_offset_);
}
2. 调整中断后恢复下载的 offset 和 size
对于中断后恢复下载的情况,无非就是对 base_offset_ 和 payload_size 进行调整。
// 中断后恢复下载,计算恢复后的 resume_offset,然后使用 resume_offset 对 base_offset_ 和 install_plan_.payload_size 进行调整
uint64_t resume_offset =
manifest_metadata_size + manifest_signature_size + next_data_offset;
if (!install_plan_.payload_size) {
fetcher->AddRange(base_offset_ + resume_offset);
} else if (resume_offset < install_plan_.payload_size) {
fetcher->AddRange(base_offset_ + resume_offset,
install_plan_.payload_size - resume_offset);
}
3. fetch 是什么?AddRange又做了什么?
前面提到 offset 和 size 被传递给 fetcher->AddRange() 调用。
这里的 fetcher 是 MultiRangeHttpFetcher,从定义看,其包装了一个 HttpFetcher。
调用 fetcher->AddRange(offset, size) 时,将 offset 和 size 参数存放到私有成员变量 ranges_ 中。
void AddRange(off_t offset, size_t size) {
CHECK_GT(size, static_cast<size_t>(0));
// 这里参数 (offset, size) 用于设置 range 的 (offset, length)
ranges_.push_back(Range(offset, size));
}
void AddRange(off_t offset) {
ranges_.push_back(Range(offset));
}
然后在传输开始时,调用 MultiRangeHttpFetcher::StartTransfer() 将上面提到的 ranges_, 将其传递给子类 base_fetcher_ 去设置实际传输的 offset 和 length。
// State change: Stopped or Downloading -> Downloading
void MultiRangeHttpFetcher::StartTransfer() {
if (current_index_ >= ranges_.size()) {
return;
}
Range range = ranges_[current_index_];
LOG(INFO) << "starting transfer of range " << range.ToString();
// 将 offset 传递给具体使用的 base_fetcher
bytes_received_this_range_ = 0;
base_fetcher_->SetOffset(range.offset());
// 将 length 传递给具体使用的 base_fetcher
if (range.HasLength())
base_fetcher_->SetLength(range.length());
else
base_fetcher_->UnsetLength();
if (delegate_)
delegate_->SeekToOffset(range.offset());
base_fetcher_active_ = true;
base_fetcher_->BeginTransfer(url_);
}
这里的 base_fetcher_ 对象又是什么呢?
base_fetcher 就是在 UpdateAttempterAndroid::BuildUpdateActions(url) 调用时根据 url 构造的 Fetcher:
* 如果是 file:/// 协议,则是 FileFetcher
* 如果是其他协议(如: http://), 则是 LibcurlHttpFetcher
4.2 FileFetcher 和 LibcurlHttpFetcher
前面分析到,offset 和 size 参数最终会传递给底层的 FileFetcher 或 LibcurlHttpFetcher 的 SetOffset() 和 SetLength() 函数。
这两个底层的 Fetcher 类又是如何处理的呢?
1. FileFetcher
// 文件: system/update_engine/common/file_fetcher.h
void SetOffset(off_t offset) override { offset_ = offset; }
void SetLength(size_t length) override { data_length_ = length; }
void UnsetLength() override { SetLength(0); }
FileFetcher 的 SetOffset(offset) 和 SetLength(length) 接口会设置内部的 offset_ 和 data_length_ 成员,这些成员在 BeginTransfer() 和 ScheduleRead() 中被使用:
// Begins the transfer, which must not have already been started.
void FileFetcher::BeginTransfer(const string& url) {
...
// 根据传入的 url 打开相应的文件
string file_path = url.substr(strlen("file://"));
stream_ =
brillo::FileStream::Open(base::FilePath(file_path),
brillo::Stream::AccessMode::READ,
brillo::FileStream::Disposition::OPEN_EXISTING,
nullptr);
...
// 用 offset_ 设置文件读取开始的位置
if (offset_)
stream_->SetPosition(offset_, nullptr);
...
}
void FileFetcher::ScheduleRead() {
...
// 根据 data_length_ 设置需要读取的数据长度
if (data_length_ >= 0) {
bytes_to_read = std::min(static_cast<uint64_t>(bytes_to_read),
data_length_ - bytes_copied_);
}
...
}
这里:
1. 传输开始时,FileFetcher::BeginTransfer() 设置文件数据读取的起始位置 offset
2. 传输中,FileFetcher::ScheduleRead() 设置文件数据读取的长度 length
2. LibcurlHttpFetcher
// 文件: system/update_engine/libcurl_http_fetcher.h
void SetOffset(off_t offset) override { bytes_downloaded_ = offset; }
void SetLength(size_t length) override { download_length_ = length; }
void UnsetLength() override { SetLength(0); }
LibcurlHttpFetcher 的 SetOffset(offset) 和 SetLength(length) 接口会设置内部的 bytes_downloaded_ 和 download_length_ 成员。这些成员在 ResumeTransfer() 和 LibcurlWrite() 中被使用。
// 文件: system/update_engine/libcurl_http_fetcher.cc
void LibcurlHttpFetcher::ResumeTransfer(const string& url) {
...
// 根据 bytes_downloaded_ 和 download_length_ 设置 curl 操作读取数据的 range 范围
if (bytes_downloaded_ > 0 || download_length_) {
// Resume from where we left off.
resume_offset_ = bytes_downloaded_;
CHECK_GE(resume_offset_, 0);
// Compute end offset, if one is specified. As per HTTP specification, this
// is an inclusive boundary. Make sure it doesn't overflow.
size_t end_offset = 0;
if (download_length_) {
end_offset = static_cast<size_t>(resume_offset_) + download_length_ - 1;
CHECK_LE((size_t) resume_offset_, end_offset);
}
// Create a string representation of the desired range.
string range_str = base::StringPrintf(
"%" PRIu64 "-", static_cast<uint64_t>(resume_offset_));
if (end_offset)
range_str += std::to_string(end_offset);
CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_RANGE, range_str.c_str()),
CURLE_OK);
}
...
}
size_t LibcurlHttpFetcher::LibcurlWrite(void *ptr, size_t size, size_t nmemb) {
...
// curl 每次接收到数据后写入本地缓冲,调整 bytes_downloaded_,即下次获取数据的偏移
bytes_downloaded_ += payload_size;
...
return payload_size;
这里:
1. 传输中,LibcurlHttpFetcher::ResumeTransfer() 根据 bytes_downloaded_(offset) 和 download_length_(size) 设置 curl 获取远程数据的范围
2. 将接收数据写入缓冲区后,LibcurlHttpFetcher::LibcurlWrite() 调整 bytes_downloaded_(offset),设置 curl 随后获取数据的 offset
4.3 offset 和 size 总结
整个 offset 和 size 参数跟踪的路径比我想象的要长不少,前面分析得比较啰嗦,这里捡重点说一下。
命令行传入升级文件的地址和 offset 以及 size 参数后,代码根据升级文件地址是本地(“file:///”)还是远程,使用不同的方式读取文件。
本地文件则直接打开文件进行读取,远程则使用 curl 工具进行获取。
不论是读取本地文件还是获取远程文件,目标都是要获取有效的 payload 数据,因此有三个必须的参数,那就是:
1. 读哪个文件?由命令行的 payload 参数指定
2. 从哪里开始读取?由命令行的 offset 参数指定
3. 读取多少内容?由命令行的 size 参数指定
对于 payload.bin 文件,因为是原始的 payload 文件,所以 offset 为 0, size 为整个 payload.bin 文件的大小,如果没有提供 size, 默认就读取文件直到文件结束。
对于 update.zip 文件,里面包含了压缩的 payload.bin 文件,因为是压缩过的 payload,所以为了读取完整的 payload,必须提供 payload 在 zip 包的偏移位置,以及 payload 在 zip 包压缩后的数据大小。
再强调一次,对于 update.zip,需要提供 payload 在 zip 文件中的偏移(offset),以及压缩后的大小(size)。如果 offset 不正确,则解析不到正确的数据。如果 size 太小,则获取的数据不完整,size 太大没有问题,只不过会多获取一些没用的数据,浪费带宽。
至于 update.zip 包中,payload 文件的 offset 和 size 要如何计算,另外单独开一篇文章详细说明。
5 总结
Android 升级客户端例子 update_engine_client 支持本地文件和远程文件升级,同时也支持使用原始的 payload.bin 文件或压缩包 update.zip 文件。
1. 对于使用 payload.bin 文件升级,可以不用传递 offset 和 size 参数 (offset 默认为 0, size 参数从 headers 的 “FILE_SIZE” 中提取)。
2. 对于使用 update.zip 文件升级,必须需要传递 payload 数据在 update.zip 中的起始位置 (offset) 和大小 (size)。
如果没有传递 size 参数,代码会从 headers 的 “FILE_SIZE” 提取,因为 update.zip 中的 payload 一般是经过压缩的,所以 update.zip 包中 payload 数据一般会小于 “FILE_SIZE” 值。
对于使用本地文件,远程文件,原始的 payload.bin 和升级包 update.zip 进行升级,参考下面两个例子:
使用本地的 payload.bin 文件升级:
update_engine_client \
--payload=file:///data/ota_package/payload.bin \
--update \
--headers="\
FILE_HASH=R7FKJ+WstITiZpW2/nMIl6Duln+bwgLnTWy0W1d79cg=
FILE_SIZE=2607014024
METADATA_HASH=eW3P9td/sH6dV0es1gf5t0N1U3iE/CMzdXZpmi1gKUA=
METADATA_SIZE=180056
"
使用远程的 update.zip 包升级:
update_engine_client \
--payload=http://192.168.1.200/update.zip \
--update \
--offset=7985 \
--size=1096237091 \
--headers="\
FILE_HASH=fyDltdH3RkMxjJMLKWMU8SAkeWlnp+Dxb42jQpo30zc=
FILE_SIZE=1096237091
METADATA_HASH=72+DLYstrkKDp41oTV0xMCJtAIH5YAIs4Mw/4VSUXbY=
METADATA_SIZE=125561
"
对于使用 update.zip 包升级时的 offset 和 size 参数如何获取,请参考下一篇文章。
6. 其它
到目前为止,我写过 Android OTA 升级相关的话题包括:
基础入门:《Android A/B 系统》系列
核心模块:《Android Update Engine 分析》 系列
动态分区:《Android 动态分区》 系列
虚拟 A/B:《Android 虚拟 A/B 分区》系列
升级工具:《Android OTA 相关工具》系列
更多这些关于 Android OTA 升级相关文章的内容,请参考《Android OTA 升级系列专栏文章导读》。
————————————————
版权声明:本文为CSDN博主「洛奇看世界」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/guyongqiangx/article/details/122430246