APK安装过程解析

news2025/1/11 15:59:54

应用端发起安装APK的代码一般如下:

 Intent installintent = new Intent();
 installintent.setAction(Intent.ACTION_VIEW);
 installintent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 installintent.setDataAndType(xxx,"application/vnd.android.package-archive");
 context.startActivity(installintent);

安装apk,本质上是通过系统的应用packageInstaller.apk来完成的。因此,我们需要查看的是packageInstaller的源码。打开 PackageInstaller 的AndroidManifest.xml文件,我们会发现跟上面Intent要启动的Activity匹配的是InstallStart,这也是PackageInstaller应用的入口:

<activity android:name=".InstallStart"
        android:theme="@android:style/Theme.Translucent.NoTitleBar"
        android:exported="true"
        android:excludeFromRecents="true">
        <intent-filter android:priority="1">
            <action android:name="android.intent.action.VIEW"/>
            <action android:name="android.intent.action.INSTALL_PACKAGE"/>
            <category android:name="android.intent.category.DEFAULT"/>
            <data android:scheme="content"/>
            <data android:mimeType="application/vnd.android.package-archive"/>
        </intent-filter>
                         
        <intent-filter android:priority="1">
            <action android:name="android.intent.action.INSTALL_PACKAGE"/>
            <category android:name="android.intent.category.DEFAULT"/>
            <data android:scheme="package"/>
            <data android:scheme="content"/>
        </intent-filter>
        
        <intent-filter android:priority="1">
            <action android:name="android.content.pm.action.CONFIRM_INSTALL"/>
            <category android:name="android.intent.category.DEFAULT"/>
        </intent-filter>
    </activity>

InstallStart.java

/frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java

跳转InstallStart .java

主要工作:

  1. 判断是否需要展示一个确认安装的对话框,如果是,则跳转PackageInstallerActivity;如果否则跳转3
final boolean isSessionInstall =PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction());
  1. 判断是否勾选“信任未知来源”选项,若未勾选,则判断版本是否小于Android 8.0,是则取消安装;否则判断版本是否大于Android 8.0且没有设置REQUEST_INSTALL_PACKAGES权限,是则取消安装
isTrustedSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false);
  1. 判断Uri的Scheme协议,若是content://则跳转 InstallStaging, 若是 package://则跳转 PackageInstallerActivity,但是实际上 InstallStaging中的 StagingAsyncTask 会将content协议的Uri转换为File协议,然后跳转到PackageInstallerActivity

  2. finish当前InstallStart界面

PackageInstallerActivity.java

主要工作:

  1. 构建确认安装对话框
