关于性能优化:浅谈App的启动优化

5次阅读

共计 10417 个字符,预计需要花费 27 分钟才能阅读完成。

1. 利用启动的形式

在 Android 中,利用启动个别可分为三种:冷启动 温启动 热启动

那么什么是 冷启动 温启动 热启动 呢?上面咱们来简略看一下它们的定义:

  • 冷启动:当启动利用时,后盾没有该利用的过程。这时零碎会又一次创立一个新的过程调配给该利用,这个启动形式就是冷启动。
  • 温启动:当启动利用时,后盾已有该利用的过程,然而 Activity 可能因为内存不足被回收。这样零碎会从已有的过程中来启动这个 Activity,这个启动形式叫温启动。
  • 热启动:当启动利用时,后盾已有该利用的过程,且 Activity 依然存在内存中没有被回收。这样零碎间接把这个 Activity 拉到前台即可,这个启动形式叫热启动。

因为冷启动绝对于其余启动形式多了过程的创立(Zygote 过程 fork 创立过程)以及利用的资源加载和初始化(Application 的创立及初始化),所以相对来说会比拟耗时,所以咱们个别说的 App 启动优化个别指的都是 App 的冷启动优化。

2. 优化计划

App 启动优化的实质就是:启动速度和体验的优化

这就好比早些年你去饭店吃饭,你想要点餐但等了半天都没有服务人员过去,可能就等得不耐烦间接走开了。同样的,对于 APP 来说,如果用户点击 App 后长时间都打不开,用户就很可能失去急躁而卸载利用。

所以 启动速度是用户对咱们 App 的第一体验。如果启动速度过慢,用户第一印象就会很差,这样即便你性能做出花来,用户也不会违心去应用。

其实要想优化 App 的启动体验,要害就是要让用户更快地获取到利用的内容(晦涩,不卡顿、不期待),那么咱们应该怎么做呢?

2.1 案例剖析

这里咱们还是先以之前说的去饭店吃饭为例来展开讨论。当初的饭店竞争是尤为得强烈,为了可能进步顾客的体验、留住顾客,真的是使出了浑身解数,除了口味之外,服务品质也是被摆在了越来越重要的地位。

就比方说:

  • 为了可能更快地给用户提供点餐服务,当初的饭店每个座子上根本都贴上了点餐的二维码。
  • 一些热门须要排队的餐馆,在前台张贴了二维码,提供排队取号和提前点餐服务。
  • 一些长年爆满须要排队的餐馆,还提供了零食、茶水、棋牌以及美甲等服务。
  • 有些餐厅为了进步上菜的速度和体验,设置了倒计时免单的服务。

以上的改良措施,都是这些年餐饮行业在竞争下,一直进步服务质量的产物。能够说谁的服务质量落下了,谁就有可能被淘汰。

其实咱们仔细分析一下下面所列举的,不难看出有很多是能够让咱们借鉴的。

(1)案例 1

剖析:饭店提供了点餐的二维码,实质上是将一件被动期待转变为被动申请的一种过程,主观上缩小了期待的工夫,从而进步了速度。

类比:这对应咱们的应用程序,就像原先一些耗时不必要的三方库须要被动期待其初始化结束程序才会持续进行,转变为先不初始化这部分耗时的三方库,等真正用到时再进行初始化;又相似咱们应用程序的游客模式,无需被迫进行一堆简单的用户注册过程,就能够间接进入程序应用,待波及一些用户信息性能的时候再提醒用户注册。

(2)案例 2

剖析:热门餐馆同时提供排队取号和提前点餐服务,实质上是将原先无奈同时进行的操作(须要先排上队等到座位,能力扫描座位号点餐)变成了可同时进行的操作,将串行工作转化为并行任务,从而节俭了工夫。

类比:这对应咱们的应用程序,就是将一些本来在主线程串行执行的耗时资源 / 数据加载,改为在子线程中并发执行。这在几个耗时工作耗时差距不大的时候优化尤为显著。

(3)案例 3

剖析:在顾客排队期待的时候,提供零食、茶水、棋牌以及美甲等服务,实质上就是让顾客提前享受本餐馆的服务,从而缓解顾客期待的焦虑。

