V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
little2song
V2EX  ›  Android

深入理解 android 包体积优化,给 apk 瘦身全部技巧

  •  
  •   little2song · 2021-10-23 21:52:14 +08:00 · 12405 次点击
    这是一个创建于 1128 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    随着 iphone13p 最大内存放大到了 1T ,大内存手机的时代悄然降临,在 android 里面,三星也有,罗老师几年前说:如果我告诉你们我们在做 1T 的手机,你们可能以为我疯了

    看看现在,估计未来会有更多手机有 1T 版,大家开始真香了。

    但是,如果现在有人说:要做一个 1T 大小的 app ,那他可能是真疯了,至少未来十年不可能。因为手机内存是越大越好,你一个 app 当然是能小就小呀

    Android app 的文件格式为 apk ,本文就是探讨对于一个 android apk ,有哪些方法可以减小体积

    Apk 组成

    要想减小体积,首先我们需要了解 apk 的构成

    373c7fa912fa93d601a2bee46c76ae2d.jpg

    • 我们写的.java 文件会被编译为.class 文件,再由 dx 工具编译为 Classes.dex 文件,由于 android 限制,每个 dex 文件最多 65535 个方法,所以多出来的方法就生成 Classes2.dex , Classes3.dex~ClassesN.dex

    • Resource(res)与 Assets 比较像,区别是 res 目录下会生成资源 ID ,并在.R 文件中记录,可以直接使用,这里平常我们用得很多,而 assets 不会有 ID ,而是通过 AssetManager 接口获取;

      所以 res 类似于我们的桌面,一般放我们要操纵的控件资源,而 assets 类似于桌下的抽屉,放诸如数据库,html 这类资源

    • Native Libraries 平时打交道少,优化空间也很有限

    上面是抽象的 apk 结构,下面我们看一个实际的

    将 qq.apk 拖入 android studio

    image-20211023160756347

    可以看到最大的 R 文件夹,点进去,都是一些图片,第二大的是 assets ,里面是一些表情包以及插件图片

    其他的我们刚刚也说过,值得注意的是,里面多了一个 META-INF

    他存放了应用的签名信息,其中

    • .MF: 每一个资源都有一个 SHA1 签名,存放在这里

    • .SF: 文件存放.MF 经过 base64 编码后的签名

    • .RSA: 对.SF 文件使用 SHA1 算法生成数字摘要(注意:.MF 中是对每一个资源进行 SHA1 ,这里是对文件),然后进行 RSA 加密,再用开发者私钥进行签名,安装时使用公钥解密

    这样子,一个 app 安装在手机时,解密这一数字摘要,然后与内部的.MF 文件比对,如果相符,证明资源内容没有被修改

    Dex 文件

    在 APK 组成中我们可以看到,占用内存最大的是 res ,assets 与 classs.dex 文件,这也是我们的优化方向,接下来,我们看看如何优化 dex

    首先我们看看 dex 的结构

    undefined

    更详细的版本在官网,这里如果对这些结构的作用有兴趣,可以看下图的详细版本

    image-20211023162712238

    ProGuadrd

    dex 是代码编译而来,而对于代码文件,最重要的优化就是混淆了,将方法名,属性名等变为又短又无意义的名字,不仅能缩小体积还能避免反编译被人破解

    在 IDE 中,我们可以看到 qq 里面的类都是小写字母,里面的变量和方法都按字母顺序排列了,从 a 开始

    image-20211023163108352

    除了修改变量名,ProGuadrd 还可以在功能等价的基础上重写代码,比如把多个函数调用写到一个函数里面去,更加增大了阅读理解难度(虽然初学者一般已经这样做了),以及打乱格式,增加空格等

    主要步骤如下

    • 压缩( Shrink ): 检测和删除没有使用的类,字段,方法和特性。

    • 优化( Optimize ) : 分析和优化 Java 字节码。

    • 混淆( Obfuscate ): 使用简短的无意义的名称,对类,字段和方法进行重命名。

    • 预检( Preveirfy ): 用来对 Java class 进行预验证(预验证主要是针对 JME 开发来说的,Android 中没有预验证过程,默认是关闭)。

    D8 与 R8 优化

    这两平时接触不多,他们主要是在字节码处做优化的,开发时感知不强(感觉就是用来面试的)

    D8 主要是在编译字节码时重排序,将占用空间变得更小,比如对于 greetingType 方法,正常编译后的结果是

    [000584] Main.greetingType:(LGreeting;)Ljava/lang/String;
    0000: sget-object v0, LMain$1;.$SwitchMap$Greeting:[I
    0002: invoke-virtual {v2}, LGreeting;.ordinal:()I
    0005: move-result v1
    0006: aget v0, v0, v1
    0008: packed-switch v0, 00000017  // 这里
    

    如果使用 D8 优化,编译后的结果

    [0005f0] Main.greetingType:(LGreeting;)Ljava/lang/String;
    0000: sget-object v0, LMain$1;.$SwitchMap$Greeting:[I
    0002: invoke-virtual {v1}, LGreeting;.ordinal:()I
    0005: move-result v1
    0006: aget v0, v0, v1
    -0008: packed-switch v0, 00000017  //  这里
    +0008: const/4 v1, #int 1
    +0009: if-eq v0, v1, 0014
    +000b: const/4 v1, #int 2
    +000c: if-eq v0, v1, 0017
    

    可以看到 0008 处后的几条指令有变化,多了几个 if ,对于不同的 case 做创建不同的变量,可以节省空间

    R8 也类似,只是策略有些不一样

    更详细的了解可以参考 D8 Optimizations

    总之,他们的作用是就是,在不改变功能的情况下,重写部分 class 指令,减小空间占用,但是有可能会增加指令数量

    Redex 优化

    Redex 是 Facebook 推出的一个优化 Dex 文件的工具,和 D8R8 一样,也是对字节码的处理,有以下效果

    1. 内联函数,减少调用
    2. 删除无用代码
    3. 将只有一个实现类的接口或者父类用实现类代替
    4. 字符串混淆所见

    ……

    不过这个我没用过,但是感觉 Proguard 与 D8R8 都多多少少能做到,可能是他在细节上用了更好的算法

    但是不管多少框架,对 dex 文件的优化说来说去也就这些

    移除多余的库与代码

    最后是移除第三方库和冗余代码,属于业务逻辑上的原因

    • 多余的库

      对于自己的小项目,还好,对于多人参与的大型项目,很有可能对同一个功能,不同的人用了不同的轮子,手 Q 里面就有,比如要写单测,之前使用 Powermock ,后来用 JMock ,再后来改为 Mockk ,一个项目,三个单测框架

      由于不同的单测框架已经写了不少单测,短时间移除是不太可能的,但是可以慢慢转为同一种单测框架

    • 多余代码

      Android studio 会自己检测,没有用过的会置位灰色提醒,但是会漏掉很多,通过插件 Lint 可以检测,

    资源清理

    上面都是在代码层面减小 dex ,apk 的另一个空间占用大户,是资源,尤其是其中的图片,

    图片,你可知道,多少 OOM 因你而起?多少 app 因你闪退?

    图片压缩与更换格式

    我们先看看图片为什么那么大

    图片的显示,有 ARGB 4 个通道,其中默认的显示模式是 ARGB8888 ,ARGB8888 表示每个通道的颜色区间为[0,255],也就是两个 16 进制数表示,也就是 8bit -> 1 字节

    所以 ARGB8888 模式下,一个像素 4 个通道下占用 4 字节,一张 1024*1024 的手机图片图片,就是 $$ 2^{10} * 2^{10} * 2^2 = 2^{22} = 4M $$ 一张图 4M ,太离谱了!

    上面是打开后在运存的占用,我们可以修改颜色通道,不然 ARGB565 来减小单个像素所占用运存,不过有点跑题,本篇我们讲的是 app 的大小,也就是所占用手机的内存(我们约定 手机运存 = 电脑内存,手机内存 = 电脑硬盘)

    内存与运存中的图片存在形式是不一样的,压缩方法也不一样,很多人容易弄混

    回到内存,内存中,图片是以 png ,jpg 等格式存储

    我之前开发的时候都是先将 png 图片,往 tinypng 网站中压缩一下再放入,所以可以压缩图片,一般能压个三分之一~三分之二。

    也可以更换图片格式,比如 webp ,svg 可以更小,android studio 也提供了对应的支持,但是没有最好的格式,只是适用场景不同

    几种格式的优缺点

    这里多提一下 webp ,因为这是 google 推出的,大家在谷歌浏览器下载图片的时候,一般默认下载下来就是 webp 格式,所谓更小的内存占用,本质上是对图片进行了压缩,webp 的压缩算法是 VP8 视频编码,核心逻辑就是将图片分割成更小的子块,然后预测周围像素值,预测越准,周围的像素值就可以删去,再在图片打开时算出删掉的像素

    图片网络化

    在微信或者 qq 聊天中,对方发来一张图片,我们在聊天窗口往往先看到一张很模糊的缩略图,当点击时才会加载出高清图,

    这个思路也可以用在 apk 中,很多入口较深的高清大图,或者需要经常更新的图片,也许用户根本不看,就没有必要内置在 apk 中,看时加载即可,如果需要提前占位置,可以用缩略图代替

    至于哪些图网络化,需要根据业务与用户体验来权衡了

    比如淘宝,在断网情况下打开时,只有 icon 内置了

    image-20211023211648469

    其他策略

    无论是对 Dex 还是对资源进行优化,虽然安全有效,但是本质上是将原来有的东西变得更小,对 apk 的瘦身程度是有限的,还有一些”七伤拳“,优化率极高,但是对 apk 的影响也很大,需要谨慎使用。

    插件化

    所谓插件化,就是将 apk 中的非主要功能弄成独立的 apk ,原主 apk 称为宿主。

    比如支付宝里面,就是搞支付的,那么他里面的什么口碑,基金,天猫一堆乱七八糟,同时功能独立的东西就非常适合做成插件,用户用到的时候再从网络加载进来,这样极大的减少了 apk 占用。

    但是这里涉及到比较多的技术问题:

    1. 用户现在只有宿主 apk ,如何让宿主加载到插件 apk 里面的代码?
    2. android 四大组件都需要到 manifest 中注册,插件里面的组件显然不可能提前注册到宿主的 manifest 中(不然注册了,插件没加载进来,会找不到类),所以如何让系统认为下载下来的插件有注册?
    3. 宿主与插件资源能否正确互相引用?

    一般来说,通过的是代理和反射来处理,腾讯有一个 shadow 框架可以大致实现”零反射“,

    • 复用独立安装 App 的源码
    • 零反射无 Hack 实现插件技术
    • 全动态插件框架
    • 宿主增量极小
    • Kotlin 实现

    不过插件化技术不在今天的讨论范围,有兴趣可以研究下tencent-shadow

    当使用了插件化后,项目基本是要重构了,相比起改改 Dex 和图片,这个工程量极大,但是收益也会很高

    webview

    这里类似于图片网络化,相对于图片,直接将整个界面都变成 url ,

    我们手机 app 中的小程序一般都是 url 显示在 webview 中

    相关技术可以使用 jsBridge 与 Hybird ,本质上就是通过 bridge 连接 h5 与 android iOS ,实现通信

    image-20211023201811533

    不过代价就是,加载速度慢于原生,还要注意防止网址篡改等

    小结

    本文我们讨论的是 apk 的瘦身方案,首先先明确了 apk 的主要组成部分为 dex 文件与资源文件

    • 对于 dex 文件,我们可以进行混淆,字节码重排序,移除多余库与代码

    • 对于资源文件,我们可以替换格式,压缩图片,网络化

    除了这些常规操作,我们还可以使用插件化与 Webview 方法极致减少体积,但是这两个技术工程量大,而且有性能代价,需要谨慎使用。

    参考资料

    深入探索 Android 包体积优化(匠心制作-上)

    Android 项目中资源文件 -- asset 目录和 res 目录

    顶象 App 加固技术解析:DEX 文件格式的详解

    D8 Optimizations

    Android 开发应该掌握的 Proguard 技巧

    40 条回复    2021-10-25 12:00:42 +08:00
    geekvcn
        1
    geekvcn  
       2021-10-23 22:20:26 +08:00   ❤️ 5
    毫无意义,一个技术过硬的码农,可以完全用 NDK 开发一个纯原生,甚至图标界面全都用代码矢量绘制,完事一个体积小巧性能飞快的 APP 诞生了,然后呢?

    然后淘宝 微信 QQ 等一众垃圾代码写的软件还内置个垃圾 webview 把省下的空间一秒钟占掉,内存也占掉,这个技术过硬的码农的软件被系统后台强杀了,流氓软件在相互唤醒保活和其他非同阵营流氓软件打架。

    不说用 NDK 开发,安卓现在能用 JAVA Kotlin 写原生软件的都不多了,webview 套壳多方便,热更新,前端随便招,软件 H5 部分全平台通用,JS 再慢又如何。
    zpxshl
        2
    zpxshl  
       2021-10-23 22:39:46 +08:00 via Android   ❤️ 5
    @geekvcn 张口就来。
    1 主流 app 的包体积控制一直是 kpi ,包体积和转换率有正相关关系,多家实验做过了。
    2 内置 webview 在国内基本是必须的,做过的都知道国内手机 webview 的坑有多少,不同版本,各家魔改。
    3 楼主提包体积,你提保活,搭不上边吧。
    3 js 再慢又如何???
    不是所有 app 都是微信那种它做得再差用户都得用的。
    Cheons
        3
    Cheons  
       2021-10-23 23:07:28 +08:00 via Android
    国内手机厂商坐一块把内置的 webview 标准给统一了,最简单也是最难的一步
    yuhuazhu
        4
    yuhuazhu  
       2021-10-23 23:12:31 +08:00
    真羡慕 iOS 的 webview ,Android 不用第三方的一大堆问题要搞 T^T
    makelove
        5
    makelove  
       2021-10-23 23:48:11 +08:00   ❤️ 1
    这个叫内存,那真的内存叫什么

    我对包体积不太在意,但很在意内存用量,象淘宝这种一打开就要用 1.6G 的 App,不知道他们内部会用低端一点的 4G 内存手机测试吗,简直没法用
    little2song
        6
    little2song  
    OP
       2021-10-24 00:09:24 +08:00
    @makelove 文章有提到 [我们约定 手机运存 = 电脑内存,手机内存 = 电脑硬盘]
    little2song
        7
    little2song  
    OP
       2021-10-24 00:15:26 +08:00   ❤️ 1
    @geekvcn 大哥,不要这么极端,另外,我之前参与了手 Q 开发,也使用过上面的一些方法来减小体积,纯原生的 apk 价值不大,可以做个小组件,或者工具类 App , 对于亿级流量的 app ,webview 是必须的,但是,也不至于全是 webview 套壳,事实上,手 Q 里面 Webview 的代码占比很小
    mxalbert1996
        8
    mxalbert1996  
       2021-10-24 00:19:43 +08:00 via Android
    你对 R8 的理解是错的。R8 跟 ProGuard 一样都是 Code Shrinker ,功能也和 ProGuard 一样包括压缩混淆优化,是 Google 开发的 ProGuard 替代品。从 AGP 3.4.0 开始 R8 就是默认的 Shrinker ,如果不显式制定的话是不会用 ProGuard 的(也没有必要用,R8 比 ProGuard 更强大,能进行的优化更多)。
    little2song
        9
    little2song  
    OP
       2021-10-24 00:35:03 +08:00
    @mxalbert1996 受教了,是我想当然了,还得多学习
    geekvcn
        10
    geekvcn  
       2021-10-24 00:54:47 +08:00 via iPhone   ❤️ 4
    @zpxshl 别特么给自己找理由了,还 KPI ,国内手游明明可以做到启动器数据包分离,这样游戏安装包几十兆就能搞定,但是都内置数据包,完事解压运行双倍空间占用,有 KPI 脑子有坑才这样做。
    AX5N
        11
    AX5N  
       2021-10-24 01:12:31 +08:00
    @makelove 真的内存就叫“真的内存”
    codehz
        12
    codehz  
       2021-10-24 08:13:50 +08:00 via Android
    @geekvcn 还是国内生态的问题,没有统一商店分发,所有东西最好打包到一起才能“方便”地交给用户(运行时下载的体验实在不好),因此最好是由应用商店统一分发。不同厂商提供商店的差异很大,很多根本不支持单独分发 obb 。。。
    Caan07
        13
    Caan07  
       2021-10-24 08:46:10 +08:00
    逆天了,我竟然不知道 iphone13p 最大内存放大到了 1T ?如果 1T 内存那 13p 的价格真的划算到极点。
    Jabin
        14
    Jabin  
       2021-10-24 09:17:18 +08:00
    基本同意 @geekvcn
    瘦身文章一大堆, 关键还是
    1. 混淆
    2. 减少资源图片, 换用矢量图
    至于使用 webview, 就没啥可聊的了, 把 app 当浏览器使用没啥好说的
    模块 /插件化的东西不能完全算作减小 apk 体积大小的方法, 那根不不算一个完整的 apk, 用户更新安装也需要时间, 插件也占用内存, 和瘦身一起聊意思变味了
    zpxshl
        15
    zpxshl  
       2021-10-24 09:23:39 +08:00 via Android   ❤️ 2
    @geekvcn 你世界观里是不是非黑即白。kpi 的意思是指标,不是不顾一切的指标,一个项目里面多少指标只考虑一个吗?
    王者荣耀新安装启动要下载 3g 的数据包体验很好??? 另外下载下来的数据包就不占空间吗? 下载的带宽成本呢?
    没搞过移动端你就别回复了。 作为用户你喷产品随便喷,这帖子好好讨论技术就别瞎杠了。
    zpxshl
        16
    zpxshl  
       2021-10-24 09:25:29 +08:00 via Android
    还有一点楼主没提到,通过混淆删 kotlin 的部分生成代码,比如判空,实现简单收益良好
    zpxshl
        17
    zpxshl  
       2021-10-24 09:29:39 +08:00 via Android
    @Jabin 反驳插件的看法。
    1 apk 的体积对用户下载留存率有正相关关系。
    2 插件一般用于非核心功能,没下载也大概率不影响用户使用。举个例子,我们内置的 webview 内核就是动态下发的,下发成功前降级使用系统的 webview 内核。
    这里包体积其实分两种,安装包的体积和安装后的体积,一般关注前者,说白了市场上大部分用户关注啥,产品就关注啥,哪个技术优化带来留存提高都是有实验的。
    NewYear
        18
    NewYear  
       2021-10-24 09:33:31 +08:00
    路过,我家服务器也才 128G 内存,你家一部手机就 1T 内存了,您知道每增加 1G 内存就会增加多少耗电量么? 1T 内存就你那小身板的电池能扛得住?还 1T 内存,就你最离谱,讲的是专业的科普,说的却是基本名词都错。
    v2yllhwa
        19
    v2yllhwa  
       2021-10-24 09:49:55 +08:00 via Android
    @NewYear [我们约定 手机运存 = 电脑内存,手机内存 = 电脑硬盘]
    我觉得没必要争论这个名词,尤其是在楼主已经约定了的情况下...
    loukky
        20
    loukky  
       2021-10-24 10:10:13 +08:00 via Android   ❤️ 1
    其实不用约定,至少在塞班时代,手机运存真就等于电脑内存,手机内存真就等于电脑硬盘,前面的人真是少见多怪。
    jemyzhang
        21
    jemyzhang  
       2021-10-24 10:11:10 +08:00
    不应该是手机内存=电脑内存,手机存储=电脑硬盘?哪里来的运存这个词
    jemyzhang
        22
    jemyzhang  
       2021-10-24 10:12:03 +08:00   ❤️ 1
    英文 storage & memory
    loukky
        23
    loukky  
       2021-10-24 10:14:11 +08:00 via Android
    @jemyzhang 运行内存的简称
    so898
        24
    so898  
       2021-10-24 11:39:31 +08:00
    咋说呢,拼多多之前这样一套东西可能还有取舍,还有较为合理的 KPI 制定
    拼多多之后,开场一个 H5 ,所有 Native 组件都后面插件化下载装载,管他核心功能非核心功能……
    我们老大说过一句很经典的话:当你的应用做到拼多多那样 1M 不到的时候,用户不小心点了一个广告,还没来得及反悔,应用就下完了
    NewYear
        25
    NewYear  
       2021-10-24 11:53:54 +08:00
    @v2yllhwa
    @loukky

    我也勉强作为一个开发者,我是真的不明白,为什么要把同一个东西搞两个名字,而不同的两个东西却使用同一个名称。

    普通人这不是更容易弄混吗?一个东西一个名称让普通人更加好记不是么?

    毕竟现在大家都有电脑和手机。两个东西都会遇到,交流的时候也会用到啊。
    AlexPUBLIC
        26
    AlexPUBLIC  
       2021-10-24 13:10:35 +08:00   ❤️ 2
    其实压缩的奥义只有一个:一个聊天工具就干聊天的活就好,非要整什么看点,视频号,钱包;一个支付工具就做支付好了,不要总想着做社交,断舍离,组件少了,自然体积就下来了,啥都想干,去定制 OS 吧,比如 hm
    Lemeng
        27
    Lemeng  
       2021-10-24 14:35:13 +08:00
    太用心了,有点长
    jiayong2793
        28
    jiayong2793  
       2021-10-24 16:32:02 +08:00
    老板、领导:这不是没事找事吗?新功能写好了没?
    muzuiget
        29
    muzuiget  
       2021-10-24 16:56:16 +08:00   ❤️ 1
    写这种文章的“内存”只是指 storage 而不是 memory ,就不想看了。
    cev2
        30
    cev2  
       2021-10-24 19:21:28 +08:00   ❤️ 1
    下面楼都歪了,→_→我还是习惯手机上 RAM=内存,ROM=闪存的叫法
    bclerdx
        31
    bclerdx  
       2021-10-24 20:40:09 +08:00 via Android
    @Jabin 把 app 当浏览器是不对的吧,app 应该调用外置浏览器吧。
    bclerdx
        32
    bclerdx  
       2021-10-24 20:43:15 +08:00 via Android
    @AlexPUBLIC 不搞点啥,怎么创收呢。
    little2song
        33
    little2song  
    OP
       2021-10-24 22:03:25 +08:00   ❤️ 1
    @muzuiget 不知道你哪来的优越感,我分享文章不是给您过目的
    sw926
        34
    sw926  
       2021-10-24 23:04:33 +08:00
    优化包体积是一个“伪技术”,重复代码和重复资源太多,再怎么优化也没用,关键还是要编码的时候注意,代码要封装,资源要尽量复用。
    diaosi
        35
    diaosi  
       2021-10-25 00:25:31 +08:00
    @little2song #6 不是手机开发,看到你一楼描述第一反应仍然是惊讶 iPhone 内存居然到 1T 了,再转头一想觉得怎么都不可能,才意识到你说的是存储空间。所以我们为什么要改变约定已久的称呼去重新约定呢?
    viosonlee114
        36
    viosonlee114  
       2021-10-25 09:15:05 +08:00   ❤️ 1
    以前的手机可以插存储卡,所以有内置存储和外置存储的说法,所以很多人口中的手机内存就是指的手机内置存储。还有,有必要在这个点上批判楼主吗?非要杠精上身把愿意分享的人都轰走然后贴吧化?
    junyee
        37
    junyee  
       2021-10-25 09:55:16 +08:00
    微信从早期的 几 MB 到现在 230MB 了.

    随便一个抓 APP,就有可能有 ffmpeg,webview ,合着全国人民都用着旗舰手机呢.
    不仅 dex 巨大, 内嵌资源还多. 多就算了,不常用的资源广告也往里塞,一联网就更新资源.
    让我们看广告也不替我们淘流量费.

    APP 版本恨不得一天一更新似的.
    lvsecoto
        38
    lvsecoto  
       2021-10-25 10:02:10 +08:00
    aab
    Lxcm
        39
    Lxcm  
       2021-10-25 10:13:53 +08:00 via Android
    纠结运存 内存的都是 00 后吗? 80 90 后的都经历过手机的变革,这些名词都是时代的印证,而且楼主还约定了,很好理解。那些纠结症患者是脑子转不过来吗?总是塞住不好。
    john6lq
        40
    john6lq  
       2021-10-25 12:00:42 +08:00
    说白了还是技术的话语权没有产品的大,人家能吹牛,带来流量、拉来投资。
    所以说同一个人的开源项目基本可以秒杀他参与的商业项目。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3521 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 11:23 · PVG 19:23 · LAX 03:23 · JFK 06:23
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.