protected void onCreate(Bundle icicle) {
	...
	bindUi();
}
  1. 初始化安装需要用的各种对象,比如 PackageManager、IPackageManager、AppOpsManager、UserManager、PackageInstaller 等等

  2. 根据传过来的scheme协议(package/file)做不同处理,主要就是获取PackageInfo对象,PackageInfo包含了Android 应用的信息,例如应用名称,权限列表等,通过PackageInfo创建AppSnippet对象,里面包含待安装应用的图标和标题

 boolean wasSetUp = processPackageUri(packageUri);
  1. 使用 checkIfAllowedAndInitiateInstall() 来检查APK来源,展示"未知来源APK安装"的对话框,当点击"settings"按钮后跳转到设置页
  Intent settingsIntent = new Intent();
    settingsIntent.setAction(
    Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
    Uri packageUri = Uri.parse("package:" + argument);
    settingsIntent.setData(packageUri);
    try{
        getActivity().startActivityForResult(settingsIntent, REQUEST_TRUST_EXTERNAL_SOURCE);
    } catch(ActivityNotFoundException exc){
        Log.e(TAG, "Settings activity not found for action: " + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
    }
  1. 打开“允许未知来源安装”设置选项后回到 PackageInstallerActivity,在 onActivityResult()中,调用initiatenstall()检查应用列表判断该应用是否已安装,若已安装则提示该应用已安装,由用户决定是否替换,然后显示确认安装界面

  2. 在安装界面,点击 OK 按钮确认安装后,会调用 startInstall 开始安装工作

  3. startInstall 方法用来跳转到 InstallInstalling,并关闭掉当前的 PackageInstallerActivity

InstallInstalling.java

主要工作:

  1. 获取PackageManager,然后获取其中的PackageInstaller对象

  2. PackageInstaller中创建用于传输APK安装包以及安装信息的Session,并返回SessionId

  3. 创建异步任务InstallingAsyncTask,并在异步任务中根据之前返回的SessionId打开PackageInstallerSession

  4. 通过Session将APK安装包以及相关信息传递

  5. 在异步任务中onPostExecute方法中执行session.commit(pendingIntent.getIntentSender())

  6. 通过注册观察者 InstallEventReceiver监听安装成功和失败的回调,跳转到对应结果页面

     <receiver android:name=".InstallEventReceiver"
        android:permission="android.permission.INSTALL_PACKAGES"
        android:exported="true">
            <intent-filter android:priority="1">
                <action android:name="com.android.packageinstaller.ACTION_INSTALL_COMMIT" />
            </intent-filter>
     </receiver>

PackageInstallerSession

session.commit()方法通过binder跨进程调到了 PackageInstallerSession 服务中。

通过aidl文件定义的接口为:

/frameworks/base/core/java/android/content/pm/IPackageInstallerSession.aidl

IPackageInstallerSession 的实现类为:

/frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java
//PackageInstallerSession.java
public void commit(@NonNull IntentSender statusReceiver, boolean forTransfer) {
    ...
    dispatchStreamValidateAndCommit();
}

private void dispatchStreamValidateAndCommit() {
    mHandler.obtainMessage(MSG_STREAM_VALIDATE_AND_COMMIT).sendToTarget();
}

主要工作

  1. 发送了 MSG_STREAM_VALIDATE_AND_COMMIT 消息到 mHandler
private final Handler.Callback mHandlerCallback = new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_STREAM_VALIDATE_AND_COMMIT:
                handleStreamValidateAndCommit(); //
                break;
            case MSG_INSTALL:
                handleInstall(); //
                break;
            ...
        }
        return true;
    }
};
  1. 最后执行在 handleStreamValidateAndCommit()中,然后里面又发送了消息MSG_INSTALL,这个执行在handleInstall()
//PackageInstallerSession.java
    public void handleStreamValidateAndCommit() {
        ...
         mHandler.obtainMessage(MSG_INSTALL).sendToTarget();
    }
    rivate final Handler.Callback mHandlerCallback = new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_STREAM_VALIDATE_AND_COMMIT:
                    handleStreamValidateAndCommit(); //
                    break;
                case MSG_INSTALL:
                    handleInstall(); //
                    break;
                ...
            }
            return true;
        }
    };

    private void handleInstall() {
        ...
        List<PackageInstallerSession> childSessions = getChildSessionsNotLocked();

        installNonStagedLocked(childSessions);
        ...
    }

    private void installNonStagedLocked(List<PackageInstallerSession> childSessions) {
        final PackageManagerService.ActiveInstallSession installingSession = makeSessionActiveLocked();
         ...
         //安装走到了 PMS 中
         mPm.installStage(installingSession);
      ...
    }
  1. 最后,安装过程走到了 PMSinstallStage()

目前为止,只是通过 PackageInstaller 维持了安装 Session,把安装包写入到 Session中,真正的安装过程是 PMS 来执行。

PackageManagerService.java

主要工作

  1. installStage()创建 InstallParams 对象,传入message消息;发送了 INIT_COPY 消息到mHandler
//PMS
void installStage(ActiveInstallSession activeInstallSession) {
    ...
    final Message msg = mHandler.obtainMessage(INIT_COPY);
    final InstallParams params = new InstallParams(activeInstallSession);
    msg.obj = params;
    mHandler.sendMessage(msg);
}

mHandlerPackageHandler,这是在 PackageManagerService 构造方法中创建的。
mHandler 处理INIT_COPYInstallParams.startCopy():

