Android FPS流畅度测试

前言:

流畅度,是页面在滑动、渲染等过程中的体验。Android系统要求每一帧都要在 16ms 内绘制完成,平滑的完成一帧意味着任何特殊的帧需要执行所有的渲染代码(包括 framework 发送给 GPU 和 CPU 绘制到缓冲区的命令)都要在 16ms 内完成,保持流畅的体验。如果没有在期间完成渲染秒就会发生掉帧。掉帧是用户体验中一个非常核心的问题。丢弃了当前帧,并且之后不能够延续之前的帧率,这种不连续的间隔会容易会引起用户的注意,也就是我们常说的卡顿、不流畅。
导致掉帧的原因会有很多,这里只列出以下几点:
•花了非常多时间重新绘制界面中的大部分东西,这样非常浪费CPU周期;
•过度绘制严重,在绘制用户看不到的对象上花费了太多的时间
•有一大堆动画重复了一遍又一遍,消耗 CPU 、 GPU 资源
•频繁的触发垃圾回收。

追踪性能方法

Hierarchy View
Overdraw
Rendering (渲染性能)

Hierarchy View

Hierarchy View 在Android SDK里自带,常用来查看界面的视图结构是否过于复杂,用于了解哪些视图过度绘制。还给出关于测量、布局、绘制的性能。


分析示例:如果red dot在一个叶子节点或者一个只有几个子节点的ViewGroup上,可能会存在一些问题。APP整体表现可能不坏,但是你要意识到那个节点为什么是红色的!可以使用Systrace或者 TraceView做进一步的分析。
如果ViewGroup的Measure 是红色的,要分析一下它的子节点。
如果View存在黄色甚至红色dot,可能在手机上的运行效率并不慢,可能是由于view的数目过多造成的。可以使用Systrace或者 TraceView做进一步的分析。
如果一个层级结构的根节点measure dot是红色的,layout dot是红色的,draw dot是黄色的,这是一种非常典型的情况,因为它是所有节点的根节点。
如果一个有20+ view的层级结构中,一个叶子节点draw dot是红色的,那肯定是问题,检查一下它的onDraw方法。
官方介绍
https://developer.android.com/studio/profile/hierarchy-viewer-walkthru.html

Overdraw

Overdraw表示某些组件在屏幕上的一个像素点的绘制次数超过 1 次。可以通过开发者选项里打开 “ 调试 GPU 过度绘制获得相关信息。


用颜色来区别过度绘制程度:


①没颜色:没有过度绘制,显示应用本来的颜色
②蓝色:1倍过度绘制
③绿色:2倍过度绘制
④浅红色:3倍过度绘制
⑤深红色:4倍过度绘制及以上

导致过度绘制的原因很多,例如:

①视图相互重叠的问题
|-- 不必要的背景重叠,如上图官网介绍
②不合理的xml布局对绘制的影响
|-- 当布局文件的节点树的深度越深,XML 中的标签和属性设置越多,对界面的显示有巨大影响。一个界面要显示出来,第一步会进行解析布局,在 requestLayout 之后还要进行一系列的 measure 、 layout 、 draw 操作,若布局文件嵌套过深、拥有的标签属性过于臃肿,每一步的执行时间都会受到影响,而界面的显示是进行完这些操作后才会显示的,所以每一步操作的时间增长,最终显示的时间就会越长。

官网给出的合理的页面过度绘制情况

渲染性能

渲染性能往往是掉帧的罪魁祸首,通过在 Android 设备的设置 APP 的开发者选项里打开 “ GPU 呈现模式分析 ” 选项,可以帮我们有效的分析渲染性能。

追踪渲染性能,通过向 dumpsys 传递“gfxinfo”命令来跟踪相关的信息

1、帧的聚合信息
这些高级统计信息可以较高水平地传达应用的呈现性能及其在多个帧之间的稳定性

简单的时间帧信息:
|-- 通过gfxino获得,Draw、Process、Excute

$ adb shell dumpsys gfxinfo packageName

1、draw代表的是这一帧绘制 Display List 的时间。通俗来说,就是记录了需要花费多长时间在屏幕上更新视图。用代码语言来说,就是执行视图的 onDraw 方法,创建或更新每一个视图的 Display List 的时间。
2、process代表的是这一帧 OpenGL 渲染 Display List 所需要的时间。通俗来说,就是记录了执行视图绘制的耗时。用代码语言来说,就是 Android 用 OpenGL ES 的 API 接口进行 2D 渲染 Display List 的时间。
3、excute代表的是这一帧 CPU 等待 GPU 处理的时间。通俗来说,就是 CPU 等待 GPU 发出接到命令的回复的等待时间。用代码语言来说,就是这是一个阻塞调用。

