实现Android APK瘦身99.99%

摘要: 如何瘦身是 APK 的重要优化技术。APK 在安装和更新时都需要经过网络下载到设备,APK 越小,用户体验越好。本文作者通过对 APK 内在机制的详细解析,给出了对 APK 各组成成分的优化方法及技术,并实现了一个基本 APK 的最小化过程。

正文:

高尔夫运动中,分数最小者胜出。

让我们将这一原则应用到 Android App 开发中。我们将玩转一个称为“ApkGolf”的 APK,目的是创建一个尽可能具有最少字节数的 App,并可安装在运行 Oreo 的设备上。

基线测定

一开始,我们用 Android Studio 生成一个缺省的 App,创建密钥库(Keystore)

并对 App 签名,然后使用命令stat -f%z $filename测定生成 APK 文件的字节数大小。

进一步,为确保该 APK 工作正常,我们将在一台运行 Oreo 的 Nexus 5x 手机上安装它。

img

看上去挺漂亮。但是现在我们的 APK 大小近乎 1.5Mb。

APK Analyser

考虑到我们 App 的功能非常简单,1.5Mb 的规模看上去过于臃肿了。因此,我们要深入了解一下该项目,看看是否有一些能立竿见影地削减文件大小的地方。Android Studio 生成了:

  • 扩展AppCompatActivity而得到的MainActivity
  • 使用根视图ConstraintLayout的布局文件;
  • Value 文件,其中包含三种颜色、一个字符串资源(Resource)和一个主题(Theme);
  • AppCompatConstraintLayout的支持库;
  • 一个AndroidManifest.xml文件;
  • PNG 格式的启动图标,分别是正方形、圆形和前台的。

看上去首当其冲的目标是启动图标文件,因为 APK 中共包含了 15 个图像文件,并且在mipmap-anydpi-v26下还有两个 XML 文件。下面,让我们使用 Android Studio 的 APK Analyser