void doHandleMessage(Message msg) {
        switch (msg.what) {
            case INIT_COPY: {
                // 得到安装参数  HandlerParams 
                HandlerParams params = (HandlerParams) msg.obj;
                if (params != null) {
                    if (DEBUG_INSTALL) Slog.i(TAG, "init_copy: " + params);
                    Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, "queueInstall",
                            System.identityHashCode(params));
                    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "startCopy");
                    // 2 调用  startCopy() 方法
                    params.startCopy();
                    Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
                }
                break;
            }
            ...
        } 
}          

里面调用了 HandlerParamsstartCopy() 方法。HandlerParamsPMS的内部抽象类。PMS的内部类 InstallParamsMultiPackageInstallParams 是其实现类。

//HandlerParams.java
final void startCopy() {
    handleStartCopy();
    handleReturnCode();
}

handleStartCopy()、handleReturnCode() 都是是抽象的方法。因此,这里的对象是哪一个呢? 经过之前的分析我们知道是 InstallParams

  1. handleStartCopy()扫描了apk的轻量信息、安装空间、apk完整性的校验等。创建 FileInstallArgs 对象,赋值返回code。

  2. handleReturnCode()先是调用了 InstallArgs 的 copyApk()方法执行APK拷贝,然后执行安装APK;

  void handleReturnCode() {
            if (mVerificationCompleted && mIntegrityVerificationCompleted && mEnableRollbackCompleted) {
                ...
                //如果前面校验ok,这里执行apk拷贝
                if (mRet == PackageManager.INSTALL_SUCCEEDED) {
                    mRet = mArgs.copyApk();
                }
                //执行安装
                processPendingInstall(mArgs, mRet);
            }
        }
    }

FileInstallArgs 继承了 InstallArgs类。 copyApk() 内部调用了 doCopyApk();

doCopyApk() 先获取了拷贝文件路径:/data/app,使用PackageManagerServiceUtils.copyPackage()进行APK拷贝,接着是 .so文件的拷贝。也就是说,把发送到 Session暂存目录 data/app-staging 的APK 拷贝到了 /data/app。

/data/app-stagging 临时目录把apk拷贝到/data/app目录。

//拷贝apk
    int ret = PackageManagerServiceUtils.copyPackage(
            origin.file.getAbsolutePath(), codeFile);
    if (ret != PackageManager.INSTALL_SUCCEEDED) {
        Slog.e(TAG, "Failed to copy package");
        return ret;
    }

接着看执行安装processPendingInstall(mArgs, mRet)

  1. processPendingInstall()调用了processInstallRequestsAsync(),processInstallRequestsAsync()进行安装前校验,开始安装APK,发送通知安装结果的广播
  private void processInstallRequestsAsync(boolean success, List<InstallRequest>installRequests) {
        mHandler.post(() -> {
            if (success) {
                for (InstallRequest request : installRequests) {
                    //安装前检验:returnCode不为INSTALL_SUCCEEDED就移除拷贝的apk等
                    request.args.doPreInstall(request.installResult.returnCode);
                }
                synchronized (mInstallLock) {
                    //安装:解析apk
                    installPackagesTracedLI(installRequests);
                }
                for (InstallRequest request : installRequests) {
                    //同安装前检验
                    request.args.doPostInstall(request.installResult.returnCode, request.installResult.uid);
                }
            }
            for (InstallRequest request : installRequests) {
                //安装后续:备份、可能的回滚、发送安装完成先关广播
                restoreAndPostInstall(request.args.user.getIdentifier(), request.installResult, new PostInstallData(request.args, request.installResult, null));
            }
        });
    }

这里我们先重点看安装过程,installPackagesTracedLI() 又走到 installPackagesLI():

