Android性能优化笔记

布局优化

0, UI渲染机制

人眼所看到的流畅画面,需要的帧数在40帧每秒到60帧每秒之间,
最佳的ftp在60左右,在Android中,系统通过VSYNC信号出发对UI的渲染、重绘,其间隔时间是16ms,这就意味着程序的大多数操作都必须在16ms内完成,这个16ms其实就是1000ms中显示60帧画面的单位时间。即1000、60,如果系统每次渲染都保持在16ms之内,那么我们看到的UI将十分的流畅,但这也是需要将所有的逻辑都保证在16ms里,如果16ms不能完成绘制,那么就会造成丢帧的现象,即当前该重绘的帧被未完成的逻辑阻塞,用户便感知到卡顿;

1,include标签
同样的布局抽出用include
解析xml布局时,检测到include就解析然后再解析被include进来的布局的root view元素

2,merge标签

这里写图片描述

如果如上图的布局会多一层嵌套,此时用merge标签来替换FrameLayout2可以减少嵌套

注意:默认一个activity会有一层FrameLayout(base_content)在外层,所以如果include的布局中是FrameLayout则应在activity最外层用merge标签,这样减少了层级

3,ViewStub视图
当布局需要延迟加载,点击可见的时候,采用
viewstub只占位不做具体事,宽高都为0,用户手动设置viewstub的inflate或者setVisibility为Visible时,ViewStub就从父控件移除并加载目标布局,完成视图的动态替换

4,减少视图树层级
能用RelativeLayout一个布局搞定的就不要用多层嵌套;
总结:

1,尽量多使用RelativeLayout,减少层级
2,在ListView等列表组件中避免使用Linearlayout的layout_weight属性,因为Linearlayout采用了weight属性后子view要绘制2次
3,将可复用的组件抽取用include标签
4,使用ViewStub标签加载一些不常用的布局
5,使用merge标签减少布局的嵌套布局

内存优化

0,什么是内存

由于Android的沙箱机制,每个应用所分配的内存大小是有限度的,内存太低就会触发LMK-Low Memory Killer机制。那么到底什么是内存呢?通常情况下我们所说的内存是指手机的RAM,它包括以下几个部分

寄存器(Registers)
速度最快的存储场所,因为寄存器位于处理器内部.在程序中无法控制

栈(Stack)
存放基本类型的数据和对象的引用.但对像本身不存放在栈中,而是存放在堆中
堆内存(Heap)

堆内存用来存放由new创建的对象和数组.在堆中分配的内存,由java虚拟机的自动垃圾回收(GC)管理
静态存储区域(static Field)

静态存储区域就是指在固定的位置存放应用程,子运行时一直存在的数据,java在内存中专门划分了一个静态存储区域来管理一些特殊的 数据变量如静态的数据变量
常量池(Constant Pool)

JVM虚拟机必须为每个被装载的类型维护一个常量池,常量池就是该类型所用到常量的一个有序集合,包括直接常量(基本类型,String)和对其他类型. 字段和方法的符号引用
在这些概念中最容易搞错的就是堆和栈的区分。

当定义一个变量,Java虚拟机就会在栈中为该变量分配内存空间 这部分内存空间会马上被用作新的空间进行分配,如果使用new的方式创建一个变量, 那么就会在堆中为这个对象分配内存空间,即使该时象的作用域结束, 这部分内存也不会立即被回收.而是等待系统Gc进行回收,堆的大小随着手机的不断发展而不断变大.

在过程中.可以使用如下所示的代码来获得堆的大小,所谓的
内存分析,正是分析Heap中的内存状态。

ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
int heapSize = manager.getLargeMemoryClass();

1,珍惜Services资源
限制Service最好的方法是使用IntentService,它会在处理完后扔给它的intent任务之后尽快结束自己
启动一个service,系统会为了保留这个Service而一直保留Service所在的进程。持续保留Service会导致APP因RAM的限制而性能糟糕。

