谷歌Android增量文件系统incfs(Incremental File System )

业务背景

官网功能介绍:边下载边玩

“下载即玩”是 Google Play 商业提供的一项新服务,“将允许用户在几秒钟内进入游戏,而游戏资产在后台下载。” 谷歌开发这种新的“下载时播放功能”的原因很简单。“用户希望立即开始玩游戏,但随着游戏质量的不断提高,它们的大小会增加,从而导致下载时间过长而乏味,”谷歌解释道。这个概念本身并不新鲜——索尼的 PlayStation 和微软的 Xbox 游戏机多年来一直提供类似的功能——但直到最近才在 Android 上对它的需求不大,因为大多数手机游戏的尺寸都比游戏机或游戏机小得多。电脑游戏。

谷歌表示,所有通过 Play 商店发布应用和游戏的开发者都可以使用“下载即玩”,一旦实施,游戏“准备好以至少 2 倍的速度打开”。在某些情况下,这种改进可能非常显着,谷歌表示 400MB 大小的游戏只需 10 秒即可加载,而不是几分钟。

只有运行最新版 Android 的设备才能利用这一新功能,原因是该功能“内置于 Android 12 的核心中”。谷歌向XDA证实,在幕后,“下载时播放”利用了Android 的增量文件系统(Incremental File System),这是一个“特殊用途的 Linux 虚拟文件系统,允许在其二进制文件和资源文件仍在运行时执行程序。懒惰地通过网络下载。”

尽管“下载即玩”对所有应用程序开发人员开放,但它要求开发人员使用 Android App Bundle 格式构建他们的应用程序。这是因为该功能使用了Play Asset Delivery,这需要应用程序包格式。2021 年 8 月 1 日之后提交到 Google Play 的新应用必须使用 Android App Bundle 格式,但一些开发者对此要求并不满意。如果 App Bundle 要求不打扰您,那么“边下载边玩”将是一个不错的功能,可以让用户更快地进入游戏的游戏循环。

增量文件系统(Incremental File System)

Incremental FS(IncFS)是谷歌在2019年提出的一个计划用于Android应用下载更新的文件系统[2]。IncFS不允许直接写入文件,并且追加写后的内容也永不改变。它允许在大型Android应用程序的二进制文件和资源没有完全加载到Android设备上之前运行。如果应用读取的内容尚未加载,它需要等待数据块被提取,但是在大多数情况下是热块可以预先加载,并且应用程序几乎可以立即流畅运行。

增量文件系统官方介绍

一、增量文件系统特点


● 只读文件,读/写文件系统
● 文件可以在块级别以增量和无序方式传递
● 读取文件未传递的部分会触发对用户空间的通知,并保持读取直到完成或超时
● 文件内容可以压缩
● 读取时验证文件
● 文件数量不受限制

二、增量文件系统实现


● 堆叠文件系统
● 每个文件在底层文件系统中都有对应的文件
● 目录操作(移动、删除、链接等)是直通的
● 读取解释底层文件并返回正确的数据
● 写入经过特殊解释并提供增量交付的通道
● 存在索引目录来管理增量交付
● 两个命令文件和两个 IOCTL 完善了接口

三、增量文件系统架构

如下图所示,IncFS工作在其他文件系统之上(如:EXT4,F2FS),大部分操作可以透传给下层的文件系统。对于读操作如果需要的数据还没有完成加载,IncFS会通知上层应用等待直到超时,读到数据后需要进行校验;写操作需要记录数据块同时更新索引。数据块是被压缩存储的,在读的时候解压后返回给应用。读操作会被记录到日志文件,用于后续优化应用更新时数据块的下载顺序。

四、增量文件系统 (IncFS) 内核模块

官方文档入口

通过 Android 11 中引入的增量文件系统 (IncFS) 内核模块,Android 操作系统能够通过 Android 调试桥 (adb) 来接收流式传输 APK。

这个独立的内核模块会创建一个基于现有 Android 文件系统的新虚拟文件系统。这是对框架中以及 SDK 中的变更的补充,使应用和游戏开发者能够通过 adb 将大型 APK 部署到搭载 Android 11 或更高版本的设备。

通过内核变更采用了新的 APK 签名方案 v4 格式,为 Android 软件包管理器中的 Android 框架变更、新的系统服务以及 adb 的变更提供支持。

五、增量文件系统 (IncFS) Framework内部,涉及安装服务

作为一个独立famework服务存在,但是对开发者是隐藏的。

使用方法

IncrementalManager manager = (IncrementalManager) getSystemService(Context.INCREMENTAL_SERVICE);

* IncrementalStorage storage = manager.openStorage("/path/to/incremental/dir");

源码在frameworks/base/core/java/android/os/incremental/IncrementalManager.java

IncrementalManager