安装过程分为四个阶段:

  1. 准备,分析当前安装状态,解析包 并初始校验:在 preparePackageLI() 内使用 PackageParser2.parsePackage() 解析AndroidManifest.xml,获取四大组件等信息;使用ParsingPackageUtils.getSigningDetails() 解析签名信息;重命名包最终路径 等。
    prepareResult = preparePackageLI(request.args, request.installResult);
  1. 扫描,根据准备阶段解析的包信息上下文 进一步解析:确认包名真实;根据解析出的信息校验包有效性(是否有签名信息等);搜集apk信息——PackageSetting、apk的静态库/动态库信息等。
    final List<ScanResult> scanResults = scanPackageTracedLI( prepareResult.packageToScan, prepareResult.parseFlags, prepareResult.scanFlags, System.currentTimeMillis(), request.args.user);
  1. 核对,验证扫描后的包信息,确保安装成功:主要就是覆盖安装的签名匹配验证。
    reconciledPackages = reconcilePackagesLocked( reconcileRequest, mSettings.mKeySetManagerService);

4. 提交,提交扫描的包、更新系统状态:添加 PackageSetting 到 PMS 的 mSettings、添加 AndroidPackage 到 PMS 的 mPackages 、添加 秘钥集 到系统、应用的权限添加到 mPermissionManager、四大组件信息添加到 mComponentResolver 。这是唯一可以修改系统状态的地方,并且要对所有可预测的错误进行检测。
commitRequest = new CommitRequest(reconciledPackages, mUserManager.getUserIds()); commitPackagesLocked(commitRequest);

安装完成后,调用了 executePostCommitSteps() 准备app数据、执行dex优化:

  • prepareAppDataAfterInstallLIF():提供目录结构/data/user/用户ID/包名/cache(/data/user/用户ID/包名/code_cache)
  • mPackageDexOptimizer.performDexOpt():dexopt 是对 dex 文件 进行 verification 和 optimization 的操作,其对 dex 文件的优化结果变成了 odex 文件,这个文件和 dex 文件很像,只是使用了一些优化操作码(譬如优化调用虚拟指令等)。

这两个操作最终都是使用 Installer 对应的方法来操作。前面介绍 PMS 创建时传入了 Installer 的实例,而 Installer 继承自 SystemService 也是一个系统服务。这里来看下:

  • 在 Installer的 onStart()方法中 通过 installd 的Binder对象获取了 mInstalld 实例。可见这里是IPC操作,即 System_Server 进程中的 Installer IPC 到 具有root权限的守护进程。像目录 /data/user 的创建 必须要有root权限。
  • PMS中使用了Installer的很多方法,Installer是Java层提供的Java API接口,Installd 则是在init进程启动的具有root权限的Daemon进程。

到这里安装完成,再回到 PMS 的 processInstallRequestsAsync(),最后调用restoreAndPostInstall()进行 备份、可能的回滚、发送安装完成先关广播:

private void restoreAndPostInstall(
        int userId, PackageInstalledInfo res, @Nullable PostInstallData data) {
    ...
    Message msg = mHandler.obtainMessage(POST_INSTALL, token, 0);
    mHandler.sendMessage(msg);
    ...
}

mHandler还是 PackageHandler对象。

mHandler 使用 PMS的 handlePackagePostInstall()方法处理 POST_INSTALL

  • 根据安装结果 发送 Intent.ACTION_PACKAGE_ADDED 等广播,桌面Launcher 收到广播后就会在桌上增加App的Icon
  • 调用 PackageInstallSession 中保存的IPackageInstallObserver2实例的onPackageInstalled()方法,最后发送安装成功的通知显示在通知栏,通过 IntentSender 发送 在 InstallInstalling 中就定义好的广播,最后 InstallInstalling页面 根据结果展示 安装成功/安装失败 。

到这里,APK的安装过程 就梳理完毕了,来回顾下:

  • APK用写入Session且包信息和APK安装操作 都提交到了PMS;
  • PMS中先把APK拷贝到 /data/app,然后使用PackageParser2解析APK 获取 四大组件、搜集签名、PackageSetting等信息,并进行校验确保安装成功;
  • 接着提交信息包更新系统状态及PMS的内存数据;
  • 然后使用 Installer 准备用户目录/data/user、进行 dexOpt;
  • 最后发送安装结果通知UI层。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1009245.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringMVC系列(四)之SpringMVC实现文件上传和下载