类比:这对应咱们的应用程序,就是开屏启动页。在 Android12 上,Google 强制减少了这个开屏页,就是为了让用户提前看到你的利用页面,让用户产生利用启动很晦涩的假象,从而进步用户的启动体验。

(4)案例 4

剖析:设置倒计时免单服务,实质上就是给期待加上了进度条下限以及超时抵偿。俗话说最让人胆怯的是期待,比期待更让人胆怯的是看不见止境的期待。而为期待设置下限,能够极大地缓解顾客期待的焦虑,毕竟期待超时了也是会有弥补的。

类比:这对应咱们的应用程序,就是一些利用(比方游戏)首次启动会十分耗时,所以它们通常会在启动页减少一个初始化 / 加载进度条页面,来通知用户啥时候能加载完,而不是无止境未知的期待。

通过剖析,咱们能够看到(1)、(2)两种操作是从技术的角度来实现的优化,而(3)、(4)两种操作则更多的是从业务的角度去实现的优化。

2.2 优化策略

从下面的案例剖析,咱们能够得出,利用启动优化,咱们能够别离从技术和业务的角度来进行决策。

2.2.1 技术优化

(1)针对启动流程工作进行梳理。

  • 无耗时工作 -> 主线程
  • 耗时且须要同步工作 -> 异步线程 + 同步锁
  • 耗时且无需同步工作 -> 异步线程

(2)非必要不执行。

  • 非必要数据 -> 懒加载
  • 非必要工作 -> 提早 / 闲暇执行
  • 非必要界面 / 布局 -> 提早加载
  • 非必要性能 -> 删除 / 插件化

(3)数据结构优化,减小初始化工夫。

  • 数据结构尽可能复用,防止内存抖动
  • 数据结构申请的空间要恰到好处,不能过大(占用空间,创立慢)也不能过小(频繁裁减,效率低)
  • 对于必要且量大的数据,可采取分段加载。
  • 重要的资源本地缓存

2.2.2 业务优化

(1)业务流程整合。

  • 多个相干的串行业务整合为对立的一个业务。
  • 不相干的串行业务整合为并行的业务。

(2)业务流程拆分调整。

  • 对业务进行拆分,拆分出次要(必要)业务和主要(非必要)业务。
  • 别离对次要业务和主要业务进行优先级评估,业务执行按优先级从高到底顺次执行。

2.3 优化方向

在优化之前,让咱们先来剖析一下冷启动的过程:

Zygote 创立利用过程 -> AMS 申请 ApplicationThread -> Application 创立 -> attachBaseContext > onCreate -> ActivityThread 启动 Activity -> Activity 生命周期(创立、布局加载、屏幕安排、首帧绘制)

以上过程,只有 Application 和 Activity 的生命周期这两个阶段对咱们来说是可控的,所以这就是咱们的优化方向。

3. 优化措施

3.1 启动流程优化

1. 根据之前咱们列举的技术优化策略,首先须要对启动的所有工作流程进行梳理,而后对其执行形式进行优化。

  • 只有必要且非耗时的工作在 主线程 执行。
  • 耗时且须要同步的工作,应用 异步线程 + 同步锁 的形式执行。
  • 耗时且无需同步的工作在 异步线程 执行。

2. 第三方 SDK 初始化优化。

  • 对于那些在启动时非必要的第三方 SDK,能够提早初始化。
  • 对于初始化耗时的第三方 SDK,能够开启一个后盾服务 / 异步线程进行初始化。

3. 应用工作执行框架。

这里咱们还能够应用一些第三方的工作启动框架,对启动流程进行优化。上面我就拿我开源的 XTask 简略介绍一下:

这里咱们模仿了三种类型的工作:

  • 1. 优先级最重要的工作,执行工夫 50ms;
  • 2. 独自的工作,没有执行上的先后顺序,然而须要同步,每个执行 200ms;
  • 3. 耗时、最不重要的工作,等所有工作执行结束后执行,每个执行须要 1000ms。

优化前

/**
 * 优化前的写法, 这里仅是演示模仿,理论的可能更简单
 */
private void doJobBeforeImprove(long startTime) {new TopPriorityJob(logger).doJob();
    for (int i = 0; i < 4; i++) {new SingleJob((i + 1), logger).doJob();}
    new LongTimeJob(logger).doJob();
    log("工作执行结束,总共耗时:" + (System.currentTimeMillis() - startTime) + "ms");
}