精确的帧时间信息

通过 gfxinfo framestats获得
|-- 获取精确时间帧数据:

$ adb shell dumpsys gfxinfo packageName framestats
具体的列出在绘制过程中每一步的时间,并且所有时间戳均以纳秒为单位
数据分析

已获取到的第二种数据为例,先解释每列数据代表的含义
●标志

  •窗口布局发生变化(例如,应用的第一帧或在旋转后)
  •此外,如果帧的某些值包含无意义的时间戳,则也可能跳过该帧。 例如,如果帧的运行速度超过 60fps,或者如果屏幕上的所有内容最终都准确无误,则可能跳过该帧,这不一定表示应用中存在问题。
  •“标志”列带有“0”的行可以通过从 FRAME_COMPLETED 列中减去 INTENDED_VSYNC 列计算得出总帧时间。
  •该列为非零值的行将被忽略,因为其对应的帧已被确定为偏离正常性能,其布局和绘制时间预计超过 16 毫秒。 可能出现这种情况有如下几个原因:

●INTENDED_VSYNC

  •帧的预期起点。如果此值不同于 VSYNC,则表示 UI 线程中发生的工作使其无法及时响应垂直同步信号。

●VSYNC

  •所有垂直同步侦听器中使用的时间值和帧绘图(Choreographer 帧回调、动画、View.getDrawingTime() 等等)
  •如需进一步了解 VSYNC 及其对应用产生的影响,请观看[了解 VSYNC](https://www.youtube.com/watch?v=1iaHxmfZGGc&list=PLOU2XLYxmsIKEOXh5TwZEv89aofHzNCiu&index=23) 视频。

●OLDEST_INPUT_EVENT

  •输入队列中最早输入事件的时间戳或 Long.MAX_VALUE(如果帧没有输入事件)。
  •此值主要用于平台工作,对应用开发者的作用有限。

●NEWEST_INPUT_EVENT

  •输入队列中最新输入事件的时间戳或 0(如果帧没有输入事件)。
  •此值主要用于平台工作,对应用开发者的作用有限。
  •但是,可以通过查看 (FRAME_COMPLETED - NEWEST_INPUT_EVENT) 大致了解应用增加的延迟时间。

●HANDLE_INPUT_START

  •将输入事件分派给应用的时间戳。
  •通过观察此时间戳与 ANIMATION_START 之间的时差,可以测量应用处理输入事件所花的时间。
  •如果这个数字较高(> 2 毫秒),则表明应用处理 View.onTouchEvent() 等输入事件所花的时间太长,这意味着此工作需要进行优化或转交给其他线程。 请注意,有些情况下(例如,启动新Activity或类似活动的点击事件),这个数字较大是预料之中并且可以接受的。

●ANIMATION_START

  •在 Choreographer 中注册的动画运行的时间戳。
  •通过观察此时间戳与 PERFORM_TRANVERSALS_START 之间的时差,可以确定评估正在运行的所有动画(ObjectAnimator、ViewPropertyAnimator 和通用转换)所需的时间。
  •如果这个数字较高(> 2 毫秒),请检查您的应用是否编写了任何自定义动画,或检查 ObjectAnimator 在对哪些字段设置动画并确保它们适用于动画。
  •如需了解有关 Choreographer 的更多信息,请观看[利弊](https://developers.google.com/events/io/sessions/325418001)视频。

●PERFORM_TRAVERSALS_START

  •如果您从此值中扣除 DRAW_START,则可推断出完成布局和测量阶段所需的时间(请注意,在滚动或动画期间,您会希望此时间接近于零)。
  •如需了解有关呈现管道的测量和布局阶段的更多信息,请观看[失效、布局和性能](https://www.youtube.com/watch?v=we6poP0kw6E&list=PLOU2XLYxmsIKEOXh5TwZEv89aofHzNCiu&index=27)视频。

●DRAW_START

  •performTraversals 绘制阶段的开始时间。这是记录任何失效视图的显示列表的起点。
  •此时间与 SYNC_START 之间的时差就是对树中的所有失效视图调用 View.draw() 所需的时间。
  •如需了解有关绘图模型的详细信息,请参阅[硬件加速](https://developer.android.com/guide/topics/graphics/hardware-accel.html#hardware-model)或[失效、布局和性能](https://www.youtube.com/watch?v=we6poP0kw6E&list=PLOU2XLYxmsIKEOXh5TwZEv89aofHzNCiu&index=27)视频。

●SYNC_START

  •绘制同步阶段的开始时间。
  •如果此时间与 ISSUE_DRAW_COMMANDS_START 之间的时差较大(约 > 0.4 毫秒),则通常表示绘制了大量必须上传到 GPU 的新位图。
  •如需进一步了解同步阶段,请观看 [GPU 呈现模式分析](https://www.youtube.com/watch?v=VzYkVL1n4M8&index=24&list=PLOU2XLYxmsIKEOXh5TwZEv89aofHzNCiu)视频。

●ISSUE_DRAW_COMMANDS_START

  •硬件呈现器开始向 GPU 发出绘图命令的时间。
  •此时间与 FRAME_COMPLETED 之间的时差让您可以大致了解应用生成的 GPU 工作量。 绘制过度或呈现效果不佳等问题都会在此显示出来。

●SWAP_BUFFERS

  •调用 eglSwapBuffers 的时间,此调用不属于平台工作,相对乏味。

●FRAME_COMPLETED

  •全部完成!处理此帧所花的总时间可以通过执行 FRAME_COMPLETED - INTENDED_VSYNC 计算得出。

由于数据块是 CSV 格式的输出,因此将其粘贴到所选的电子表格工具或使用脚本进行收集和解析非常简单。我们可以导入Excel进行数据处理。

利用公式,进一步进行计算结果,并转换单由纳秒 -> 毫秒

最后利用Excel生成简单直观的图表,统计平均绘制时间,以及绘制时间的占比。如下图表示大部分帧在16ms一下,表示页面流畅度“还可以”
具体哪一帧高了,可到对应的数据列表中查看,检查是哪一步导致的。gfxinfo只提供这种“浏览方式”,如果还需要进一步详细的跟踪,还是需要System Trace的分析。

控制统计信息收集

时间

Framestats 和简单的帧计时均可在极短的时间内(相当于约呈现 2 秒)收集数据。 因此需要要精确控制此时间范围(例如,将数据限制于特定动画),这里可以通过reset命令,重置所有计数器并汇总收集的统计信息
|-- 清空最近的信息

$ adb shell dumpsys gfxinfo packageName reset
方法:

通过命令行,输出固定的滑动坐标,点到点,防止人为操作造成误差。

$ adb shell input swipe 700 2000 700 200

这里只是看滑动过程中的流畅性,然后收集,用pyhton写了个渣脚本临时处理excel表格。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author:John Hao
# 测试流畅度平均绘制时长的小脚本
# 功能:滑动当前页面两次,间隔1秒,收集gfxinfo并制表
# titlename:表示要测试的模块和最后文件保存的名字
# |-- 收集gfxinfo framestats详细帧数据信息
# |-- 用第三方模块openpyxl处理收集到的excel数据
# |-- 输出2秒内的120帧每一帧的绘制时间
# |-- 输出平均绘制时间

import time
import sys
import os
import openpyxl
from openpyxl import Workbook
from openpyxl.styles.colors import Color
from openpyxl.chart import LineChart, Reference, Series

titlelist = [' UI 线程中发生的工作使其无法及时响应垂直同步信号','应用处理输入事件所花的时间( >2 毫秒表示处理输入事件时间长)',
                '正在运行的所有动画(ObjectAnimator、ViewPropertyAnimator 和通用转换)所需的时间',
                '完成布局和测量阶段所需的时间','对树中的所有视图调用 View.draw() 所需的时间',
                '约 > 0.4 毫秒表示绘制了大量必须上传到 GPU 的新位图','GPU 工作量','处理此帧所花的总时间','达标线']
subtitlelist = ['IntendedVsync','HandleInputStart','AnimationStart','PerformTraversalsStart',
                'DrawStart','SyncStart','IssueDrawCommandsStart','FrameCompleted','Avg']

# 要测试测模块名,最后文件会以该名称命名
titlename = "Feed"
print "Starting"

for j in range(1,6):
    time.sleep(1)
    print "开始执行第" + str(j) + "遍"

    wb = Workbook()
    ws = wb.active
    ws.title = "data"
    valueofwidth = 16
    ws.column_dimensions["A"].width = valueofwidth
    ws.column_dimensions["B"].width = valueofwidth
    ws.column_dimensions["C"].width = valueofwidth
    ws.column_dimensions["D"].width = valueofwidth
    ws.column_dimensions["E"].width = valueofwidth
    ws.column_dimensions["F"].width = valueofwidth
    ws.column_dimensions["G"].width = valueofwidth
    ws.column_dimensions["H"].width = valueofwidth
    ws.column_dimensions["I"].width = valueofwidth
    ws.column_dimensions["J"].width = valueofwidth
    ws.column_dimensions["K"].width = valueofwidth
    ws.column_dimensions["L"].width = valueofwidth
    ws.column_dimensions["M"].width = valueofwidth
    ws.column_dimensions["N"].width = valueofwidth

    # 重置所有计数器并汇总收集的统计信息
    os.popen("adb shell dumpsys gfxinfo PackageName reset")
    print "清理帧信息回到初始状态"

    # 模拟滑动页面操作
    for i in range (1,3):
        print "执行滑动页面操作" + str(i) + "次"
        os.system("adb shell input swipe 700 2000 700 200") 
        time.sleep(1)

    # 过滤、筛选精确的帧时间信息
    command = "adb shell dumpsys gfxinfo PackageName framestats | grep -A 120 'Flags'"
    r = os.popen(command)
    info = r.readlines()

    # 数据处理中
    print "缓存数据中......"
    for line in info:  #按行遍历
        # line = line.strip('\r\n')
        eachline = line.split(',')
        # 将行写入Excel表格
        ws.append(eachline)
        # print line

    # 新建sheet用来统计数据
    resultsheet = wb.create_sheet("result",0)
    resultsheet.column_dimensions["A"].width = valueofwidth
    resultsheet.column_dimensions["B"].width = valueofwidth
    resultsheet.column_dimensions["C"].width = valueofwidth
    resultsheet.column_dimensions["D"].width = valueofwidth
    resultsheet.column_dimensions["E"].width = valueofwidth
    resultsheet.column_dimensions["F"].width = valueofwidth
    resultsheet.column_dimensions["G"].width = valueofwidth
    resultsheet.column_dimensions["H"].width = valueofwidth
    resultsheet.column_dimensions["I"].width = valueofwidth

    # 为结果页添加title说明
    resultsheet.append(titlelist)
    resultsheet.append(subtitlelist)
    # resultsheet.RowDimension(height = 5)

    # 填入公式,cell值由纳秒转换为毫秒
    for i in range(3,123):
        resultsheet.cell(row = i, column = 1, value = "=data!C" + str(i-1) + "-data!B"+ str(i-1))

    for i in range(3,123):
        value = "=(data!G" + str(i-1) + "-data!F"+ str(i-1)
        resultsheet.cell(row = i, column = 2, value = value + ")/1000000")

    for i in range(3,123):
        value = "=(data!H" + str(i-1) + "-data!G"+ str(i-1)
        resultsheet.cell(row = i, column = 3, value = value + ")/1000000")

    for i in range(3,123):
        value = "=(data!I" + str(i-1) + "-data!G"+ str(i-1)
        resultsheet.cell(row = i, column = 4, value = value + ")/1000000")

    for i in range(3,123):
        value = "=(data!K" + str(i-1) + "-data!I"+ str(i-1)
        resultsheet.cell(row = i, column = 5, value = value + ")/1000000")

    for i in range(3,123):
        value = "=(data!L" + str(i-1) + "-data!K"+ str(i-1)
        resultsheet.cell(row = i, column = 6, value = value + ")/1000000")

    for i in range(3,123):
        value = "=(data!L" + str(i-1) + "-data!K"+ str(i-1)
        resultsheet.cell(row = i, column = 7, value = value + ")/1000000")

    for i in range(3,123):
        value = "=(data!N" + str(i-1) + "-data!B"+ str(i-1)
        resultsheet.cell(row = i, column = 8, value = value + ")/1000000")

    # 插入平均值16ms的列
    for i in range(3,123):
        resultsheet.cell(row = i, column = 9, value = 16)

    # 插入平均Frame值
    resultsheet['J1'] = "平均值ms"
    resultsheet['J2'] = "=AVERAGEA(H3:H122)"

    # 画图准备
    chart = LineChart()
    chart.title = titlename + str(j)
    # chart.style = 5       #style都很丑,还不如默认的
    chart.y_axis.title = 'ms'
    chart.x_axis.title = 'Frame'
    chart.width = 30
    chart.height = 15

    # data选取范围
    data = Reference(resultsheet, min_col=8, min_row=2, max_col=9, max_row=122)
    chart.add_data(data, titles_from_data=True)

    # 创建图表,在B3位置插入
    resultsheet.add_chart(chart,"B3")

    #记录时间戳作为文件名
    # filename = time.strftime('%Y%m%d_%H%M%S',time.localtime(time.time())) + ".xlsx"
    # wb.save(filename)

    #以执行名称 titlename作为文件名
    filename2 = titlename + str(j) + ".xlsx"
    wb.save(filename2)

    # 数据完毕
    print "缓存处理完毕,保存数据到本地" + str(filename2)
    time.sleep(3)

Trace View的分析

重要的参数:
• Cpu Time/call
• Calls + Recur
TraceView各项信息及意义:
Name:方法的详细信息,包括包名和参数信息
Incl Cpu Time Cpu:执行该方法该方法及其子方法所花费的时间
Incl Cpu Time %:Cpu执行该方法该方法及其子方法所花费占Cpu总执行时间的百分比
Excl Cpu Time Cpu:执行该方法所话费的时间
Excl Cpu Time %:Cpu执行该方法所话费的时间占Cpu总时间的百分比
Incl Real Time:该方法及其子方法执行所话费的实际时间,从执行该方法到结束一共花了多少时间
Incl Real Time %:上述时间占总的运行时间的百分比
Excl Real Time %:该方法自身的实际允许时间
Excl Real Time:上述时间占总的允许时间的百分比
Calls+Recur:调用次数+递归次数,只在方法中显示,在子展开后的父类和子类方法这一栏被下面的数据代替
Calls/Total:调用次数和总次数的占比
Cpu Time/Call Cpu:执行时间和调用次数的百分比,代表该函数消耗cpu的平均时间
Real Time/Call :实际时间于调用次数的百分比,该表该函数平均执行时间

其他方法:

• Animator duration scale

通过在 Android 设备的设置 APP 的开发者选项里打开 “ 窗口动画缩放 ” / “ 过渡动画缩放 ” / “ 动画程序时长缩放 ”,来加速或减慢动画的时间,以查看加速或减慢状态下的动画是否会有问题。

• StrictMode

通过在 Android 设备的设置 APP 的开发者选项里启动 “ 严格模式 ” ,来查看应用哪些操作在主线程上执行时间过长。当一些操作违背了严格模式时屏幕的四周边界会闪烁红色,同时输出 StrictMode 的相关信息到 LOGCAT 日志中

Reference:
一些官方参考
ØHierarchy View 官网介绍
https://developer.android.com/studio/profile/hierarchy-viewer-walkthru.html
ØOverdraw 官网介绍
https://developer.android.com/studio/profile/dev-options-overdraw.html
ØProfiling GPU Rendering 官网介绍
https://developer.android.com/studio/profile/dev-options-rendering.html
ØTrace View 官网介绍
https://developer.android.com/studio/profile/traceview-walkthru.html

一些测试相关参考
Ø测试显示性能
http://www.jcodecraeer.com/a/anzhuokaifa/developer/2015/0920/3483.html
ØAndroid 编程下的 TraceView 简介及其案例实战
http://www.cnblogs.com/sunzn/p/3192231.html
ØAndroid界面性能调优手册
https://testerhome.com/topics/4304
Ø正确使用 Android 性能分析工具——TraceView
https://testerhome.com/topics/5049
Ø【Bugly干货分享】那些年我们用过的显示性能指标
http://blog.csdn.net/tencent_bugly/article/details/51354517

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 界面是 Android 应用中直接影响用户体验最关键的部分。如果代码实现得不好,界面容易发生卡顿且导致应用占用大量...
    Ten_Minutes阅读 670评论 0 9
  • 失眠已经成为了我一直以来的痛病,让人不能安眠,每当夜幕降临,是想早早入睡的,却也不得心意,各种烦心琐事在这个...
    旧事酒浓意味深长阅读 390评论 0 1
  • 那星辰荣华为谁筑成屋脊 纵更深露重只得园外栖身 遥遥天路无尽 近观无计生存 是福是祸 又何必去问 渲墨纸上轻叹一世...
    暮岚雪阅读 240评论 2 4