2,当UI隐藏时释放内存
当用户切换到其他应用而你的APP不可见,应该释放UI上所占用的所有资源,这个时候可以增加系统缓存进程的能力。
实现Activity里面的onTrimMemory()回调方法,使用该方法监听到TRIM_MEMORY_UI_HIDDEN级别的回调
扩展:http://www.cnblogs.com/xiajf/p/3993599.html

public void onTrimMemory(int level) {  
    super.onTrimMemory(level);  
    switch (level) {  
    case TRIM_MEMORY_UI_HIDDEN:  
        // 进行资源释放操作  
        break;  
    }  
} 

3,当内存紧张时释放部分内存
扩展:http://blog.csdn.net/guolin_blog/article/details/42238627
根据onTrimMemory方法中的内存级别来决定释放那些资源:
API14才支持的onTrimMemory,对于低版本,可以用onLowMemory来兼容,
onLowMemory相当于TRIM_MEMORY_COMPLETE
Note:当系统开始清除LRU缓存中的进程时,尽管它首先按照LRU的顺序来操作,但是,它同样会考虑进程的内存使用量。因此,消耗越少的进程越容易被留下来。

4,检查你应该使用多少内存
通过调用getMemoryClass()来获取你的App的可用heap大小。特殊情况下,通过在manifest的application标签下添加==largeHeap=true==的属性来申明一个更大的heap空间,然后通过getLargeMemoryClass()来获取到一个更大的heap size。当然这种主要用于图片视频编辑类APP(需要消耗大量RAM)

5,避免Bitmap的浪费
加载Bitmap时,通过 BitmapFactory.Options采样压缩 ,只需要保留适配当前屏幕分辨率的数据,原图高于分辨率就需要做缩小的动作,因为增加Bitmap的尺寸会对内存呈现出2次方的增加;
扩展:http://blog.csdn.net/yudajun/article/details/9323941

6,使用优化的数据容器
用SparseArray替换HashMap,通常Hashmap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外SparseArray更加高效在于其避免了对key和value的autobox自动封装,也避免了装箱后的解箱。其他替换如:SparseBooleanArray,LongSparseArray Framework里面优化过的容器类。

7,注意内存开销
Enums的内存消耗通常是static constants的2倍,所以要避免在Android上用enums。
在Java中的每一个类都会使用大概500 bytes,每一个类的实例产生的花销是12-16 bytes,往HashMap添加一个entry需要一个额外占用的32 bytes的entry对象。

抽象是好的编程实践,但是代码会被map到内存中,所以如果抽象没有显著提升效率,就避免使用

使用Protocol Buffers 代替 JSON
扩展:
http://www.oschina.net/translate/choose-protocol-buffers?cmp
http://cxshun.iteye.com/blog/1974498
JSON数据有一个缺点:冗余太大--每条数据都包含前缀(如下面的Name和Gender),据统计,JSON数据至少有20%是无效的

[{"Name": "Jane", "Gender": 0}, {"Name": "Waith", "Gender": 1}]

与JSON不同,Protocol Buffers用二进制编码数据,而且数据的格式是事先通过一个后缀名为.proto的文件指定的,
如:

message Person {
  required string Name = 2;
  optional int32 Gender = 3;
}

8,避免使用依赖注入和外部库
针对Guice或者RoboGuice类似的注入框架,这些框架会扫描代码,消耗RAM来map代码;
外部库过多影响效率,能用自己实现的就自己实现,而不是导入一个大而全的方案;

9,使用ProGuard来删除不需要的代码
ProGuard能移除不需要的代码,重命名类,等方法,对代码进行压缩,优化和混淆。

10,对最终的APK使用zipalign
注:GooglePlay不接受没有经过zipalign的APK

11,使用多进程
如果使用不当会增加内存,当APP需要在后台运行与前天一样的大量的任务才可以用多进程
==典型的例子:==
创建一个可长时间后台播放的Music Player,如果整个APP运行在一个进程中,当后台播放
时候,前台的UI资源无法得到释放,这样类似的APP可以切分成2个进程:一个用于UI操作,一个用于后台Service;
可以通过manifest文件中申明android:process属性来实现某个组件运行在另外一个进程的操作:

<servcice  android:name=".PlaySercice"  android:process=":background" />

多线程使用注意:不要在任何非UI线程里面去持有UI对象的引用。系统为了确保所有的UI对象都只会被UI线程所进行创建,更新,销毁的操作,特地设计了对应的工作机制(当Activity被销毁的时候,由该Activity所触发的非UI线程都将无法对UI对象进行操作,否者就会抛出程序执行异常的错误)来防止UI对象被错误的使用

内存泄漏,GC等

0,使用Memory Monitor和DDMS下的Heap可以查看内存使用信息
扩展:http://blog.csdn.net/itfootball/article/details/48712595
1,内存泄漏检测--LeakCanary
扩展:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0509/2854.html
2,Memory Churn内存抖动
内存抖动是因为在短时间内大量的对象被创建又马上被释放。瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,会触发GC从而导致刚产生的对象又很快被回收。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。

解决上面的问题有简洁直观方法,如果你在Memory Monitor里面查看到短时间发生了多次内存的涨跌,这意味着很有可能发生了内存抖动。


同时我们还可以通过Allocation Tracker来查看在短时间内,同一个栈中不断进出的相同对象。这是内存抖动的典型信号之一。
当你大致定位问题之后,接下去的问题修复也就显得相对直接简单了。例如,你需要避免在for循环里面分配对象占用内存,需要尝试把对象的创建移到循环体之外,自定义View中的onDraw方法也需要引起注意,每次屏幕发生绘制以及动画执行过程中,onDraw方法都会被调用到,避免在onDraw方法里面执行复杂的操作,避免创建对象。对于那些无法避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,但是这里需要注意结束使用之后,需要手动释放对象池中的对象。

3,代码问题导致内存抖动例子
演示一个例子,如何通过修改代码来避免内存抖动。优化之前的内存检测图:


定位代码之后,修复了String拼接的问题:

优化之后的内存监测图:

4,使用对象池
http://blog.csdn.net/qq_31726827/article/details/50484815
在程序里面经常会遇到的一个问题是短时间内创建大量的对象,导致内存紧张,从而触发GC导致性能问题。对于这个问题,我们可以使用对象池技术来解决它。通常对象池中的对象可能是bitmaps,views,paints等等。关于对象池的操作原理,不展开述说了,请看下面的图示:

android_perf_2_object_pool

使用对象池技术有很多好处,它可以避免内存抖动,提升性能,但是在使用的时候有一些内容是需要特别注意的。通常情况下,初始化的对象池里面都是空白的,当使用某个对象的时候先去对象池查询是否存在,如果不存在则创建这个对象然后加入对象池,但是我们也可以在程序刚启动的时候就事先为对象池填充一些即将要使用到的数据,这样可以在需要使用到这些对象的时候提供更快的首次加载速度,这种行为就叫做预分配。使用对象池也有不好的一面,程序员需要手动管理这些对象的分配与释放,所以我们需要慎重地使用这项技术,避免发生对象的内存泄漏。为了确保所有的对象能够正确被释放,我们需要保证加入对象池的对象和其他外部对象没有互相引用的关系。

5,缓存LRU
在Android上面最常用的一个缓存算法是LRU(Least Recently Use),关于LRU算法,不展开述说,用下面一张图演示下含义:

android_perf_2_lru_mode

LRU Cache的基础构建用法如下:
android_perf_2_lru_key_value

为了给LRU Cache设置一个比较合理的缓存大小值,我们通常是用下面的方法来做界定的:
android_perf_2_lru_size

使用LRU Cache时为了能够让Cache知道每个加入的Item的具体大小,我们需要Override下面的方法:
android_perf_2_lru_sizeof

使用LRU Cache能够显著提升应用的性能,可是也需要注意LRU Cache中被淘汰对象的回收,否者会引起严重的内存泄露。