目录 前言 一. SpringMVC文件上传 1. 配置多功能视图解析器 2. 前端代码中&#xff0c;将表单标记为多功能表单 3. 后端利用MultipartFile 接口&#xff0c;接收前端传递到后台的文件 4. 文件上传示例 1. 相关依赖&#xff1a; 2. 逆向生成对应的类 3. 后端代码&#xf…

DC电源模块在保护设备损坏的重要功能

BOSHIDA DC电源模块在保护设备损坏的重要功能 DC电源模块是一种电源管理设备&#xff0c;用于将交流电转换为直流电并提供给设备供电。它通常由多个电子元件组成&#xff0c;包括整流器、滤波器、稳压器等&#xff0c;以确保电源输出稳定&#xff0c;满足设备的电源需求。 在…

“文件管理技巧:批量归类相同名称的文件到指定文件夹“

在日常生活和工作中&#xff0c;我们经常需要处理大量的文件&#xff0c;如果每个文件都单独归类整理&#xff0c;会浪费大量的时间和精力。有没有一种简单的方法可以批量将相同名称的文件归类到指定文件夹里呢&#xff1f;答案是肯定的&#xff01;下面就让我们一起来了解这个…

JavaScript对象实战及应用

&#x1f3ac; 岸边的风&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 引言 1. 对象属性 访问属性 修改属性 删除属性 动态添加属性 属性枚举 属性描述符 2. 对象 API Object.ke…

cms之wordpress安装教程

1、下载程序 到wordpress官方网站下载wordpress程序&#xff0c;官方下载地址&#xff1a;https://cn.wordpress.org/download/。 下载最新版的wordpress程序 https://cn.wordpress.org/latest-zh_CN.zip 2、上传程序 上传程序前先确认主机是否符合安装的环境要求&#xff…

芯科蓝牙BG27开发笔记7-配置蓝牙参数

基础的要求 1. 设置广播参数为间隔1000ms&#xff0c;不停止 2. 添加广播消息&#xff0c;含01、03、09、FF TYPE 3. 设置蓝牙通信间隔参数为320ms、400ms、2、4000ms超时 3. 配置发射功率为较低 4. 配置GATT所有数据与原Nordic 配置一致 为了解决以上疑问&#xff0c;需…

4.zigbee开发,传感器网络管理进阶(网状和树状拓扑),zigbee的ADC

一。zigbee的串口 1.串口通信的基本概念 &#xff08;1&#xff09;同步通信与异步通信 同步通信&#xff1a; 一般情况下同步通信指的是通信双方根据同步信号进行通信的方式。比如通信双方有一个共同的时钟信号&#xff0c;通讯中通常双方会统一规定在时钟信号的上升沿…

DP专题3 使用最小花费爬楼梯

题目&#xff1a; 思路&#xff1a; 根据题意&#xff0c;我们先明确 dp 数组 i 的含义&#xff0c; 这里很明显&#xff0c;可以知道 i 是对应阶梯的最少花费&#xff0c; 其次dp初始化中&#xff0c;我们的 dp[0] 和 dp[1] 是 0 花费&#xff0c; 这是我们可以选择的&am…

关键词生成原创文章软件-原创文章生成软件

大家好&#xff0c;今天我想和大家分享一下我对147SEO关键词生成原创文章工具的感受。作为一个经常需要写作的人&#xff0c;我深知寻找创意和构建文章结构的挑战。关键词生成原创文章似乎为这些问题提供了一种解决方案。 首先&#xff0c;让我谈谈我的感受。关键词生成原创文章…

9个值得收藏的WebGL性能优化技巧

在这里&#xff0c;我们推荐一些经证明非常适合创建基于 Web 的交互体验的优化技术。 本章主要基于 Soft8Soft 在 Verge3Day Europe 2019 会议上的演讲。 推荐&#xff1a;用 NSDT编辑器 快速搭建可编程3D场景 1、几何/网格 几何是 3D 应用程序的基础&#xff0c;因为它构成了…

华为云云耀云服务器实例使用教学