执行后果:

因为所有的工作都是执行在主线程,串行执行,所以花了大概 1865ms。

优化后

/**
 * 优化后的写法, 这里仅是演示模仿,理论的可能更简单
 */
private void doJobAfterImprove(final long startTime) {ConcurrentGroupTaskStep groupTaskStep = XTask.getConcurrentGroupTask();
    for (int i = 0; i < 4; i++) {groupTaskStep.addTask(buildSingleTask(i));
    }
    XTask.getTaskChain()
            .addTask(new MainInitTask(logger))
            .addTask(groupTaskStep)
            .addTask(new AsyncInitTask(logger))
            .setTaskChainCallback(new TaskChainCallbackAdapter() {
                @Override
                public void onTaskChainCompleted(@NonNull ITaskChainEngine engine, @NonNull ITaskResult result) {log("工作齐全执行结束,总共耗时:" + (System.currentTimeMillis() - startTime) + "ms");
                }
            }).start();
    log("主线程工作执行结束,总共耗时:" + (System.currentTimeMillis() - startTime) + "ms");
}

@NonNull
private XTaskStep buildSingleTask(int finalI) {return XTask.getTask(new TaskCommand() {
        @Override
        public void run() throws Exception {new SingleJob((finalI + 1), logger).doJob();}
    });
}    

执行后果:

因为只有优先级最重要的工作在主线程执行,其余工作都是异步执行,所以主线程工作执行耗费 57ms,所有工作执行耗费 1267ms。

3.2 IO 优化

3. 网络申请优化。

  • 启动过程中防止不必要的网络申请。对于那些在启动时非必要执行的网络申请,能够延时申请或者应用缓存。
  • 对于须要进行屡次串行网络申请的接口进行优化整合,管制好申请接口的粒度。比方后盾有获取用户信息的接口、获取用户举荐信息的接口、获取用户账户信息的接口。这三个接口都是必要的接口,且存在先后关系。如果顺次进行三次申请,那么工夫基本上都花在网络传输上,尤其是在网络不稳固的状况下耗时尤为显著。但如果将这三个接口整合为获取用户的启动(初始化)信息,这样数据在网络中传输的工夫就会大大节俭,同时也能进步接口的稳定性。

4. 磁盘 IO 优化

  • 启动过程中防止不必要的磁盘 IO 操作。这里的磁盘 IO 包含:文件读写、数据库(sqlite)读写和 SharePreference 等。
  • 对于启动过程中所必须的数据加载,抉择适合的数据结构。能够抉择反对随机读写、延时解析的数据存储构造以代替 SharePreference。
  • 启动过程中防止大量的序列化和反序列化。

3.3 线程优化

咱们在开发利用的过程中,都或多或少会应用到线程。当咱们创立一个线程时,须要向零碎申请资源,分配内存空间,这是一笔不小的开销,所以咱们平时开发的过程中都不会间接操作线程,而是抉择应用线程池来执行工作。

但问题就在于如果线程池设置不对的话,很容易被人滥用,引发内存溢出的问题。而且通常一个利用会有多个线程池,不同性能、不同模块乃至是不同三方库都会有本人的线程池,这样大家各用各的,就很难做到资源的协调对立,劲不往一处使。

3.3.1 线程优化剖析

1. 线程池耗时剖析。

要想进行线程优化,首先咱们就须要理解线程池在应用过程中,哪些地方比拟耗时。

  • 首先,从惯例上来讲,线程池在应用过程中,线程创立、线程切换和 CPU 调度比拟耗时。
  • 其次,须要从业务的角度去剖析以后利用的线程应用状况,到底是哪些工作导致线程池大量的创立和执行耗时。这里咱们能够应用 Hook 的形式,去 Hook 线程的创立 (ThreadFactory 的newThread 办法)和执行进行统计。
  • 最初,咱们须要联合业务的重要性(优先级)以及其相应的线程执行开销,进行综合考量,调整线程池的执行策略。

2. 线程池执行剖析。

而后再让咱们看看线程池的执行逻辑:

咱们晓得,一个线程池通常由一个外围线程池和一个阻塞队列组成。那么当咱们调用线程池去执行一个工作的时候,线程池是如何执行的呢?