(https://developer.android.com/studio/build/apk-analyzer.html)

对该 APK 文件做一个定量分析。

img

给出的结果与我们的最初假设大相径庭,其中显示 Dex 文件是大头,而上述资源仅占 APK 大小的 20%。

文件大小占比
classes.dex74%
res20%
resources.arsc4%
META-INF2%
AndroidManifest.xml<1%

下面让我们逐个分析每个文件的行为。

Dex 文件

看上去罪魁祸首是classes.dex文件,它占据了 73% 的空间,因而它成为我们的首要削减目标。该文件为 Dex 格式

其中包含了我们的全部编译后代码,以及对 Android 框架和支持库中外部方法的引用。

然而android.support软件包中引用了超过 13000 种的方法,对于一个简单的“Hello World”App 而言,完全没有必要。

资源

目录“res”中包含了大量的布局(Layout)文件、Drawable 和动画,它们并非在 Android Studio UI 中立刻可见。同样,它们也是由支持库推入其中的,约占 APK 规模的 20%。

img

resources.arsc文件中,还包含了对每个资源的引用。

签名

目录“META-INF”中包含有CERT.SFMANIFEST.MFCERT.RSA文件,这些文件都需要 v1 APK 签名

(https://source.android.com/security/apksigning/v2#v1-verification) 。

如果有攻击者修改了我们 APK 中的代码,签名就会不匹配。这一机制保障了用户能避免执行第三方恶意软件的风险。

MANIFEST.MF文件中列出了 APK 中的所有文件。其中,CERT.SF文件中包含了文件清单的摘要,以及每个文件的独立摘要。CERT.RSA文件中包含了一个公钥,用于验证CERT.SF文件的完整性。

img

在签名文件中,没有目标明显可优化。

AndroidManifest 文件

看上去AndroidManifest文件非常类似于我们的原始输入文件。唯一差别在于,文件中的字符串和 Drawable 等资源被整数资源 ID 所替代,这些 ID 以0x7F开头。

启用最小化功能(Minification)

我们尚未在 App 的build.gradle文件中设置允许最小化(Minification)和资源收缩(Resource Shrinking)。我们现在做此设置:

android {buildTypes {release {minifyEnabled trueshrinkResources trueproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'}}
}
-keep class com.fractalwrench.** { *; }

minifyEnabled属性设置为“true”值,这将启用 Proguard

(https://www.guardsquare.com/en/proguard) ,

该功能将从 App 中剥离出那些未使用的代码,并对符号的名称做模糊化处理,使得 App 难以被反向工程。

设置shrinkResources属性,将会在 APK 中移除任何并非直接引用的资源。这时如果我们使用反射机制间接地访问资源,就会导致问题,但是本文给出的 App 并不存在这样的问题。

优化为 786 Kb(削减 50%)

我们已经实现了 APK 规模减半,并未对我们的 APP 有任何可见的影响。

img

对于那些尚未在 App 中启用AndroidManifest.xmlshrinkResources的开发人员,这是本文给出的最需要重视的并应学会的技巧。他们仅花费数小时做配置和测试,就能轻松地削减数兆的规模。

我们尚未了解 AppCompat 的工作机制

现在classes.dex文件已削减到占用 APK 的 57%。在我们的 Dex 文件中,大多数方法引用属于android.support软件包,因此我们将要去除该支持库。具体做法为:

  • build.gradle中彻底清除依赖块。 dependencies { implementation ‘com.android.support:appcompat-v7:26.1.0’ implementation ‘com.android.support.constraint:constraint-layout:1.0.2’ }
  • 更新MainActivity,以扩展android.app.Activity
public class MainActivity extends Activity
  • 更新布局,使用单一的TextView
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:text="Hello World!" />
  • 删除styles.xml文件,并从AndroidManifest文件的<application>元素中移除android:theme属性。
  • 删除colors.xml文件。
  • 在 gradle 同步时做 50 次上推(push-up)。

优化为 108 Kb(削减 87%)

天哪,我们刚刚实现了近十倍的削减,即从 786Kb 削减到 108Kb。唯一可见的更改是工具条(Toolbar)的颜色,现在它使用了缺省的 OS 主题。

img

目录“res”现在占用 APK 规模约 95%,原因是所有的加载图标。如果这些 PNG 图片是由我们自己的设计师所给出的,那么我们可以尝试 将它们转换为 WebP 格式,该格式更加高效,并被 API 15 及以上所支持。

幸运的是,Google 已经优化了我们的 Drawable。即便没有这种优化,ImageOptim 也可优化 PNG 并从中剥离不必要的元数据。

让我们当一次坏人,将我们所有的加载图标替换为单一的单像素黑点,并置于未验证的res/drawable目录中。图片大小约 67 个字节。

优化为 6808 字节(削减 94%)

我们已经移除了几乎全部的资源,因此毫不奇怪 APK 规模已经削减了约 95%。但是resources.arsc依然引用了如下项:

  • 一个布局文件;
  • 一个字符串资源;
  • 一个调用图标。

让我们从第一项着手。

布局文件(优化为 6262 字节,削减 9%)

Android 框架会膨胀我们的 XML 文件

并自动创建一个TextView对象,用于Activity对象的contentView

我们可以尝试一些跳过中间的过程,具体做法是移除 XML 文件,并使用程序设置contentView。这样会降低资源的规模,因为我们减少了一个 XML 文件。但是 Dex 文件将会增大,因为我们引用了额外的TextView方法。

TextView textView = new TextView(this);
textView.setText("Hello World!");
setContentView(textView);

让我们查看一下这一权衡做法的工作情况,它削减了 5710 个字节。

App 名称(优化为 6034 字节,削减 4%)

下面我们将删除strings.xml文件,并将AndroidManifest中的android:label属性值更改为“A”。这看上去是一个小更改,但是它从resources.arsc中删除了一项,削减了 Manifest 文件中的字符数,并从“res”目录中移除了一个文件。略有裨益,我们削减了 228 个字节。

加载图标(优化为 5300 字节,削减 13%)

Android Platform 代码库中的resources.arsc的文档

告诉我们,APK 中的每个资源通过resources.arsc中的一个整数 ID 引用。这些 ID 具有两个命名空间(Namespace):

0x01: 系统资源(预装在 framework-res.apk 中);0x7f: 应用资源(捆绑在应用的.apk 文件中)。

那么如果在0x01命名空间中引用了一个资源,我们的 APK 发生了什么?我们应该可以在削减文件规模的同时,得到一个更漂亮的图标。

android:icon="@android:drawable/btn_star"

img

虽然文档是这样说的,但是在一个生产 App 中,我们应该保持“永远不要信任系统资源”这一原则。该步骤会导致 Google Play 验证失败,而且考虑到我们知道某些制造商已经重定义了白色

因此在具体操作时需要慎重。

Manifest 文件(优化为 5252 字节,削减 1%)

目前为止,我们尚未对 Manifest 文件下手。

android:allowBackup="true"
android:supportsRtl="true"

移除这些属性将会削减 48 个字节。

防止破解(优化为 4984 字节,削减 5%)

看上去 Dex 文件中依然包括BuildConfigR

-keep class com.fractalwrench.MainActivity { *; }

如果我们精炼 Proguard 规则,就会清除掉这些类。

命名混淆(优化为 4936 字节,削减 1%)

现在对我们的Activity赋予一个混淆后的名字。对于正常类,Proguard 可自动实现混淆功能,但是考虑到Activity类名会通过Intents唤醒,因此缺省情况下不要混淆Activity的名字。

MainActivity -> c.javacom.fractalwrench.apkgolf -> c.c

META-INF(优化为 3307 字节,削减 33%)

当前在 App 签名中,我们使用了 v1 和 v2 签名。看上去这完全是浪费,尤其是 v2 会对整个 APK 做哈希,提供了更高级的保护能力和性能

(https://source.android.com/security/apksigning/#apk-signing-schemes)。

在 APK Analyser 中,v2 签名并不可见,因为它在 APK 文件本身中以二进制块的形式存在。v1 签名是可见的,它是以CERT.RSACERT.SF文件的形式给出。

Android Studio UI 中提供了 v1 签名的复选框,我们需要去除该选择,并生成一个签名的 APK。我们也需要做相反的过程。

签名大小(字节)
v13511
v23307

看上去从此以后我们使用的是 v2。

下面的操作将无需 IDE 的支持

现在我们要手工编辑我们的 APK 了。我们将使用如下命令:

# 1\. 创建一个未签名的 APK./gradlew assembleRelease# 2\. 解压缩归档文件。
unzip app-release-unsigned.apk -d app# 对文件进行编辑。# 3\. 压缩归档文件
zip -r app app.zip# 4\. 运行 zipalign。
zipalign -v -p 4 app-release-unsigned.apk app-release-aligned.apk# 5\. 使用 v2 签名运行 apksigner。
apksigner sign --v1-signing-enabled false --ks $HOME/fake.jks --out signed-release.apk app-release-unsigned.apk# 6\. 验证签名。
apksigner verify signed-release.apk

详细概述了 APK 签名过程。总而言之,gradle 生成了一个未签名的归档文件,zipalign 更改了未压缩资源的字节对齐方式,用于改进加载 APK 时的 RAM 使用,最后 APK 将被加密签名。

未签名且未对齐的 APK 大小为 1902 字节,这意味着签名和对齐过程增加了约 1 Kb。

文件大小差异(优化为 2608 字节,削减 21%)

很奇怪!我们对未对齐的 APK 解压缩并手工签名,并手动移除了META-INF/MANIFEST.MF,这削减了 543 字节。如果有人知道原因,请告诉我!

现在我们的签名 APK 中只有三个文件,当然还可以去除resources.arsc,因为我们并未定义任何资源!

这将使我们仅保留 Manifest 和classes.dex文件,两个文件大小相当。

压缩破解(Compression Hack)(优化为 2599 个字节,削减 0.5%)

让我们将剩余的字符串都更改为‘c’,更新版本为 26,然后生成一个签名的 APK。

compileSdkVersion 26buildToolsVersion "26.0.1"defaultConfig {applicationId "c.c"minSdkVersion 26targetSdkVersion 26versionCode 26versionName "26"}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="c.c"><applicationandroid:icon="@android:drawable/btn_star"android:label="c"><activity android:name="c.c.c">

这将削减 9 个字节。

尽管文件中的字符数并未改变,但是我们更改了‘c’字符的频次。这使得压缩算法可以进一步降低文件的大小。

你好,ADB(优化到 2462 字节,削减 5%)

通过移除```Activity````的 Launch Intent Filter,我们可以进一步优化 Manifest。此后,我们将使用如下命令加载 App:

adb shell am start -a android.intent.action.MAIN -n c.c/.c

下面给出新的 Manifest 文件:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="c.c"><application><activityandroid:name="c"android:exported="true" /></application>
</manifest>

我们还移除了加载图标。

削减方法引用(优化为 2179 字节,削减 12%)

我们最初需求是生成一个可安装在设备上的 APK。现在是运行“Hello World”的时候了。

我们的 App 引用了TextViewBundleActivity中的方法。通过移除Activity,并替换为用户定义的Application类,我们可以进一步削减 Dex 文件大小。现在我们的 Dex 文件应该仅引用了单一的方法,即Application的构造函数。

现在我们的源文件如下:

package c.c;
import android.app.Application;
public class c extends Application {}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="c.c"><application android:name=".c" />
</manifest>

我们可以使用 adb 验证该 APK 是可以成功安装的,也可以通过 Setting App 做验证。

img

Dex 优化(优化为 1961 字节,削减 10%)

在此次优化中,我花费了多个小时研究 Dex 文件格式

意在了解诸如校验码和偏移量等各种机制,它们是手工编辑文件中的难点。

但是长话短说,被我证实的是,只要存在classes.dex文件,APK 文件就能安装。因此,只要简单地删除原始文件并在终端运行touch classes.dex,使用这一空文件就能获得近 10% 的规模削减。

有时看上去最愚蠢的方法反而最有效。

理解 Manifest 文件(优化为 1961 字节,削减 0%)

非签名 APK 中的 Manifest 文件是二进制的 XML 格式,该格式看上去并没有官方的文档。我们可以使用 HexFiend编译器去修改文件内容

(https://github.com/ridiculousfish/HexFiend) 。

我们可以猜测出位于文件头部的数个感兴趣项。头四个字节编码了38,是与 Dex 文件所使用的版本相同。随后的两个字节编码为660,这无疑是文件的大小。

下面,我们尝试通过设置 targetSdkVersion 为1并更新文件大小头部为659,去删除一个字节。不幸的是,Android 系统拒绝了这个非法的 APK,因此看上去这里另有玄机。

无需理解 Manifest 文件(优化为 1777 字节,削减 9%)

下面我们让我们对整个文件输入虚字符,然后在不更改文件大小的情况下尝试安装 APK。这将确定校验码是否发挥作用,以及更改是否使得文件头部的偏移值失效。

令人惊奇的是,下图的 Manifest 文件被解释为一个有效的 APK,可运行在运行 Oreo 的 Nexus 5X 手机上:

img

我想我听到了负责维护BinaryXMLParser.java的 Android Framework 工程师对着枕头在大声尖叫。

为最大化收益,我们将使用空字节(Null)替换这些虚字符。这可使简化使用 HexFiend 查看文件的重要部分,也将使前期的压缩破解可削减一些字节。

UTF-8 格式的 Manifest 文件

下图给出了一些 Manifest 文件中的重要成分。如果没有这些成分,APK 将会安装失败。

img

一些事情即刻是很明显的,例如 Manifest 文件和软件包标记。在字符串池中还可以找到软件包名称和 versionCode。

十六进制的 Manifest 文件

img

以十六进制查看文件可显示文件头部的值,这些值描述了字符串池及其它值,例如0x9402是文件的大小。字符串也具有一种有意思的编码。如果字段超出了 8 个字节,它们的总长度将在随后的两个字节中指定。

但是,看上去我们并不能从中做更进一步的削减。

大功告成?(优化为 1757 字节,削减 1%)

让我们查看一下最终的 APK。

img

终归,我们使用 v2 签名在 APK 中留名。让我们创建一个利用压缩破解的新密钥库。

img

这可削减 20 个字节。

第五阶段:最终采纳

现在的1757个字节是相当的小。据我所知,这是最小的现有 APK。

但是我完全有理由确信,Android 社区中会有人能再做进一步的优化,并打破我的记录。

更多Android进阶指南 可以扫码 解锁 《Android十大板块文档》

1.Android车载应用开发系统学习指南(附项目实战)

2.Android Framework学习指南,助力成为系统级开发高手

3.2023最新Android中高级面试题汇总+解析,告别零offer

4.企业级Android音视频开发学习路线+项目实战(附源码)

5.Android Jetpack从入门到精通,构建高质量UI界面

6.Flutter技术解析与实战,跨平台首要之选

7.Kotlin从入门到实战,全方面提升架构基础

8.高级Android插件化与组件化(含实战教程和源码)

9.Android 性能优化实战+360°全方面性能调优

10.Android零基础入门到精通,高手进阶之路

敲代码不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔

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

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

相关文章

【PyGame/PyQy5】get-start 快速入门

1.windows、linux&#xff0c;mac 安装python3 2.PC系统安装pygame, 指令&#xff1a;pip install pygame 3. 保存如下文件&#xff1a; test_game.py 4.PC上运行 python test_game.py import pygame# 初始化Pygame pygame.init()# 创建游戏窗口 window pygame.display.s…

【面试经典150题】跳跃游戏Ⅱ

题目链接 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 < j < nums[i]i j < n 返回到达 nums[n…

SeamlessM4T—Massively Multilingual Multimodal Machine Translation

本文是LLM系列的文章&#xff0c;针对《SeamlessM4T—Massively Multilingual & Multimodal Machine Translation》的翻译。 SeamlessM4T&#xff1a;大规模语言多模态机器翻译 摘要1 引言2 多模态翻译的社会技术维度2.12.22.3 3 SeamlessAlign&#xff1a;自动创建语音对…

产品经理如何进行需求管理

产品经理在进行需求管理时&#xff0c;可以遵循以下步骤&#xff1a; 1. 确定需求目标&#xff1a;明确产品的愿景和目标&#xff0c;确定需求管理的方向和重点。 2. 收集需求&#xff1a;与利益相关者&#xff08;包括用户、业务部门、技术团队等&#xff09;沟通&#xff0c;…

Spring Boot多数据源配置运行报错:No operations allowed after connection closed连接异常的解决

上一篇文章我们讲了如何配置多数据源&#xff0c;但是配置在使用一段时间之后&#xff0c;查询数据库会发生报错&#xff1a;No operations allowed after connection closed。 一、问题原因&#xff1a; 经过排查发现是因为MySQL5.0以后针对超长时间DB连接做了一个处理&#…

语言深入理解指针(非常详细)(三)

目录 数组名的理解使用指针访问数组 一维数组传参的本质二级指针指针数组指针数组模拟二维数组 数组名的理解 在上⼀个章节我们在使用指针访问数组的内容时&#xff0c;有这样的代码&#xff1a; int arr[10] {1,2,3,4,5,6,7,8,9,10}; int *p &arr[0];这里我们使用 &am…

史上最全PMP学习资料、项目管理资料、备考经验包,3A一次通过

你是否也有过类似的经历&#xff1f; 为了获取备考资料&#xff0c;有的同学在论坛、知乎或者相关垂直类网站下载了很多的资料&#xff0c;这些资料大部分是机构进行获客引流的资料&#xff0c;没有真正的干货。 经常会看到10G、20G的资料包&#xff0c;感觉内容很丰富&#xf…

【LeetCode算法系列题解】第51~55题

CONTENTS LeetCode 51. N 皇后&#xff08;困难&#xff09;LeetCode 52. N 皇后 II&#xff08;困难&#xff09;LeetCode 53. 最大子序和&#xff08;中等&#xff09;LeetCode 54. 螺旋矩阵&#xff08;中等&#xff09;LeetCode 55. 跳跃游戏&#xff08;中等&#xff09; …

HTML <thead> 标签

实例 带有 thead、tbody 以及 tfoot 元素的 HTML 表格: <table border="1"><thead><tr><th>Month</th><th>Savings</th></tr></thead><tfoot><tr><td>Sum</td><td>$180<…

智慧电力系统

智慧电力系统依托电易云-智慧电力物联网&#xff0c;将人工智能技术、物联网技术与电力行业相结合&#xff0c;通过数据的收集、分析和应用&#xff0c;实现电力资源精准调度、安全稳定运行、智能管理的全过程解决方案。 智慧电力系统的核心理念是将科技与能源紧密结合&#x…

Ubuntu 介绍和使用

Ubuntu 介绍和使用 一.Debian&Ubuntu Ubuntu 是基于另一个 Linux 发行版 Debian 发展而来&#xff0c;在介绍 Ubuntu 前有必要 了解了解 Debian 的历史。 1.1 Debian Debian 也被称为 Debian GNU/Linux&#xff0c;是众多 Linux 发行版之一。 主要由Ian Murdock在 1993 …

pycharm创建的虚拟环境为什么用conda env list命令查询不到?

问题描述&#xff1a;pycharm创建的虚拟环境为什么用conda env list命令查询不到。 pycharm开发环境可以创建虚拟环境&#xff0c;目的是为隔绝其他环境种库带来的版本干扰&#xff0c;但是发现一个问题&#xff0c;无论是在windows终端、anaconda终端、Pycharm开发环境中的终…

【Spring】SpringBoot的10个参数验证技巧

这里写目录标题 前言1.使用验证注解2 使用自定义验证注解3 在服务器端验证4 提供有意义的错误信息5 将 i18n 用于错误消息messages.properties6 使用分组验证7 对复杂逻辑使用跨域验证8 对验证错误使用异常处理9 测试你的验证逻辑10 考虑客户端验证总结 前言 参数验证很重要&am…

第一次做接口测试用例

一、简介 在开始接口测试之前&#xff0c;我们想一下&#xff0c;接口测试的流程是什么&#xff1f;说到这里&#xff0c;有些人就会产生好奇和疑问&#xff0c;心里mmp&#xff1a;接口测试要什么流程哈&#xff1f;&#xff1f;&#xff1f;不就是参考接口文档&#xff0c;直…

C# 实现PictureBox从指定的文件夹内进行翻页操作

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System

如何通过构建遥感光谱反射信号与地表参数之间的关系模型来准确估算植被参数?植被参数光学遥感反演方法(Python)及遥感与生态模型数据同化算法

目录 专题一 植被参数遥感反演理论 专题二 植被叶片及冠层反射率模拟与处理 专题三 植被遥感模型参数敏感性分析 专题四 基于查找表(LUT)方法反演植被参数 专题五 基于优化算法反演植被参数 专题六 基于机器学习反演植被参数 专题七 遥感数据同化理论 专题八 同化遥感反…

《论文阅读》用提示和释义模拟对话情绪识别的思维过程 IJCAI 2023

《论文阅读》用提示和复述模拟对话情绪识别的思维过程 IJCAI 2023 前言简介相关知识prompt engineeringparaphrasing模型架构第一阶段第二阶段History-oriented promptExperience-oriented Prompt ConstructionLabel Paraphrasing损失函数前言 你是否也对于理解论文存在困惑?…

Ceph入门到精通-Nginx 大量请求 延迟优化

优化nginx以处理大量请求并减少延迟可以通过以下几种方法实现&#xff1a; 调整worker_processes和worker_connections参数&#xff1a;增加worker_processes值可以增加nginx的进程数量&#xff0c;提高并发处理能力。增加worker_connections参数的值可以增加每个worker进程可…

MASM32编程状态栏显示字符动画,按钮跑马灯

一、需求分析 由于sysInfo扫描的内容比较多&#xff0c;打算为它增加一点动画效果&#xff0c;提醒用户程序正在运行&#xff0c;耐心等待。 二、构建测试窗口 测试窗口上放置有一个按钮&#xff0c;按钮上的初始文字是“开始扫描”&#xff1b;并使用状态栏&#xff0c;状态…

视频垂直镜像播放,为您的影片带来新鲜感

大家好&#xff01;在制作视频时&#xff0c;我们常常希望能够给观众带来一些新鲜感和独特的视觉效果。而垂直镜像播放是一个能够让您的影片与众不同的技巧。然而&#xff0c;传统的视频剪辑软件往往无法直接实现视频的垂直镜像播放&#xff0c;给我们带来了一些困扰。现在&…