6,Avoiding Allocations in onDraw()
我们都知道应该避免在onDraw()方法里面执行导致内存分配的操作,下面讲解下为何需要这样做。
首先onDraw()方法是执行在UI线程的,在UI线程尽量避免做任何可能影响到性能的操作。虽然分配内存的操作并不需要花费太多系统资源,但是这并不意味着是免费无代价的。设备有一定的刷新频率,导致View的onDraw方法会被频繁的调用,如果onDraw方法效率低下,在频繁刷新累积的效应下,效率低的问题会被扩大,然后会对性能有严重的影响。

android_perf_2_ondraw_gc

如果在onDraw里面执行内存分配的操作,会容易导致内存抖动,GC频繁被触发,虽然GC后来被改进为执行在另外一个后台线程(GC操作在2.3以前是同步的,之后是并发),可是频繁的GC的操作还是会影响到CPU,影响到电量的消耗。
那么简单解决频繁分配内存的方法就是把分配操作移动到onDraw()方法外面,通常情况下,我们会把onDraw()里面new Paint的操作移动到外面,如下面所示:
android_perf_2_ondraw_paint

性能优化

0,过度绘制
用户界面卡顿,原因Overdraw--屏幕某像素在同一帧的时间内被绘制多次

View的onDraw方法不要执行大量操作(不要创建新的局部对象,占内存)
View的onDraw方法不要执行耗时操作,也不要执行成千上万的循环(循环抢占cpu时间片,绘制卡顿)

Android中的图形渲染,经过三个阶段:测量onMeasure,布局onLayout,绘制onDraw
通常视图层级越深,测量视图的时间就越长;
在视图渲染期间,每个View都要向它的父View提供自己的尺寸。如果父View发现了任意一个尺寸问题,它就会强制要求所有的子View重新测量。即便没有错误发生,重新测量也可能出现。

例如:为了正确地进行布局,RelativeLayout通常会对它们的子视图进行2次测量,子视图使用了layout_weight属性的LinearLayout也会对它的子视图进行2次测量;
测量和重新测量的代价昂贵,会严重影响渲染速度。想确保应用流畅需要移除那些非必须的View以及减少View层级(面试题必备)

1,使用Hierarchy Viewer
如下图:三个点分别表示,测量,布局,绘制三个阶段耗费的时间
红色表示该View的渲染速度比其他的所有参与测试的节点都慢
黄色表示该View的渲染速度慢于50%以上的其他测试节点
绿色表示该View的渲染速度至少快于一半以上的其他测试节点

image

知道哪些界面渲染比较慢再对症下药

2,Lint检查工具
Lint的功能非常强大,他能够扫描各种问题。当然我们可以通过Android Studio设置找到Lint,对Lint做一些定制化扫描的设置,可以选择忽略掉那些不想Lint去扫描的选项,我们还可以针对部分扫描内容修改它的提示优先级。

image.png

image.png

3,数据采集和分析--TraceView
扩展:http://blog.jobbole.com/78995/

4,Overdraw, Cliprect, QuickReject
引起性能问题的一个很重要的方面是因为过多复杂的绘制操作
过于复杂的自定义的View(重写了onDraw方法),Android系统无法检测具体在onDraw里面会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。

除了clipRect方法之外,我们还可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。做了那些优化之后,我们可以通过上面介绍的Show GPU Overdraw来查看效果。

5,Container Performance
运算性能问题是选择基础合理的算法,例如冒泡排序与快速排序的性能差异:


避免我们重复造轮子,Java提供了很多现成的容器,例如Vector,ArrayList,LinkedList,HashMap等等,在Android里面还有新增加的SparseArray等,我们需要了解这些基础容器的性能差异以及适用场景。这样才能够选择合适的容器,达到最佳的性能。

6,自定义View中的性能

  • Useless calls to onDraw():我们知道调用View.invalidate()会触发View的重绘,有两个原则需要遵守,第1个是仅仅在View的内容发生改变的时候才去触发invalidate方法,第2个是尽量使用ClipRect等方法来提高绘制的性能。
  • Useless pixels:减少绘制时不必要的绘制元素,对于那些不可见的元素,我们需要尽量避免重绘。
  • Wasted CPU cycles:对于不在屏幕上的元素,可以使用Canvas.quickReject把他们给剔除,避免浪费CPU资源。另外尽量使用GPU来进行UI的渲染,这样能够极大的提高程序的整体表现性能。