外围线程池 -> 阻塞队列 -> 最大线程数(新建线程)-> RejectedExecutionHandler(回绝策略)
  • 当线程池中的外围线程池线程饱和后,工作会被塞进阻塞队列中期待。
  • 如果阻塞队列也满了,线程池会创立新的线程。
  • 然而如果目前线程池中的线程总数曾经达到了最大线程数,这个时候会调用线程池的回绝策略(默认是间接中断,抛出异样)。

其中外围线程池的最大线程数并不是设置的越大越好,为什么这么说?因为 CPU 的解决能力是无限的,四核的 CPU 一次也只能同时执行四个工作,如果外围线程池数设置过大,那么各工作之间就会相互竞争 CPU 资源,加大 CPU 的调度耗费,这样反而会升高线程池的执行效率。

一般来说,外围线程池的最大线程数满足上面的公式:

最佳外围线程数目 =((线程等待时间 + 线程 CPU 工夫)/ 线程 CPU 工夫)* CPU 数目

(1)线程等待时间所占比例越高,须要越多线程。
(2)线程 CPU 工夫所占比例越高,须要越少线程。

这里咱们思考两种最常见的状况:

  • CPU 密集型工作: 这种工作绝大多数工夫都在进行 CPU 计算,线程等待时间简直为 0,因而这时最佳外围线程数为 n + 1(或者为 n),这里 n 为 CPU 外围数。
  • IO 密集型工作: 这种工作,CPU 通常须要期待 I /O(读 / 写)操作,这样 CPU 的解决工夫和等待时间简直差不多,因而这时最佳外围线程数为 2n + 1(或者为 2n),这里 n 为 CPU 外围数。
3.3.2 线程优化指标

通过上面对线程池的剖析,咱们能够晓得:

  • 线程池设置过大,会强占内存,相互竞争 CPU 资源,减少 CPU 的调度耗时。
  • 线程池设置过小,工作会阻塞串行,升高线程池执行效率。

因而,咱们 线程优化的指标 是:

(1)领有能够兼顾全局的对立的线程池。
(2)能依据机器的性能来管制数量,正当调配线程池大小。
(3)可能依据业务的优先级进行调度,优先级高的先执行。

3.3.3 线程优化具体措施

1. 建设主线程池 + 副线程池的组合线程池,由线程池管理者对立协调治理。主线程池负责优先级较高的工作,副线程池负责优先级不高以及被主线程池回绝降级下来的工作。

这里执行的工作都须要设置优先级,工作优先级的调度通过 PriorityBlockingQueue 队列实现,以下是主副线程池的设置,仅供参考:

  • 主线程池:外围线程数和最大线程数:2n(n 为 CPU 外围数),60s keepTime,PriorityBlockingQueue(128)。
  • 副线程池:外围线程数和最大线程数:n(n 为 CPU 外围数),60s keepTime,PriorityBlockingQueue(64)。

2. 应用 Hook 的形式,收集利用内所以应用 newThread 办法的中央,改为由线程池管理者对立协调治理。

3. 将所有提供了设置线程池接口的第三方库,通过其凋谢的接口,设置为线程池管理者治理。没有提供设置接口的,思考替换库或者插桩的形式,替换线程池的应用。

3.4 闪屏优化

闪屏优化属于启动用户体验的优化。毕竟谁也不想应用页面一闪一闪的利用。

1. 设置自定义闪屏页。

设置自定义的闪屏页能够进步咱们启动的 ” 视觉速度 ”。通常会设置一个背景,而后把 logo 居中显示,能够应用 xml 文件来布局(留神,该图片不可展现动画,并且展现工夫也不可控)。这种形式能够给用户一种启动十分快的感觉,不仅解决了启动白屏的问题,并且展现了品牌 logo 也有助于晋升品牌认知。

(1) 应用 xml 自定义一张带有 logo 的图片。

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:opacity="opaque">
    <item android:drawable="?attr/xui_config_color_splash_bg"/>

    <item android:bottom="?attr/xui_config_app_logo_bottom">
        <bitmap
            android:gravity="center"
            android:src="?attr/xui_config_splash_app_logo"/>
    </item>

    <item android:bottom="?attr/xui_config_company_logo_bottom">
        <bitmap
            android:gravity="bottom"
            android:src="?attr/xui_config_splash_company_logo"/>
    </item>
