又是一年双十一,亿万用户都会在这一天打开手机淘宝,高兴地在会场页面不断浏览,面对琳琅满目的商品图片,抢着添加购物车,下单付款。为了让用户 更顺畅更方便地实现这一切,作到“如丝般顺滑”,双十一前夕手机淘宝成立了“521”(我爱你)性能优化项目,在平常优化基础之上进行三个方面的专项优化 攻关,分别是1)H5页面的一秒法则;2)启动时间和页面帧率提高20%;3)Android内存占用下降50%。优化过程当中遇到的困难,思考后找寻的方 案,实施后提取的经验都会在下面详细地介绍给读者。前端
“1S法则”是面向Web侧,H5链路上加载性能和体验方向上的一个指标,具体指:1)“强网”(4G/WIFI)下,1秒彻底完成页面加载,包括首屏资源,可看亦可用;2)3G下1秒完成首包的返回;3)2G下1秒完成建连。ios
在移动网络环境下,http请求和资源加载与有线网络或者PC时代相比有着本质区别,尤为是在2G/3G网络下,每每一个资源请求建连的时间都会 是整个Request-Response流程里面的大头,一些小资源上拖累效应尤为明显。例如一个1k的图片,即便在10k/s 的极慢网速下,理论上0.1秒可下载完毕,但因为创建链接的巨大消耗,这样一个请求会要耗上好几秒。web
仅仅“建连”这一个点,就能说明移动时代的Web侧性能优化和PC时代目标和方式都相去甚远,要求咱们必须从更底层,更细致的去抓,才能取得看起来相对有效的结果。数据库
15年初的性能状况数组
平均LoadTime-WIFI浏览器 |
平均LoadTime - 4G缓存 |
平均LoadTime - 2G安全 |
3.35s性能优化 |
3.84s服务器 |
14.34s |
能够看到优化前,平均时间很难接近1秒。为了实现优化目标,在技术和实施抓手层面,由底层往上,作了四方面事情:
DNS解析想必你们都知道,在传统PC时代DNS Lookup基本在几十ms内。而咱们经过大量的数据采集和真实网络抓包分析(存在DNS解析的请求),DNS的消耗至关可观,2G网络大量5-10s,3G网络平均也要3-5s。
针对这种状况,手淘开发了一套HttpDNS-面向无线端的域名解析服务,与传统走UDP协议的DNS不一样,HttpDNS基于HTTP协议。基于HTTP的域名解析,减小域名解析部分的时间并解决DNS劫持的问题。
手淘HttpDNS服务在启动的时候就会对白名单的域名进行域名解析,返回对应服务的最近IP(各运营商),端口号,协议类型,心跳等信息。
优势
1)防止域名劫持
传统DNS由Local DNS解析域名,不一样运营商的Local DNS有不一样的策略,某些Local DNS可能会劫持特定的域名。采用HttpDNS可以绕过Local DNS,避免被劫持;另外,HttpDNS的解析结果包含HMAC校验,也可以防止解析结果被中间网络设备篡改。
2)更精准的调度
对域名解析而言,尤为是CDN域名,解析获得的IP应该更靠近客户端的地区和运营商,这样才能有更快的网络访问速度。然而,因为运营商策略的多样 性,其推送的Local DNS可能和客户端不在同一个地区,这时获得的解析结果可能不是最优的。HttpDNS可以获得客户端的出口网关IP,从而可以更准确地判断客户端的地区 和运营商,获得更精准的解析结果。
3)更小的解析延迟和波动
在2G/3G这种移动网络下,DNS解析的延迟和波动都比较大。就单次解析请求而言,HttpDNS不会比传统的DNS更快,但经过 HttpDNS客户端SDK的配合,整体而言,可以显著下降解析延迟和波动。HttpDNS客户端SDK有几个特性:预解析、多域名解析、TTL缓存和异 步请求。
4)额外的域名相关信息
传统DNS的解析结果只有ip,HttpDNS的解析结果采用JSON格式,除了ip外,还支持其它域名相关的信息,好比端口、spdy协议等。利用这些额外的信息,APP能够启用或中止某个功能,甚至利用HttpDNS来作灰度发布,经过HttpDNS控制灰度的比例。
出于安全目的,淘宝实现了全站SSL化。自己和H5链路性能优化没有直接的关系,可是从数据层面看,SSL化以后的资源加载耗时都会略优于普通的Http链接。
有读者会有疑惑,SSL化以后每一个域名首次请求会额外增长一个“SSL握手”的时间,DNS建连也会比http的状态下要长,这是不可避免的,可是为何一次完整的RequestRespone 流程耗时会比http状态下短呢?
合理的解释是:SSL化以后,SPDY能够默认开启,SPDY协议下的传输效率和建连复用效益将最大化。SPDY协议下,资源并发请求数将再也不受浏览器webview的并发请求数量限制,并发100+都是可能的。
同时,在保证了域名收敛以后,一样域名下的资源请求将能够彻底复用第一次的DNS建连和SSL握手,因此,仅在第一次消耗的时间彻底能够被 SPDY后续带来的资源传输效率,并发能力,以及链接复用度带来的收益补回来。甚至理论上,越复杂的页面,资源越多的状况,SSL化+SPDY以后在性能 上带来的收益越大。
收益最明显,实现中遇到困难最多的就是离线化或者说资源预加载的方案。预加载方案是为了在 用户访问H5以前,将页面静态资源(HTML/JS/CSS/IMG...)打包预加载到客户端;用户访问H5时,将网络IO拦截并替换为本地文件IO;从而实现H5加载性能的大幅度提高。
手淘实现要比上面的通用示意图复杂:由于Android和iOS安装包已经很大,因此预加载Zip包(如下简称“包”)都是从服务器端下载到客户端;本地 须要记录总体包状态,并在合适的时机与服务器通讯并交换状态信息。在包发布更新的过程当中要注意,本地版本和服务端最新包之间的差量同步,必要的网络判 断,WiFi下才下载等。
面对亿级UV,而且在服务器资源颇有限的状况下搞定这个流程,须要借助CDN来扛住压力,实际上CDN扛住了约98%的流量。
须要注意的是预加载实际上也是一种缓存,更新比H5稍慢一些,主要受几个因素影响:推送到达率(用户是否在线,用户所在网络质量),总控,服务端策略等,因此须要经过推拉结合的触发策略并优化下载包的体积(增量包)来提高到达率。
除了优化到达率,手淘还作了url解CDN Combo后再映射的优化工做,若 URL 是 Combo URL,那么会对 URL 解 Combo,解析出其中包含的资源。而后尝试从本地读取包含的资源,若是全部资源都在本地存在,那么将本地文件内容拼装为一份完整文件并返回;不然 URL 直接走线上,不作任何操做。
提高到达率和解CDN Combo再映射,这两个容器侧对于离线化方案的优化对于本次H5链路上总体性能的提高有着相当重要的意义。
前端组件:请求控制,域名收敛,图片库,前端性能CheckList
严格执行性能方面的CheckList,主要有三个点:
1)图片资源域名所有收敛到gw.alicdn.com;
2)前端图片库根据强弱网和设备分辨率作适配;
3)首屏数据合并请求为一个。
在执行中,性能的检查和校验必定要归入到发布阶段,不然就不是一个合理的流程。性能的工具和校验必定应该是工程化,研发流程里面的一部分,才可以保障性能自动化,低成本,不退化。
经过以上优化方案,H5页面的平均Loadtime在Wifi,4G下均如期进入1秒,3G和2G也有80%多达成1s法则的目标。
不少App都会遇到如下几个常见的性能问题:启动速度慢;界面跳转慢;事件响应慢;滑动和动画卡顿。
手机淘宝也不例外。咱们分为两部分来作,第一部分是启动阶段优化,目的解决启动任务繁多,缺少管控的问题,减小启动和首页响应时间。第二部分是针 对各个界面作优化,提高界面跳转时间和滑动帧率,解决卡顿问题。双十一性能优化目标之一就是将启动时间和页面帧率在原有基础上继续优化提高20%,接下来 就从这两部分的优化过程来作一一介绍。
手机淘宝做为阿里无线的航母,接入的业务Bundle超过100个,启动初始化任务超过30个,这些任务缺乏管控和性能监控。
那么首要任务就是:
全部的初始化任务能够用两个维度来区分:
1)任务必要性:有些任务是应用启动所必需的,好比网络、主容器;有些任务则不是必需的,仅仅实现单个业务功能,甚至是为了业务自身体验和性能而考虑在启动阶段提早执行,其合理性值得推敲。
2)任务独立性:将应用的架构简单分红基础库、中间件、业务三层,这三层中业务层最为庞大,其初始化任务也最多。对于中间件来讲,其初始化可能依赖于另一个中间件。但对于一个独立的业务模块来讲,其初始化任务应该也具备独立性,不存在跟其余业务模块依赖关系。
启动阶段任务管理机制包含了以下几方面的内容
1)任务可并行
既然不少初始化任务是独立的,那么并行执行能够提升启动效率。
2)任务可串行
虽然咱们指望全部初始化任务都相互独立,可是在实际中不可避免会存在相互依赖的初始化任务。为了支持这种状况,咱们设计任务的异步串行机制,这里主要借鉴了前端的Promise思想实现。
3)任务可插拔
面对这么多不一样优先级的初始化任务,任何一个出现异常都会致使应用不能启动,给稳定性带来严重挑战。所以咱们设计了可插拔机制,当某一项初始化任 务出现问题时可以跳过该任务,从而不影响整个应用的启动使用。这里咱们根据初始化任务的必要性作了区分,只有非必要的初始化任务才会应用可插拔的特性,这 也是为了防止出现不执行一个必要的初始化任务致使应用启动使用出现问题。
4)任务可配置
在ios上经过plist指定每一项启动任务, 其中字段optional表示该项是不是必需的,当以前运行出现crash或者异常时,若值为YES则能够不执行该项。
有了任务管理机制,并引入懒加载的理念,能够持续地合理有效管控启动阶段的各项初始化任务,是大型app必不可少的环节。
性能优化前,初始化代码都在主线程中执行,为了启动性能已将部分初始化任务放入后台线程或者异步执行。可是随着手淘业务发展和人员变动,仍是出现 了在主线程中执行很重的初始化任务。为此,在ios实现了一套应用运行时方法耗时检测机制,可以对应用中全部类的方法调用作耗时统计。方便的找到超时的方 法调用以后,就能够有针对性的作出修改,或删除或异步化。这种方法调用耗时检测机制一样适用于APP运行过程当中,从而找到致使应用卡顿的根本缘由,最后作 出对应修改。
分析各个模块的线程数量,检查线程池的合理性。经过去掉没必要要的线程和线程池,再控制线程池的并发数和优先级。进一步经过框架层的线程池来接管业务方的线程使用,以减小线程太多的问题。
从自身业务出发,去除若干初始化阶段没必要要的文件操做,以及将若干非实时性要求的文件操做延后处理。Android上对于频繁读写数据库和 SharedPreference以及文件的模块,经过增长缓存和下降采样率等手段减小对IO的读写。对于SharedPreference进行了专门的 优化,减小单个文件的大小,将毫无联系的存储键值分开到不一样文件中,而且防止将大数据块存储到SharedPreference中,这样既不利于性能也不 利于内存,由于SharedPreference会有额外的一份缓存长期存在。
例如摇一摇功能,测试发现应用场景不频密,但业务使用了高频率的游戏模式,会耗电及占用主线程时间。对该功能作了降级处理,下降检测频率。同理,对于其余非必须使用但又占据较多资源的模块也都作了适当的降级处理。
在安卓手机上咱们把启动分为两类进行检测和优化:冷启动和热启动。冷启动是程序进程不存在的状况下启动,热启动是指用户将程序切换到后台或者不断按Back键退出程序,实际进程还存在的状况下点击图标运行。
以前安卓手淘在按Back键退出时整个首页Activity销毁了,热启动会通过一个比较长的过程。优化后首页在退出的时候并不销毁 Activity,可是会释放图片等主要资源,在下次热启动时就能更快的进入。另外,将手淘欢迎页的界面从其它bundle转移到首页的模块,在进入欢迎 页时就开始初始化首页资源,作到更快展现。
在通过一系列的优化后,启动方面已经有了明显的改善,在进入首页的时候不会卡顿,GC次数也减小了一半以上。
各界面优化咱们也是围绕着提升帧率和加快展示而展开的,手淘的几个主链路界面,都是相对比较复杂的,既使用多图,也使用了动态模板的技术。功能越 复杂,也越容易产生性能问题,因此常遇到布局复杂、过渡绘制多、Activity主要函数耗时、内容展现慢、界面从新布局(Layout)、GC次数多等 问题。
经过开发者选项的GPU过渡绘制选项检查界面的过渡绘制状况。该优化并不复杂,经过去掉层叠布局中多余的背景设置、图片控件有前景内容的时候不显 示背景、界面背景定义到Activity的主题中、减小Drawable的复杂Shape使用等手段就能够基本消除过渡绘制,减小对GPU和CPU的浪 费。
层级越多,测量和布局的时间就会相应增长,建立硬件列表的时间也会相应增长。有时咱们会嵌套不少布局来实现本来只要简单布局就能够实现的功能,有 时还会添加一些测试阶段才会使用的布局。经过删除无用的层级,使用Merge标签或者ViewStub标签来优化整个布局性能。好比一些显示错误界面、加 载提示框界面等,不是必须显示的这些布局可使用ViewStub标签来提高性能。
另外要灵活使用布局,并非层级越多就会性能越差,有时候1层的RelativeLayout会比3层嵌套的LinearLayout实现的性能更糟糕。
除了灵活使用布局,另外咱们还经过提早inflate以及在线程中作一些必要的inflate等来提早初始化布局,减小实际显示时候的耗时。对于一些复杂的布局,咱们还会本身作复用池,减小inflate带来的性能损耗,特别是在列表中。
1)能够经过TraceView工具找出主线程的耗时操做和其余耗时的线程并做优化。另外减小主线程的GC停顿,由于即便并行GC,也会对 heap加锁,若是主线程请求分配内存的话,也会被挂起,因此尽可能避免在主线程分配较多对象和较大的对象,特别是在onDraw等函数中,以减小被挂起的 时间。另外能够经过去掉ListView ,ScrollView等控件的EdgeEffect效果,来减小内存分配和加快控件的建立时间。
2)利用本地缓存,主要界面缓存上次的数据,而且配合增量的更新和删除,能够作到数据和服务端同步,这样能够直接展现本地数据,不用等到网络返回数据。
3)减小没必要要的数据协议字段,减小名字长度等,并做压缩。还能够经过分页加载数据来加快传输解析时间。由于JSON越大,传输和解析时间也会越久,引起的内存对象分配也会越多。
4)注意线程的优先级,对于占用CPU较多时间的函数,也要判断线程的优先级。
经过TraceView工具发现,一些Banner轮播广告和文字动画在移出可视区域后,仍然存在定时刷新,不只耗电也影响帧率。优化措施是在移出可视区域后中止动画轮播。
在ListView滑动,广告动画变化等过程当中,图片和文字有变化,常常会发现整个界面被从新布局,影响了性能。尤为布局复杂时,测量过程很费时 致使明显卡顿。对于大小基本固定的控件和布局例如TextView,ImageView来讲,这是多余的损耗。咱们能够用自定义控件来阻断,重写方法 requestLayout、onSizeChanged,若是大小没有变化就阻断此次请求。对于ViewPager等广告条,能够设置缓存子view的 数量为广告的数量。
中间件的代码被上层业务方调用的比较频繁,容易有较多的高频率函数,也容易产生细节上的问题。除了频繁分配对象外,例如类初始化性能,同步锁的额外开销,接口的调用时间,枚举的使用等等都是不能忽视的问题。
安卓上的GC会引发性能卡顿,必须重点优化。除了第三章会详细介绍对于图片内存引发GC的优化,咱们还作了以下工做:
1)减小对象分配,找出没必要要的对象分配,如可使用非包装类型的时候,使用了包装类型;字符串的+号和扩容;Handler.post(Runnable r)等频繁使用。
2)对象的复用,对于频繁分配的对象须要使用复用池。
3)尽早释放无用对象的引用,特别是大对象和集合对象,经过置为NULL,及时回收。
4)防止泄露,除了最基本的文件、流、数据库、网络访问等都要记得关闭以及unRegister本身注册的一些事件外,还要尽可能少的使用静态变量和单例。
5)控制finalize方法的使用,在高频率函数中使用重写了finalize的类,会加剧GC负担,使得性能上有几倍的差异。
6)合理选择容器,在性能上优先考虑数组,即便咱们如今习惯了使用容器,也要注意频繁使用容器在性能上的隐患点:首先是扩容开销, HashMap扩容时从新Hash的开销较大。其次是内存开销,HashMap须要额外的Map.Entry对象分配 ,须要额外内存,也容易产生更多的内存碎片。SparseArray和ArrayList等在内存方面更有优点。再次是遍历,对于实现了 RandomAccess接口的容器如ArryList的遍历,不该该使用foreach循环。
7)用工具监控和精雕细琢:在页面滑动过程当中,经过Memory Monitor查看内存波动和GC状况,还可经过AlloCation Tracker工具观察内存的分配,发现不少小对象的分配问题。
8)利用Trace For OpenGL工具找出界面上致使硬件加速耗时的点,例如一些圆角图片的处理等。
经过多种工具和手段配合,手淘各个界面性能上有了较大的提升,平均帧率提升了20%,那么内存节省50%又是如何实现的哩,请看下文。
Android上应用出现卡顿的核心缘由之一是主线程完成绘制的周期过长引发丢帧。而影响主线程完成绘制时间的主要有两方面,一方面是主线程处于运行状态 时须要作的任务太多但CPU资源有限,另一方面是GC时Suspend时直接挂起了全部线程包括主线程。GC对整体性能的影响在4.x的系统上尤其突 出,一部分是单次GC pause总时长,一部分是用户操做过程当中GC发生的次数。而决定这两部分的因素就是Dalvik内存分配。那么在手淘这样的大型应用中究竟是谁占用了 内存大头 呢?
基于双11前的手淘Android版本,咱们在魅蓝note1(4.4 OS)上滑动完首页后,dump出其Dalvik Heap,总体内存占用的分布状况以下图。能够看出,byte数组(a)占用空间最大,绝大多数是用来存放Bitmap的 像素数据(Pixel Data) 。另外(c)与(d)一块儿占用了18.4%, byte数组加上Bitmap、BitmapDrawable总共占用了64.4%,成为内存占用的主体。这也从侧面说明了手淘是以图片为浏览主体内容的 大型应用。而每每图片须要较大的内存块,在分配时引发GC的可能性也每每最大。那咱们能不能将图片这部分须要的内存移走而不在Dalvik Heap分配呢?若是能,那么不单GC会明显减小,同时Dalvik Heap总大小也会降低50%左右,对总体性能会有显著的提高。
(点击放大图像)
Ashmem即匿名共享内存,使用的核心过程是建立一个/dev/ashmem设备文件,控制反转设置文件的名字和大小,最终把设备符交给 mmap就获得了共享内存。在Android系统中Binder进程间通讯的实现就是依赖Ashmem完成不一样进程间的内存共享。但此处并不利用其共享特 性,而是使用它在Native Heap完成内存分配。图片空间如何才能使用Ashmem,答案在Facebook推出的Fresco中已有说起,那就是解码时的purgeable标 记,这样在系统底层解码位图时会走Ashmem空间分配,而非Dalvik Heap空间。这样就解决了像素数据存放由Dalvik到Native的问题了吗?
1 |
BitmapFactory.Options options = new BitmapFactory.Options(); |
2 |
/* |
3 |
* inPurgeable can help avoid big Dalvik heap allocations (from API level 11 onward) |
4 |
*/ |
5 |
options.inPurgeable = true ; |
6 |
Bitmap bitmap = BitmapFactory.decodeByteArray(inputByteArray, 0 , inputLength, options); |
事实并不是那么简单,最后实际解出来Bitmap没有像素数据(没有到Ashmem分配任何空间),根本没有去完成jpeg或者png解码。此时的 Bitmap是个空包弹!它所作的只是把输入的解码前数据拷贝到了native内存,若是把这个Bitmap交给ImageView渲染就糟了,在 View.draw()时Bitmap会在主线程进行图片解码。
并且不要天真的觉得Bitmap解码一次以后再屡次使用都不会引发二次解码,在系统内存紧张时底层可能回收Ashmem里这部份内存。回收后该Bitmap再次渲染时又将在主线程完成一次解码。若是就这样直接使用该机制,性能上无疑雪上加霜。
那么怎样才能避免这个隐形炸弹呢?还好SDK预留了一个C层方法AndroidBitmap_lockPixels。而lockPixels底层 完成的工做大体以下图所示。第一步是prepareBitmap完成真正的数据解码,在工做线程调用AndroidBitmap_lockPixels避 免了在主线程进行数据解码;第二步是完成对分配出来的Ashmem空间的锁定,这样即便在系统内存紧张时,也不会回收Bitmap像素数据,避免屡次解 码。
(点击放大图像)
貌似解决了Bitmap渲染的全部问题,但在手淘中则否则。为了兼容低版本系统以及提高webp解码性能,咱们使用了本身的解码库libwebp.so,怎样把它解码出来的数据也存放到Ashmem呢?
若是自有解码库libwebp.so要解码到Ashmem,经过SkBitmap、ashmem_create_region实现一套相似的机制 是不太现实的。一方面Skia库的源码编译兼容会存在很大问题,另外一方面不少系统层面的核心接口并无对外。因此实现这点的关键仍是要借助系统已经提供的 purgeable到Ashmem的机制,借鸡生蛋,稳定性和成本上都能获得保证:
完成解码。
上面谈到的内存迁移都是针对Decoded像素数据的,而Encoded图像数据在解码时会在Dalvik Heap保存一份,解码完成后再释放;Ashmem方式解码时在底层又会拷贝一份到Native内存,这份数据直到整个Bitmap回收时才释放。那可否 直接将网络下载的Encoded数据存放到Native内存,省去Dalvik Heap上的开销以及解码时的内存拷贝呢?的确能够,将网络流数据直接转移到 MemoryFile可实现,但遗憾的是真机测试中发现,小米及其余国产“神机”(自改ROM),多线程使用MemoryFile获取fd到BitmapFactory解码, 会出现系统死机,怀疑是在并发状况下系统代码级别的死锁形成。手机淘宝放弃了这种方案,改用ByteArrayPool复用池技术来减小Dalvik Heap针对Encoded Image的内存分配,效果也不错。若是应用能接受单线程解码,仍是MemoryFile方案更具优点。
上文提到Bitmap像素数据存放到Ashmem,有读者可能担忧数据回收问题,其实仍是由GC来触发Ashmem内存的回收。在Dalvik层若是一个Bitmap已经不被任何地方引用,那么在下一次GC时该Bitmap就会从Ashmem中回收,大体流程示意以下图。
(点击放大图像)
咱们再次在魅蓝note1中dump出首页滑动后的内存,以下图能够看出,原来byte数组(k)大量占用已经不存在了,Bitmap(c)与BitmapDrawable(已不在前14名当中)的占用也急骤降低。应用的整体内存降低近60%。
(点击放大图像)
在双11版本上,针对一些热门机型在搜索结果页不断滚动使用,进行了不一样版本的内存占用对比分析,以下图。能够看出,除华为3c和vivo这类系统内存偏小使用上一直受到控制、内存较为紧张的外,大部分机型内存的降低幅度都达到45%以上。
(点击放大图像)
内存降低不是最终目的,最终要将GC对性能的影响降到最低。仍然以魅蓝note1打开首页后滑动到底的内存堆积图来作对比。能够看到旧版本内存占 用上升趋势至关明显,一路带有各式“毛刺”直奔70MB,每造成一个毛刺就意味一次GC。而双11版本中,内存只在初期有上升,然后很快降低到21MB左 右,后期也显得平滑得多,没有那么多的“毛刺”,就意味着GC发生的次数在明显减小。
(点击放大图像)
旧版本
(点击放大图像)
双11版本
同时使用一些热门机型,针对双十一版本在首页不断滑动,进行先后版本的GC_FOR_ALLOC次数对比。热门机型GC次数降低了4~8倍,效果很是明显。
经过上文描述的各个优化方案,手机淘宝于双十一前在大部分机型上达到了521目标-Android手机内存节省50%,启动时间和页面帧率提高20%,H5页面实现1s法则。
从持续不断的优化中,咱们也获得了一套优化的经验闭环,由观察问题现象到分析缘由,创建监控,定下量化目标,执行优化方案,验证结果数据再回到观察新问题。每一次闭环只能解决部分问题,只有不断抓住细微的优化点“啃”下去,才能获得螺旋上升的良好结果。
固然,随着手机机型的日益碎片化,程序功能的复杂化多样化,性能调优是没有止境的,在部分低端机和低内存手机上手淘性能问题依然不容乐观。欲穷千里目,还需更上一层楼,接下来咱们还会努力经过更多更细致的优化方案来达到“如丝般顺滑”。