7,图片解码性能
Android为图片提供了4种解码格式,他们分别占用的内存大小如下图所示:

android_perf_2_pixel_format

随着解码占用内存大小的降低,清晰度也会有损失。我们需要针对不同的应用场景做不同的处理,大图和小图可以采用不同的解码率。在Android里面可以通过下面的代码来设置解码率:
android_perf_2_pixel_decode

8,Bitmap缩放
对bitmap做缩放,这也是Android里面最遇到的问题。对bitmap做缩放的意义很明显,提示显示性能,避免分配不必要的内存。Android提供了现成的bitmap缩放的API,叫做createScaledBitmap(),使用这个方法可以获取到一张经过缩放的图片。

android_perf_2_sacle_bitmap_created

上面的方法能够快速的得到一张经过缩放的图片,可是这个方法能够执行的前提是,原图片需要事先加载到内存中,如果原图片过大,很可能导致OOM。下面介绍其他几种缩放图片的方式。
inSampleSize能够等比的缩放显示图片,同时还避免了需要先把原图加载进内存的缺点。我们会使用类似像下面一样的方法来缩放bitmap:
android_perf_2_sacle_bitmap_code

android_perf_2_sacle_bitmap_insamplesize

另外,我们还可以使用inScaled,inDensity,inTargetDensity的属性来对解码图片做处理,源码如下图所示:
android_perf_2_sacle_bitmap_inscale

还有一个经常使用到的技巧是inJustDecodeBounds,使用这个属性去尝试解码图片,可以事先获取到图片的大小而不至于占用什么内存。如下图所示:
android_perf_2_sacle_bitmap_injust

9,使用使用ArrayMap替代HashMap,使用SparseArray替代ArrayList
对象个数的数量级最好是千以内
数据组织形式包含Map结构

10,使用StringBuilder来替代频繁的“+”
11,谨慎使用static对象
因为static的生命周期过长,和应用的进程保持一致,使用不当很可能导致对象泄漏,在Android中应该谨慎使用static对象。

android_perf_3_leak_static

12,资源文件需要选择合适的文件夹进行存放
我们知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经过scale的处理。例如我们只在hdpi的目录下放置了一张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到200200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下。
13,Removing unused resources
减少APK安装包的大小也是Android程序优化中很重要的一个方面,我们不应该给用户下载到一个臃肿的安装包。假设这样一个场景,我们引入了Google Play Service的library,是想要使用里面的Maps的功能,但是里面的登入等等其他功能是不需要的,可是这些功能相关的代码与图片资源,布局资源如果也被引入我们的项目,这样就会导致我们的程序安装包臃肿。
所幸的是,我们可以使用Gradle来帮助我们分析代码,分析引用的资源,对于那些没有被引用到的资源,会在编译阶段被排除在APK安装包之外,要实现这个功能,对我们来说仅仅只需要在build.gradle文件中配置shrinkResource为true就好了,如下图所示:
android_perf_4_remove_unused_resource

为了辅助gradle对资源进行瘦身,或者是某些时候的特殊需要,我们可以通过tools:keep或者是tools:discard标签来实现对特定资源的保留与废弃,如下图所示:
android_perf_4_remove_unused_resource_tools

Gradle目前无法对values,drawable等根据运行时来决定使用的资源进行优化,对于这些资源,需要我们自己来确保资源不会有冗余

尽量复用已经存在的资源图片,使用代码的方式对已有的资源进行复用,如下图所示:

android_perf_6_smaller_apks_reuse

以上几点虽然看起来都微不足道,但是真正执行之后,能够显著减少安装包的资源图片大小。


参考:《Android群英传》《Android开发艺术探索》《Android从小工到专家》《Google Android性能优化系列视频》http://hukai.me/android-performance-patterns-season-2/

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

推荐阅读更多精彩内容