</layer>

(2) 把这张图片通过设置主题的 android:windowBackground 属性形式显示为启动闪屏。

<style name="XUITheme.Launch.Demo">
    <item name="android:windowBackground">@drawable/xui_config_bg_splash</item>
    <item name="xui_config_splash_app_logo">@drawable/ic_splash_app_logo_xui</item>
    <item name="xui_config_splash_company_logo">@drawable/ic_splash_company_logo_xuexiang</item>
</style>

(3) 在 manifest 中将主页的主题设置为方才带启动图片的 Launch 主题。

<activity
    android:name=".activity.MainActivity"
    android:theme="@style/XUITheme.Launch.Demo">
</activity>

(4) 代码执行到主页面的 onCreate 的时候设置为程序失常的主题,这样就切回到失常主题背景了。

public class BaseActivity extends XPageActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {setTheme(R.style.AppTheme);
        super.onCreate(savedInstanceState);
    }
}

2. 应用 Android12 提供的启动画面 Splash Screen。

https://developer.android.google.cn/guide/topics/ui/splash-screen

3. 如果是因为某些不可抗拒的起因(例如一些大型游戏的首次加载),导致第一次启动十分慢的话,能够在主页面进入之前,减少一个进度条加载页面。这样的益处就是要通知用户,到底须要多久能力加载结束,给用户一个明确的信息,缓解用户的期待焦虑。

4. 缩小首页的跳转档次。除非某些不可抗拒的起因(比方广告作为一项重要支出起源),尽量不要设置启动页(SplashActivity),因为关上和敞开任何一个 Activity 都是须要耗费工夫的,每多一个 Activity 的跳转就意味着咱们主页面的关上工夫会被缩短。

3.5 主页面优化

主页面的启动和显示也是 app 启动十分重要的一部分。

3.5.1 布局优化

布局优化的外围就是:进步页面渲染的速度,避免页面适度渲染导致耗时。

  • 升高视图层级。缩小冗余或者嵌套布局,避免页面适度渲染。正当应用 merge 标签和束缚布局ConstraintLayout
  • 不必要的布局提早加载。用 ViewStub 代替在启动过程中不须要显示的 UI 控件。
  • 首页懒加载。首页不须要立刻显示的页面,能够应用懒加载。
  • 应用自定义 View 代替简单的 View 叠加。

3.5.2 页面数据预加载

一般来说,咱们喜爱在每个页面外部才开始加载和显示数据,因为这样写可能更容易让人看懂,利于当前的保护。然而如果等页面 UI 布局初始化结束后,咱们才去加载数据的话,势必会减少页面启动显示的工夫。

因为每个页面(Activity)的启动自身就是比拟耗时的过程,咱们能够将须要显示的数据进行预加载(即页面启动和数据加载同时进行,串行 -> 并行),这样等页面 UI 布局初始化结束后,咱们就能够拿着预加载的数据间接渲染显示了,这样能够缩小数据加载的期待,从而达到放慢页面显示的目标。

这里咱们能够参考开源预加载库:PreLoader

3.6 系统调度优化

  • 1.启动阶段不启动子过程 ,只在主过程执行 Application 的onCreate 办法。因为子过程会共享 CPU 的资源,导致主过程 CPU 缓和。
  • 2.启动过程中缩小零碎调用 ,防止与AMSWMS 竞争锁。因为 AMSWMS在利用启动的过程中承当了很多工作,且这些办法很多都是带锁的,这时利用该当防止与它们进行通信,避免出现大量的锁期待,阻塞要害操作。
  • 3.启动过程中除了 Activity 之外的组件启动要审慎。因为四大组件的启动都是通过主线程 Handler 进行驱动的,如果在利用启动的同时他们也启动,Handler 中的 Message 势必会减少,从而影响利用的启动速度。
  • 4.启动过程中缩小主线程 Handler 的应用 。起因同下面一样,Activity 的启动都是由主线程 Handler 进行驱动的,利用启动期间缩小主线程 Handler 的应用,能够减小对主页面启动的影响。对于那些量大且频繁的任务调度,能够应用HandlerThread 中的 Looper 创立属于子线程的 handler 来代替。
  • 5.应用 IdleHandler。利用 IdleHandler 个性,在音讯队列闲暇时,对提早工作进行分批初始化。