https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:frameworks/base/core/java/android/os/incremental/IncrementalManager.java;l=61?q=Incremental&ss=android%2Fplatform%2Fsuperproject关键方法,注册进度回调

  /**
     * Called when a new callback wants to listen to the loading progress of an installed package.
     * Increment the count of callbacks associated to the corresponding storage.
     * Only register storage listener if there hasn't been any existing callback on the storage yet.
     * @param codePath Path of the installed package. This path is on an Incremental Storage.
     * @param callback To report loading progress to.
     * @return True if the package name and associated storage id are valid. False otherwise.
     */
    public boolean registerLoadingProgressCallback(@NonNull String codePath,
            @NonNull IPackageLoadingProgressCallback callback) {
        final IncrementalStorage storage = openStorage(codePath);
        if (storage == null) {
            // storage does not exist, package not installed
            return false;
        }
        return mLoadingProgressCallbacks.registerCallback(storage, callback);
    }

frameworks/base/services/core/java/com/android/server/pm/LauncherAppsService.java

APP在安装增量APP时,会注册一个增量文件的进度回调

 /**
         * Check all installed apps and if a package is installed via Incremental and not fully
         * loaded, register loading progress listener.
         */
        void registerLoadingProgressForIncrementalApps() {
            final List<UserHandle> users = mUm.getUserProfiles();
            if (users == null) {
                return;
            }
            for (UserHandle user : users) {
                mPackageManagerInternal.forEachInstalledPackage(pkg -> {
                    final String packageName = pkg.getPackageName();
                    final IncrementalStatesInfo info =
                            mPackageManagerInternal.getIncrementalStatesInfo(packageName,
                                    Process.myUid(), user.getIdentifier());
                    if (info != null && info.isLoading()) {
                        mPackageManagerInternal.registerInstalledLoadingProgressCallback(
                                packageName, new PackageLoadingProgressCallback(packageName, user),
                                user.getIdentifier());
                    }
                }, user.getIdentifier());
            }
        }

收到回调后,回调监听者

 class PackageLoadingProgressCallback extends
                PackageManagerInternal.InstalledLoadingProgressCallback {
            private final String mPackageName;
            private final UserHandle mUser;

            PackageLoadingProgressCallback(String packageName, UserHandle user) {
                super(mCallbackHandler);
                mPackageName = packageName;
                mUser = user;
            }

            @Override
            public void onLoadingProgressChanged(float progress) {
                final int n = mListeners.beginBroadcast();
                try {
                    for (int i = 0; i < n; i++) {
                        IOnAppsChangedListener listener = mListeners.getBroadcastItem(i);
                        BroadcastCookie cookie = (BroadcastCookie) mListeners.getBroadcastCookie(i);
                        if (!isEnabledProfileOf(cookie, mUser, "onLoadingProgressChanged")) {
                            continue;
                        }
                        if (!isPackageVisibleToListener(mPackageName, cookie, mUser)) {
                            continue;
                        }
                        try {
                            listener.onPackageLoadingProgressChanged(mUser, mPackageName, progress);
                        } catch (RemoteException re) {
                            Slog.d(TAG, "Callback failed ", re);
                        }
                    }
                } finally {
                    mListeners.finishBroadcast();
                }
            }
        }

安装时会调用一个方法,监听安装

frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java

 /**
     * Makes sure files are present in staging location.
     * @return if the image is ready for installation
     */
    @GuardedBy("mLock")
    private boolean prepareDataLoaderLocked()
            throws PackageManagerException {

         ..................

   
        final IDataLoaderStatusListener statusListener = new IDataLoaderStatusListener.Stub() {
            @Override
            public void onStatusChanged(int dataLoaderId, int status) {
                switch (status) {
                    case IDataLoaderStatusListener.DATA_LOADER_BINDING:
                    case IDataLoaderStatusListener.DATA_LOADER_STOPPED:
                    case IDataLoaderStatusListener.DATA_LOADER_DESTROYED:
                        return;
                }

                if (mDestroyed || mDataLoaderFinished) {
                    switch (status) {
                        case IDataLoaderStatusListener.DATA_LOADER_UNRECOVERABLE:
                            if (systemDataLoader) {
                                onSystemDataLoaderUnrecoverable();
                            }
                            return;
                    }
                    return;
                }
                try {
                    switch (status) {
                        case IDataLoaderStatusListener.DATA_LOADER_BOUND: {
                            if (manualStartAndDestroy) {
                                FileSystemControlParcel control = new FileSystemControlParcel();
                                control.callback = new FileSystemConnector(addedFiles);
                                getDataLoader(dataLoaderId).create(dataLoaderId, params.getData(),
                                        control, this);
                            }

                            break;
                        }
                        case IDataLoaderStatusListener.DATA_LOADER_CREATED: {
                            if (manualStartAndDestroy) {
                                // IncrementalFileStorages will call start after all files are
                                // created in IncFS.
                                getDataLoader(dataLoaderId).start(dataLoaderId);
                            }
                            break;
                        }
                        case IDataLoaderStatusListener.DATA_LOADER_STARTED: {
                            getDataLoader(dataLoaderId).prepareImage(
                                    dataLoaderId,
                                    addedFiles.toArray(
                                            new InstallationFileParcel[addedFiles.size()]),
                                    removedFiles.toArray(new String[removedFiles.size()]));
                            break;
                        }
                        case IDataLoaderStatusListener.DATA_LOADER_IMAGE_READY: {
                            mDataLoaderFinished = true;
                            if (hasParentSessionId()) {
                                mSessionProvider.getSession(
                                        getParentSessionId()).dispatchSessionSealed();
                            } else {
                                dispatchSessionSealed();
                            }
                            if (manualStartAndDestroy) {
                                getDataLoader(dataLoaderId).destroy(dataLoaderId);
                            }
                            break;
                        }
                        case IDataLoaderStatusListener.DATA_LOADER_IMAGE_NOT_READY: {
                            mDataLoaderFinished = true;
                            dispatchSessionValidationFailure(INSTALL_FAILED_MEDIA_UNAVAILABLE,
                                    "Failed to prepare image.");
                            if (manualStartAndDestroy) {
                                getDataLoader(dataLoaderId).destroy(dataLoaderId);
                            }
                            break;
                        }
                        case IDataLoaderStatusListener.DATA_LOADER_UNAVAILABLE: {
                            // Don't fail or commit the session. Allow caller to commit again.
                            sendPendingStreaming(mContext, getRemoteStatusReceiver(), sessionId,
                                    "DataLoader unavailable");
                            break;
                        }
                        case IDataLoaderStatusListener.DATA_LOADER_UNRECOVERABLE:
                            throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE,
                                    "DataLoader reported unrecoverable failure.");
                    }
                } catch (PackageManagerException e) {
                    mDataLoaderFinished = true;
                    dispatchSessionValidationFailure(e.error, ExceptionUtils.getCompleteMessage(e));
                } catch (RemoteException e) {
                    // In case of streaming failure we don't want to fail or commit the session.
                    // Just return from this method and allow caller to commit again.
                    sendPendingStreaming(mContext, getRemoteStatusReceiver(), sessionId,
                            e.getMessage());
                }
            }
        };

        if (!manualStartAndDestroy) {
            final PerUidReadTimeouts[] perUidReadTimeouts =
                    mPm.getPerUidReadTimeouts(mPm.snapshotComputer());

            final StorageHealthCheckParams healthCheckParams = new StorageHealthCheckParams();
            healthCheckParams.blockedTimeoutMs = INCREMENTAL_STORAGE_BLOCKED_TIMEOUT_MS;
            healthCheckParams.unhealthyTimeoutMs = INCREMENTAL_STORAGE_UNHEALTHY_TIMEOUT_MS;
            healthCheckParams.unhealthyMonitoringMs = INCREMENTAL_STORAGE_UNHEALTHY_MONITORING_MS;

            final IStorageHealthListener healthListener = new IStorageHealthListener.Stub() {
                @Override
                public void onHealthStatus(int storageId, int status) {
                    if (mDestroyed || mDataLoaderFinished) {
                        return;
                    }

                    switch (status) {
                        case IStorageHealthListener.HEALTH_STATUS_OK:
                            break;
                        case IStorageHealthListener.HEALTH_STATUS_READS_PENDING:
                        case IStorageHealthListener.HEALTH_STATUS_BLOCKED:
                            if (systemDataLoader) {
                                // It's OK for ADB data loader to wait for pages.
                                break;
                            }
                            // fallthrough
                        case IStorageHealthListener.HEALTH_STATUS_UNHEALTHY:
                            // Even ADB installation can't wait for missing pages for too long.
                            mDataLoaderFinished = true;
                            dispatchSessionValidationFailure(INSTALL_FAILED_MEDIA_UNAVAILABLE,
                                    "Image is missing pages required for installation.");
                            break;
                    }
                }
            };

            try {
                final PackageInfo pkgInfo = mPm.snapshotComputer()
                        .getPackageInfo(this.params.appPackageName, 0, userId);
                final File inheritedDir =
                        (pkgInfo != null && pkgInfo.applicationInfo != null) ? new File(
                                pkgInfo.applicationInfo.getCodePath()).getParentFile() : null;

                if (mIncrementalFileStorages == null) {
                    mIncrementalFileStorages = IncrementalFileStorages.initialize(mContext,
                            stageDir, inheritedDir, params, statusListener, healthCheckParams,
                            healthListener, addedFiles, perUidReadTimeouts,
                            new IPackageLoadingProgressCallback.Stub() {
                                @Override
                                public void onPackageLoadingProgressChanged(float progress) {
                                    synchronized (mProgressLock) {
                                        mIncrementalProgress = progress;
                                        computeProgressLocked(true);
                                    }
                                }
                            });
                } else {
                    // Retrying commit.
                    mIncrementalFileStorages.startLoading(params, statusListener, healthCheckParams,
                            healthListener, perUidReadTimeouts);
                }
                return false;
            } catch (IOException e) {
                throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE, e.getMessage(),
                        e.getCause());
            }
        }

        ..................

        return false;
    }

六、整体流程时序(待续。。。。)

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值