目录 国内免费云服务器&#xff08;体验&#xff09; 认识国内免费云服务器 如何开通国内免费云服务器 云耀云服务器 HECS Xshell 远程连接 云服务器更改安全组 切换操作系统 服务器详情 HECS适用于哪些场景&#xff1f; 网站搭建 电商建设 开发测试环境 云端学习环…

二维码智慧门牌管理系统开发解决方案:标准化建设的基础

文章目录 前言一、系统质量保证二、系统互联互通三、系统可扩展性 前言 在现代城市管理和服务中&#xff0c;二维码智慧门牌管理系统扮演着至关重要的角色&#xff0c;它通过智能化和数字化手段提高了城市管理效率、公共服务水平&#xff0c;并有助于维护社会公共安全。然而&a…

macOS 12 Monterey:一次全新的跨设备协作体验

macOS 12 Monterey是苹果公司的一次重大突破&#xff0c;它打破了设备间的壁垒&#xff0c;将不同设备无缝地连接在一起&#xff0c;极大地提升了用户的工作效率和娱乐体验。Monterey带来了通用控制、AirPlay、捷径等新功能&#xff0c;以及一些实用的新小功能。 安装&#xf…

跨链协议支持Sui的资产所有权理念,助力资产在不同链之间流通

区块链通常支持安全地持有数字资产这一概念。然而&#xff0c;在一个链上拥有资产并不意味着它可以转移到另一个链上。支持在不同链之间移动资产的跨链协议有助于解决行业中可能出现的主要碎片化问题。 Sui通过基于开源Wormhole协议构建的Wormhole Connect支持跨链。构建者可以…

【数据分享】1901-2022年1km分辨率逐年降水栅格数据(免费获取/全国/分省)

降水数据是我们在各项研究中最常用的气象指标之一&#xff01;之前我们给大家分享过1901-2022年1km分辨率逐月降水栅格数据&#xff08;可查看之前的文章获悉详情&#xff09;&#xff01;该数据来源于国家青藏高原科学数据中心&#xff0c;这儿的逐月降水量是指当月的总降水量…

SeaArt.ai: 海艺AI绘画艺术图片模型创作平台

【产品介绍】 • 名称 SeaArt.ai • 具体描述 SeaArt.ai是一个基于人工智能技术的AI绘画工具&#xff0c;它可以根据你的描述或者关键词来生成符合你想象的图片。你可以选择不同的模式来创建不同类型的图片&#xff0c;比如人物、风景、建筑、神话、自…

自动化测试面试题解析,半小时通透

面试一般分为技术面和hr面&#xff0c;形式的话很少有群面&#xff0c;少部分企业可能会有一个交叉面&#xff0c;不过总的来说&#xff0c;技术面基本就是考察你的专业技术水平的&#xff0c;hr面的话主要是看这个人的综合素质以及家庭情况符不符合公司要求&#xff0c;一般来…

液体颗粒计数器如何选择!

随着液体污染检测技术的飞速发展&#xff0c;液体粒子计数器由于计数速度快、准确度高、重复性好、操作简便且结果不受人为因素的影响&#xff0c;成为半导体等领域用于测量和监测液体样品中颗粒物浓度和径向分布的重要工具。 液体粒子计数器是各行各业用于测量和监测液体样品中…

Jmeter+jenkins接口性能测试平台实践整理

最近两周在研究jmeter&#xff0b;Jenkin的性能测试平台测试dubbo接口&#xff0c;分别尝试使用maven&#xff0c;ant和Shell进行构建&#xff0c;jmeter相关设置略。 一、Jmeterjenkins&#xff0b;Shell&#xff0b;tomcat 安装Jenkins,JDK,tomcat,并设置环境变量&#xff…

企业网盘 VS 大文件传输, 哪个才是企业传输的正确选择?

在当今的信息时代&#xff0c;文件传输是企业日常工作中不可或缺的一项功能。无论是内部沟通、外部协作、还是客户服务&#xff0c;都需要高效、安全、便捷地传输各种类型和大小的文件。那么&#xff0c;面对市场上众多的文件传输产品和服务&#xff0c;企业应该如何选择呢&…