3.7 GC 优化

启动过程中该当缩小 GC 的次数。因为 GC 会暂停程序的执行,从而会带来提早的代价。那么咱们该当如何防止频繁的 GC 呢?

  • 1. 防止进行大量的字符串操作,特地是序列化和反序列化。不要应用 +(加号)进行字符串拼接。
  • 2. 防止长期对象的频繁创立,频繁创立的对象须要思考复用。
  • 3. 防止大量 bitmap 的绘制。
  • 4. 防止在自定义 View 的 onMeasureonLayoutonDraw中创建对象。

3.8 Webview 启动优化

如果你的利用应用到了 Webview,能够按需对 Webview 进行优化。

  • 1. 因为 WebView 首次创立比拟耗时,须要事后创立 WebView,提前将其内核初始化。
  • 2. 应用 WebView 缓存池,用到 WebView 的时候都从缓存池中拿。
  • 3. 利用内置本地离线包,如一些字体和 js 脚本,即预置动态页面资源。

3.9 利用瘦身

对利用瘦身,能够最间接地放慢资源加载的速度,从而进步利用启动的效率。

  • 1. 删除没有援用的资源。咱们能够应用 Inspect Code 或者开启资源压缩,主动删除无用的资源。
  • 2. 能用 xml 写 Drawable 的,就不要应用 UI 切图。
  • 3. 重用资源。同一图像的着色不同,咱们能够用 android:tint 和 tintMode 属性调整应用。
  • 4. 压缩 png 和 jpeg 文件。这里举荐一个十分好用的图片压缩插件:img-optimizer
  • 5. 应用 webp 文件格式。Android Studio 能够间接将现有的 bmp,jpg,png 或动态 gif 图像转换为 webp 格局。
  • 6. 应用矢量图形,尤其是那些与分辨率无关,且可伸缩的小图标尽可能应用矢量图形。
  • 7. 开启代码混同。应用 proGuard 代码混同器工具,包含压缩、优化、混同等性能。
  • 8. 对于一些独立也非必须的功能模块,采纳插件化,按需加载。

3.10 资源重排

利用 Linux 的 IO 读取策略,PageCache 和 ReadAhead 机制,依照读取程序重新排列,缩小磁盘 IO 次数。具体可参见《支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能》这篇文章。这种技术门槛较高,个别利用都不会用到。

3.11 类重排

类重排的实现通过 ReDex 的 Interdex 调整类在 Dex 中的排列程序。调整 Dex 中类的程序,把启动时须要加载的类按程序放到主 dex 里。具体实现能够参考《Redex 初探与 Interdex:Andorid 冷启动优化》这篇文章。

4. 如何进行优化

下面讲了那么多利用启动优化的策略和措施,可能有些人就会问了:那么具体到咱们每个不同的我的项目上,咱们应该如何进行优化呢?

以下是我集体的优化步骤,仅供参考:

  • 1.明确优化的内容和指标。首先,做任何优化肯定是须要带着问题(目标)去优化的。任何不带目标进行的优化都是耍流氓。
  • 2.剖析现状、确认问题。当咱们目前须要优化的内容后,接下来就是须要进行大量的埋点统计、比拟与剖析,确认到底是因为什么起因导致的利用启动过慢,找到须要优化的部位。
  • 3.进行针对性的优化。找到导致利用启动过慢的问题之后,就是依照本篇讲述的优化策略和措施,进行针对性的优化。
  • 4.对优化后果进行总结并进行继续跟进。对优化前后的数据进行统计和比拟,总结优化的教训并在组内进行分享,并在后续的版本中进行继续跟进。有条件的能够联合 CI,减少线上的启动性能监控。

最初

讲了这么多,还是心愿大家在平时开发的过程中,多器重一些利用启动优化的相干技巧,这样等他人让你优化利用启动的时候,也就不会那么不知所措了。

我是 xuexiangjys,一枚酷爱学习,喜好编程,勤于思考,致力于 Android 架构钻研以及开源我的项目教训分享的技术 up 主。获取更多资讯,欢送微信搜寻公众号:【我的 Android 开源之旅】

本文由 mdnice 多平台公布

正文完
 0