Android面试题--View相关

ListView是如何进行优化的?

  1. Item布局层级越少越好,使用hierarchyviewer工具查看
  2. 复用convertView和使用ViewHolder
  3. Item中有图片时异步加载
  4. 快速滑动时不加载图片
  5. Item中有图片时,对图片进行适当压缩
  6. 列表数据分页加载

实现自定义View的基本流程

  1. 自定义View的属性,编写attr.xml文件
  2. 在layout布局文件中引用,同时引用命名空间
  3. 在View的构造方法中获取自定义属性(构造方法中拿到attr.xml文件值)
  4. 重写onMeasure
  5. 重写onDraw

Android中Touch事件的传递机制

  1. Touch 事件传递的相关 API 有 dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent
  2. Touch 事件相关的类有 View、ViewGroup、Activity
  3. Touch 事件会被封装成 MotionEvent 对象,该对象封装了手势按下、移动、松开等动作
  4. Touch 事件通常从 Activity#dispatchTouchEvent 发出,只要没有被消费,会一直往下传递,
    到最底层的 View
  5. 如果 Touch 事件传递到的每个 View 都不消费事件,那么 Touch 事件会反向向上传递,最
    终交由 Activity#onTouchEvent 处理.
  6. onInterceptTouchEvent 为 ViewGroup 特有,可以拦截事件.
  7. Down 事件到来时,如果一个 View 没有消费该事件,那么后续的 MOVE/UP 事件都不会再
    给它

ViewPager

ViewPager 可视为一个可以实现一种卡片式左右滑动的 View 容器,使用该类类似于 ListView,需要用到自定义的适配器 PageAdapter,区别在于每次要获取一个 view 的方式,ViewPager 准确的说应该是一个ViewGroup。PagerAdapter 是 ViewPager 的支持者,ViewPager 调用它来获取所需显示的页面,而 PagerAdapter 也会在数据变化时,通知 ViewPager,这个类也是 FragmentPagerAdapter和 FragmentStatePagerAdapter 的基类。

实现PagerAdapter需要重写的方法

  1. getCount():获得 ViewPager 中有多少个 view;
  2. destroyItem(viewGroup, interesting, object):移除一个给定位置的页面;
  3. instantiateItem(viewGroup, int):将给定位置的 view 添加到 viewgroup(容器中),创建并
    显示出来,返回一个代表新增页面的 Object(key),key 和每个 view 要有一一对应的关系;
  4. isViewFromObject():判断 instantiateItem(viewGroup, int)所返回的 key 和每一个页面视图是
    否是代表的同一个视图;

FragmentPageAdapter 和 FragmentStatePagerAdapter 的区别

二者都继承PagerAdapter。FragmentPagerAdapter的每个Fragment会持久的保存在Fragment Manager中,只要用户可以返回到页面中,它都不会被销毁;FragmentStatePagerAdapter当页面不可见时,该Fragment就会被销毁,只保留Fragment的状态。所以FragmentPagerAdapter用在Fragment比较少的情况,FragmentStatePagerAdapter用在Fragment很多的情况下。

Android 中页面的横屏与竖屏操作

在配置文件中为 Activity 设置属性 android:screenOrientation="landscape(横屏)|portrait(竖屏)";在代码中设置:setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);setR
equestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);为防止切换后重新启动当前 Activity,应在配置文件中添加 android:configChanges=”keyboardHidden|orientation”属性,并在 Activity 中重写 onConfigurationChanged()方法。

获取手机中屏幕的宽和高的方法

通过 Context 的 getWindowManager()方法获得 WindowManager 对象,再从 WindowManager对象中通过 getDefaultDisplay()获得 Display 对象,再从 Display 对象中获得屏幕的宽和高。

Android 属性动画实现原理

工作原理:在一定时间间隔内,通过不断对值进行改变,并不断将该值赋给对象的属性,从而实现该对象在该属性上的动画效果。

  1. ValueAnimator:通过不断控制值的变化(初始值->结束值),将值手动赋值给对象的属性,再不断调用 View 的 invalidate()方法,去不断 onDraw()重绘 View,达到动画的效果。
    主要的三种方法:
    1. ValueAnimator.ofInt(int values):估值器是整型估值器 IntEaluator
    2. ValueAnimator.ofFloat(float values): 估值器是浮点型估值器 FloatEaluator
    3. ValueAnimator.ofObject(ObjectEvaluator, start, end):将初始值以对象的形式过渡到结束值,通过操作对象实现动画效果,需要实现 Interpolator 接口,自定义估值器。

估值器 TypeEvalutor,设置动画如何从初始值过渡到结束值的逻辑。插值器(Interpolator)决定值的变化模式(匀速、加速等);估值器(TypeEvalutor)决定值的具体变化数值。

//自定义估值器,需要实现 TypeEvaluator 接口
public class ObjectEvaluator implements TypeEvaluator {
     //复写 evaluate(),在 evaluate()里写入对象动画过渡的逻辑
     @Override 
     public Object evaluate(float fraction, Object startValue, Object endValue) {
     //参数说明
     //fraction:表示动画完成度(根据它来计算当前动画的值)
     //startValue、endValue:动画的初始值和结束值...
     //写入对象动画过渡的逻辑
     return value;
     //返回对象动画过渡的逻辑计算后的值
}
  1. ObjectAnimator:直接对对象的属性值进行改变操作,从而实现动画效果。ObjectAnimator 继承自 ValueAnimator 类,底层的动画实现机制还是基本值的改变。它是不断控制值的变化,再不断自动赋给对象的属性,从而实现动画效果。这里的自动赋值,是通过调用对象属性的 set/get 方法进行自动赋值,属性动画初始值如果有就直接取,没有则调用属性的 get()方法获取,当值更新变化时,通过属性的 set()方法进行赋值。 每次赋值都是调用 view 的 postInvalidate()/invalidate()方法不断刷新视图(实际调用了 onDraw()方法进行了重绘视图)。
    //Object 需要操作的对象;propertyName 需要操作的对象的属性;values 动画初始值&结束
    值。如果是两个值,则从 a->b 值过渡,如果是三值,则从 a->b->c
    ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String propertyName, float ...values);如果采用 ObjectAnimator 类实现动画,操作的对象的属性必须有 get()和 set()方法。

其他用法:

  1. AnimatorSet 组合动画
    AnimatorSet.play(Animator anim):播放当前动画
    AnimatorSet.after(long delay):将现有动画延迟 x 毫秒后执行
    AnimatorSet.with(Animator anim):将现有动画和传入的动画同时执行
    AnimatorSet.after(Animator anim):将现有动画插入到传入的动画之后执行
    AnimatorSet.before(Animator anim):将现有动画插入到传入的动画之前执行
  2. ViewPropertyAnimator 直接对属性操作,View.animate()返回的是一个 ViewPropertyAnimat
    or 对象,之后的调用方法都是基于该对象的操作,调用每个方法返回值都是它自身的实例
    View.animate().alpha(0f).x(500).y(500).setDuration(500).setInterpolator()
  3. 设置动画监听
Animation.addListener(new AnimatorListener() {
     @Override
     public void onAnimationStart(Animation animation) {
     //动画开始时执行
     }

     @Override
     public void onAnimationRepeat(Animation animation) {
     //动画重复时执行
     }

     @Override
     public void onAnimationCancel(Animation animation) {
     //动画取消时执行
     }

     @Override
     public void onAnimationEnd(Animation animation) {
     //动画结束时执行
     }
});

Android View 动画(补间动画)实现原理

主要有四种 AlpahAnimation\ScaleAnimation\RotateAnimation\TranslateAnimation 四种,对透明度、缩放、旋转、位移四种动画。在调用 View.startAnimation 时,先调用 View.setAnimation(Animation)方法给自己设置一个 Animation 对象,再调用 invalidate()来重绘自己。在 View.draw(Canvas, ViewGroup, long)方法中进行了 getAnimation(),并调用了 drawAnimation(ViewGroup, long, Animation, boolean)方法,此方法调用 Animation.getTranformation()方法,再调用 applyTranformation()方法,该方法中主要是对 Transformation.getMatrix().setTranslate/setRotate/setAlpha/setScale 来设置相应的值,这个方法系统会以 60FPS 的频率进行调用。具体是在调 Animation.start()方法中会调用 animationHandler.start()方法,从而调用了 scheduleAnimation()方法,这里会调用mChoreographer.postCallback(Choregrapher.CALLBACK_ANIMATION, this, null)放入事件队列中,等待 doFrame()来消耗事件。当一个 ChildView 要重画时,它会调用其成员函数 invalidate()函数将通知其 ParentView 这个ChildView 要重画,这个过程一直向上遍历到 ViewRoot,当 ViewRoot 收到这个通知后就会调用 ViewRoot 中的 draw 函数从而完成绘制。View.onDraw()有一个画布参数 Canvas,画布顾名思义就是画东西的地方,Android 会为每一个 View 设置好画布,View 就可以调用 Canvas的方法,比如:drawText()、drawBitmap()、drawPath() 等等去画内容。每一个 ChildView 的画布是由其 ParentView 设置的,ParentView 根据 ChildView 在其内部的布局来调整 Canvas,其中画布的属性之一就是定义和 ChildView 相关的坐标系,默认是横轴为 X 轴,从左至右,值逐渐增大,竖轴为 Y 轴,从上至下,值逐渐增大。

Android 补间动画就是通过 ParentView 来不断调整 ChildView 的画布坐标系来实现的,在 ParentView 的 dispatchDraw()方法会被调用。

dispatchDraw() {
       ....
       Animation a = ChildView.getAnimation();
       Transformation tm = a.getTransformation();
       Use tm to set ChildView's Canvas; Invalidate();
       ....
}

这里有两个类:Animation 和 Transformation,这两个类是实现动画的主要的类,Animation中主要定义了动画的一些属性比如开始时间、持续时间、是否重复播放等,这个类主要有两个重要的函数:getTransformation()和 applyTransformation(),在 getTransformation()中 Animation 会根据动画的属性来产生一系列的差值点,然后将这些差值点传给 applyTransformation(),这个函数将根据这些点来生成不同的 Transformation,Transformation 中包含一个矩阵和alpha 值,矩阵是用来做平移、旋转和缩放动画的,而 alpha 值是用来做 alpha 动画的(简单理解的话,alpha 动画相当于不断变换透明度或颜色来实现动画),调用 dispatchDraw()时会调用 getTransformation()来得到当前的 Transformation。某一个 View 的动画的绘制并不是由他自己完成的而是由它的父 View 完成。1)补间动画 TranslateAnimation,View 位置移动了,
可是点击区域还在原来的位置,为什么?View 在做动画是根据动画时间的插值计算出一个Matrix,不停的 invalidate(),在 onDraw()中的 Canvas 上使用这个计算出来的 Matrix 去 draw View 的内容。某个 View 的动画绘制并不是由它自己完成,而是由它的父 View 完成,使它的父 View 画布进行了移动,而点击时还是点击原来的画布。使得它看起来变化了。

requestLayout()、onLayout()、onDraw()、drawChild()区别与联系

  • requestLayout():会导致调用 measure()过程和 layout()过程。说明:只是对 View 树重新布局layout 过程包括 measure()和 layout()过程,如果 View 的 l,t,r,b 没有必变,那就不会触发onDraw;但是如果这次刷新是在动画里,mDirty 非空,就会导致 onDraw。
  • onLayout():如果该 View 是 ViewGroup 对象,需要实现该方法,对每个子视图进行布局
  • onDraw():绘制视图本身(每个 View 都需要重载该方法,ViewGroup 不需要实现该方法)
  • drawChild():去重新回调每个子视图的 draw()方法

自定义 View 处理宽度问题

在 onMeasure()的 getDefaultSize()的默认实现中,当 View 的测量模式是 AT_MOST 或 EXACTLY 时,View 的大小都会被设置成子 View MeasureSpec 的 specSize。子 View 的 MeasureSpec值是根据子 View 的布局参数和父容器的 MeasureSpec 值计算得来。当子 View 的布局参数是wrap_content 时,对应的测量模式是 AT_MOST,大小是 parentSize。

invalidate()和 postInvalidate()的区别及使用

View.invalidate():层层上传到父级,直到传递到 ViewRootImpl 后触发了 scheduleTraversals(),然后整个 View 树开始重新按照 View 绘制流程进行重绘任务。invalidate():在 UI 线程刷新 View;postInvalidate():在工作线程刷新 View(底层还是 handler)其实它的原理就是 invalidate+handler View.postInvalidate()最终会调用 ViewRootImpl.dispatchInvalidateDelayed()方法

public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

这里的 mHandler 是 ViewRootHandler 实例,在该 Handler 的 handleMessage 方法中调用了 View.invalidate()方法。

case MSG_INVALIDATE:
       ((View) msg.obj).invalidate();
       break;

requestLayout()和invalidate()的区别

  • requestLayout()会标记PFLAG_FORCE_LAYOUT,然后一层层往上调用父布局的requestLayout方法并标记PFLAG_FORCE_LAYOUT,最后调用ViewRootImpl中的requestLayout方法开始View的三大流程,然后被标记的View就会进行测量、布局和绘制流程,调用的方法为onMeasure、onLayout和onDraw。
  • invalidate()我们分析过,它的过程和requestLayout方法方法很像,但是invalidate方法没有标记PFLAG_FORCE_LAYOUT,所以不会执行测量和布局流程,而只是对需要重绘的View进行重绘,也就是只会调用onDraw方法,不会调用onMeasure和onLayout方法。

如何优化自定义 View

  1. 不要在 onDraw()或是 onLayout()中去创建对象,因为 onDraw()方法可能会被频繁调用,可以在 View 的构造函数中进行创建对象;
  2. 降低 View 的刷新频率,尽可能减少不必要的调用 invalidate()方法。或是调用带四种参数不同类型的 invalidate(),而不是调用无参的方法。无参变量需要刷新整个 View,而带参数的方法只需刷新指定部分的 View。在 onDraw()方法中减少冗余代码;
  3. 使用硬件加速,GPU 硬件加速可以带来性能增加;
  4. 状态保存与恢复,如果因内存不足,Activity 置于后台被杀重启时,View 应尽可能保存自己属性,可以重写 onSaveInstanceState()和 onRestoreInstanceState()方法,状态保存

Android 中动画的类型

  • 帧动画:通过指定每一帧图片和播放的时间,有序地进行播放而形成动画效果;
  • 补间动画:通过指定的 View 的初始状态、变化时间、方式等,通过一系列的算法去进行图形变换从而形成动画效果,主要有 Alpha、Scale、Translate、Rotate 四种效果;
  • 属性动画:在 Android3.0 开始支持,通过不断改变 View 的属性,不断的重绘而形成动画效果;

RecyclerView 在很多方面能取代 ListView,Google 为什么没有把 ListView设置成过时控件

ListView 采用的是 RecyclerBin 的回收机制在一些轻量级的 List 显示时效率更高。

RecyclerView 和 ListView 的区别

RecyclerView 可以完成 ListView、GridView 的效果,还可以完成瀑布流的效果。同时还可以设置列表的滚动方向(垂直或者水平);RecyclerView 中 View 的复用不需要开发者自己写代码,系统已经帮封装完成了。RecyclerView 可以进行局部刷新。RecyclerView 提供了 API来实现 item 的动画效果。在性能上:如果需要频繁的刷新数据,需要添加动画,则 RecyclerView 有较大的优势。如果只是作为列表展示,则两者区别并不是很大。

View事件分发机制

  1. Touch 事件分发中只有两个主角:ViewGroup 和 View。ViewGroup 包含 dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 三个相关事件。View 包含 dispatchTouchEvent、onTouchEvent 两个相关事件。其中 ViewGroup 又继承于 View;
  2. ViewGroup 和 View 组成了一个树状结构,根节点为 Activity 内部包含的一个 ViwGroup;
  3. 触摸事件由 Action_Down、Action_Move、Aciton_UP 组成,其中一次完整的触摸事件中,Down 和 Up 都只有一个,Move 有若干个,可以为 0 个;
  4. 当 Acitivty 接收到 Touch 事件时,将遍历子 View 进行 Down 事件的分发。ViewGroup 的遍历可以看成是递归的。分发的目的是为了找到真正要处理本次完整触摸事件的 View,这个 View 会在 onTouchuEvent 结果返回 true;
  5. 当某个子 View 返回 true 时,会中止 Down 事件的分发,同时在 ViewGroup 中记录该子 View。接下去的 Move 和 Up 事件将由该子 View 直接进行处理。由于子 View 是保存在 ViewGroup 中的,多层 ViewGroup 的节点结构时,上级 ViewGroup 保存的会是真实处理事件的 View 所在的 ViewGroup 对象:如 ViewGroup0-ViewGroup1-TextView 的结构中,TextView 返回了 true,它将被保存在 ViewGroup1 中,而 ViewGroup1 也会返回 true,被保存在 ViewGroup0 中。当 Move 和 UP 事件来时,会先从 ViewGroup0 传递至ViewGroup1,再由 ViewGroup1传递至 TextView;
  6. 当 ViewGroup 中所有子 View 都不捕获 Down 事件时,将触发 ViewGroup 自身的 onTouch事件。触发的方式是调用 super.dispatchTouchEvent 函数,即父类 View 的 dispatchTouchEvent 方法。在所有子 View 都不处理的情况下,触发 Acitivity 的 onTouchEvent 方法;
  7. onInterceptTouchEvent 有两个作用:1. 拦截 Down 事件的分发。2. 中止 Up 和 Move 事件向目标 View 传递,使得目标 View 所在的 ViewGroup 捕获 Up 和 Move 事件;

是否用过 SurfaceView?它的继承方式是什么?它与 View 的区别(从源码角度,如加载,绘制等)

SurfaceView 中采用了双缓冲机制,保证了 UI 界面的流畅性,同时 SurfaceView 不在主线程中绘制,而是另开辟一个线程去绘制,所以它不妨碍 UI 线程;SurfaceView 继承于 View,它和 View 主要有以下三点区别:(1)View 底层没有双缓冲机制,SurfaceView 有;(2)View 主要适用于主动更新,而 SurfaceView 适用与被动的更新,如频繁的刷新(3)View 会在主线程中去更新 UI,而 SurfaceView 则在子线程中刷新;SurfaceView 的内容不在应用窗口上,所以不能使用变换(平移、缩放、旋转等)。也难以放在 ListView 或者 ScrollView 中,不能使用 UI 控件的一些特性比如 View.setAlpha()。View:显示视图,内置画布,提供图形绘制函数、触屏事件、按键事件函数等;必须在 UI 主线程内更新画面,速度较慢。SurfaceView:
基于 view 视图进行拓展的视图类,更适合 2D 游戏的开发;是 view 的子类,类似使用双缓机制,在新的线程中更新画面所以刷新界面速度比 view 快,Camera 预览界面使用 SurfaceView。GLSurfaceView:基于 SurfaceView 视图再次进行拓展的视图类,专用于 3D 游戏开发的视图;是 SurfaceView 的子类,OpenGL 专用。

View 的滑动方式

  1. layout(left,top,right,bottom):通过修改 View 四个方向的属性值来修改 View 的坐标,从而滑动 View
  2. offsetLeftAndRight()、offsetTopAndBottom():指定偏移量滑动 view
  3. LayoutParams 改变布局参数:LayoutParams 中保存了 View 的布局参数,可以通过修改布局参数的方式滑动 View
  4. 通过动画来移动 View:注意安卓的平移动画不能改变 View 的位置参数,属性动画可以改变 View 的位置参数
  5. scrollTo/scrollBy:注意移动的是 View 的内容,scrollBy(50,50)你会看到屏幕上的内容向屏幕的左上角移动了,这是参考对象不同导致的,你可以看作是它移动的是手机屏幕,手机屏幕向右下角移动,那么屏幕上的内容就像左上角移动了
  6. Scroller:Scroller 需要配置 computeScroll 方法实现 View 的滑动,Scroller 本身并不会滑动View,它的作用可以看作一个插值器,它会计算当前时间点 View 应该滑动到的距离,然后View 不断的重绘,不断的调用 computeScroll 方法,这个方法是个空方法,所以我们重写这个方法,在这个方法中不断的从 Scroller 中获取当前 View 的位置,调用 scrollTo 方法实现滑动的效果

View 的加载流程

View 随着 Activity 的创建而加载,startActivity 启动一个 Activity 时,在 ActivityThread 的 handleLaunchActivity 方法中会执行 Activity 的 onCreate 方法,这个时候会调用 setContentView加载布局创建出 DecorView 并将我们的 layout 加载到 DecorView 中,当执行到 handleResumeActivity 时,Activity 的 onResume 方法被调用,然后 WindowManager 会将 DecorView 设置给 ViewRootImpl,这样 DecorView 就被加载到 Window 中了,此时界面还没有显示出来,还需要经过 View 的 measure、layout 和 draw 方法,才能完成 View 的工作流程。我们需要知道 View 的绘制是由 ViewRoot 来负责的,每一个 DecorView 都有一个与之关联的 ViewRoot,这种关联关系是由 WindowManager 维护的,将 DecorView 和 ViewRoot 关联之后,ViewRootImpl 的 requestLayout 会被调用以完成初步布局,通过 scheduleTraversals 方法向主线程发送消息请求遍历,最终调用 ViewRootImpl 的 performTraversals 方法,这个方法会执行 View 的measure、layout 和 draw 流程

View 的 measure、layout 和 draw 流程

在上边的分析中我们知道,View 绘制流程的入口在 ViewRootImpl 的 performTraversals 方法,在方法中首先调用 performMeasure 方法,传入一个 childWidthMeasureSpec 和 childHeightMeasureSpec 参数,这两个参数代表的是 DecorView 的 MeasureSpec 值,这个 MeasureSpec值由窗口的尺寸和 DecorView 的 LayoutParams 决定,最终调用 View 的 measure 方法进入测量流程
measure:View 的 measure 过程由 ViewGroup 传递而来,在调用 View.measure 方法之前,会首先根据 View 自身的 LayoutParams 和父布局的 MeasureSpec 确定子 view 的 MeasureSpec,然后将 view 宽高对应的 measureSpec 传递到 measure 方法中,那么子 view 的 MeasureSpec获取规则是怎样的?分几种情况进行说明

  1. 父布局是 EXACTLY 模式:
    a. 子 View 宽或高是个确定值,那么子 View 的 size 就是这个确定值,mode 是 EXACTLY(是不是说子 View 宽高可以超过父 View?见下一个)
    b. 子 View 宽或高设置为 match_parent,那么子 View 的 size 就是占满父容器剩余空间,模式就是 EXACTLY
    c. 子 View 宽或高设置为 wrap_content,那么子 View 的 size 就是占满父容器剩余空间,不能超过父容器大小,模式就是 AT_MOST
  2. 父布局是 AT_MOST 模式:
    a. 子 View 宽或高是个确定值那么子 View 的 size 就是这个确定值,mode 是 EXACTLY
    b. View 宽或高设置为 match_parent,那么子 View 的 size 就是占满父容器剩余空间,不能超过父容器大小,模式就是 AT_MOST
    c. 子 View 宽或高设置为 wrap_content,那么子 View 的 size 就是占满父容器剩余空间,不能超过父容器大小,模式就是 AT_MOST
  3. 父布局是 UNSPECIFIED 模式:
    a. 子 View 宽或高是个确定值那么子 View 的 size 就是这个确定值,mode 是 EXACTLY
    b. 子 View 宽或高设置为 match_parent,那么子 View 的 size 就是 0,模式就是 UNSPECIFIED
    c. 子 View 宽或高设置为 wrap_content,那么子 View 的 size 就是 0,模式就是 UNSPECIFIED 获取到宽高的 MeasureSpec 后,传入 View 的 measure 方法中来确定 View 的宽高,这个时候还要分情况 1. 当 MeasureSpec 的 mode 是 UNSPECIFIED,此时 View 的宽或者高要看 View 有没有设置背景,如果没有设置背景,就返回设置的 minWidth 或 minHeight,这两个值如果没有设置默认就是 0,如果 View 设置了背景,就取 minWidth 或 minHeight 和背景这个drawable 固有宽或者高中的最大值返回 2. 当 MeasureSpec 的 mode 是 AT_MOST 和 EXACTLY,此时 View 的宽高都返回从 MeasureSpec 中获取到的 size 值,这个值的确定见上边的分析。因此如果要通过继承 View 实现自定义 View,一定要重写 onMeasure 方法对 wrap_conten 属性做处理,否则,他的 match_parent 和 wrap_content 属性效果就是一样的layout:layout 方法的作用是用来确定 View 本身的位置,onLayout 方法用来确定所有子元素的位置,当 ViewGroup 的位置确定之后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在子元素的 layout 方法中 onLayout 方法又会被调用。layout 方法的流程是,首先通过 setFrame 方法确定 View 四个顶点的位置,然后 View 在父容器中的位置也就确定了,接着会调用 onLayout 方法,确定子元素的位置,onLayout 是个空方法,需要继承者去实现。
    getMeasuredHeight 和 getHeight 方法有什么区别?getMeasuredHeight(测量高度)形成于 View 的 measure 过程,getHeight(最终高度)形成于 layout 过程,在有些情况下,View 需要measure 多次才能确定测量宽高,在前几次的测量过程中,得出的测量宽高有可能和最终宽高不一致,但是最终来说,还是会相同,有一种情况会导致两者值不一样,如下,此代码会导致 View 的最终宽高比测量宽高大 100px
public void layout(int l, int t, int r, int b) {
        super.layout(l, t, r + 100, b + 100);
}

draw:
View 的绘制过程遵循如下几步:
a. 绘制背景 background.draw(canvas)
b. 绘制自己(onDraw)
c. 绘制 children(dispatchDraw)
d. 绘制装饰(onDrawScrollBars)
View 绘制过程的传递是通过 dispatchDraw 来实现的,它会遍历所有的子元素的 draw 方法,如此 draw 事件就一层一层的传递下去了
PS:View 有一个特殊的方法 setWillNotDraw,如果一个 View 不需要绘制内容,即不需要重写 onDraw 方法绘制,可以开启这个标记,系统会进行相应的优化。默认情况下,View 没有开启这个标记,默认认为需要实现 onDraw 方法绘制,当我们继承 ViewGroup 实现自定义控件,并且明确知道不需要具备绘制功能时,可以开启这个标记,如果我们重写了 onDraw,那么要显示的关闭这个标记子View 宽高可以超过父View?能 1. android:clipChildren="false" 这个属性要设置在父View上。代表其中的子 View 可以超出屏幕。2. 子View 要有具体的大小,一定要比父View大才能超出。比如父 view 高度 100px,子 view 设置高度 150px。子 view 比父 view 大,这样超出的属性才有意义。(高度可以在代码中动态赋值,但不能用 wrap_content/match_partent)。对父布局还有要求,要求使用 LinearLayout(反正我用 RelativeLayout 是不行)。你如果必须用其他布局可以在需要超出的View 上面套一个 LinearLayout 外面再套其他的布局。最外面的布局如果设置的 padding 不能超出

自定义 View 需要注意的几点

  1. 让 View 支持 wrap_content 属性,在 onMeasure 方法中针对 AT_MOST 模式做专门处理,否则 wrap_content 会和 match_parent 效果一样(继承 ViewGroup 也同样要在 onMeasure中做这个判断处理)
if(widthMeasureSpec == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension(200,200); //wrap_content 情况下要设置一个默认值,200 只是举个例子,最终的值需要计算得到刚好包裹内容的宽高值
} else if(widthMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension(200,heightMeasureSpec);
} else if(heightMeasureSpec == MeasureSpec.AT_MOST) {
setMeasuredDimension(heightMeasureSpec,200);
}
  1. 让 View 支持 padding(onDraw 的时候,宽高减去 padding 值,margin 由父布局控制,不需要 View 考虑),自定义 ViewGroup 需要考虑自身的 padding 和子 View 的 margin 造成的影响
  2. 在 View 中尽量不要使用 handler,使用 View 本身的 post 方法
  3. 在 onDetachedFromWindow 中及时停止线程或动画
  4. View 带有滑动嵌套情形时,处理好滑动冲突

图片库对比 Picasso(Square)、Glide(Google)、Fresco(Facebook)

Picasso
优点:框架体积小,使用方便、一行代码完成加载图片并显示
缺点:不支持 GIF 图,并且它可能是想让服务端去处理图片的缩放,因此它缓存的图片是未经过缩放的,也就是缓存的原图,默认使用 ARGB_8888 格式缓存图片,缓存体积大;Picasso 是二级缓存,它支持内存缓存而不支持磁盘缓存,而 Glide 是三级缓存

Glide
优点:可以说是 Picasso 的升级版,有 Picasso 的优点,并且支持 GIF 图片加载显示,图片缓存也会自动缩放,默认使用 RGB_565 格式缓存图片,如果缓存同一尺寸的图片则是 Picasso缓存体积的一半
缺点:默认使用 RGB_565 格式缓存图片,没有 ARGB_8888 格式的图片效果好,如果对图片质量要求高,需要手动设置缓存格式为 ARGB_8888

Fresco
优点
1. 图片存储在安卓系统的匿名共享内存,而不是虚拟机的堆内存中;图片的中间缓冲数据也存放在本地堆内存,所以应用程序有更多的内存使用,不会因为图片加载而导致 OOM,同时也减少 GC 频繁调用回收 Bitmap 导致的界面卡顿,性能更高
2. 渐进式加载 JPEG 图片,支持图片从模糊到清晰加载
3. 图片可以以任意的中心点显示在 ImageView,而不仅仅是图片的中心
4. JPEG 图片改变大小也是在 native 进行的,不是在虚拟机的堆内存,同样减少 OOM
缺点:框架较大(2~3MB),影响 Apk 包的体积;API 不够简洁,使用比 Glide 繁琐

Glide 源码解析

  1. Glide.with(context)创建了一个 RequestManager,同时实现加载图片与组件生命周期绑定;在 Activity 上创建一个透明的 RequestManagerFragment 加入到 FragmentManager 中,通过添加的 Fragment 感知 Activity/Fragment 的生命周期,因为添加到 Activity 中的 Fragment 会跟随 Activity 的生命周期,在 RequestManagerFragment 中的相应生命周期方法中通过 lifecycle 传递给在 lifecycle 中注册的LifecycleListener
  2. RequestManager.load(url)创建了一个 RequestBuilder<T>对象 T 可以是 Drawable 对象
    或是 ResourceType 等
  3. RequestBuilder.into(view)-->into(glideContext.buildImageViewTarget(view,transcodeClass))返回的是一个 DrawableImageViewTarget,Target 用来最终展示图片,buildImageViewTarget-->ImageViewTargetFactory.buildTarget()根据传入 class 参数的不同构建不同的 Target 对象,这个 class 对象是根据构建 Glide 时是否调用了 asBitmap()方法,如果调用了会构建出 BitmapImageViewTarget,否则构建的是 GlideDrawableImageViewTarget 对象
  4. GenericRequestBuilder.into(Target),该方法进行了构建 Request,并用 RequestTracker.runRequest()
    Request request = buildRequest(target);//构建 Request 对象,Request 是用来请求加载图片的,它调用了 buildRequestRecursive()方法,内部调用了 GenericRequest.obtain()方法
    target.setRequest(request);
    lifecycle.addListener(target);
    requestTracker.runRequest(request);//判断 Glide 当前是不是处于暂停状态,若不是则调用 Request.begin() 方法来执行 Request,否则将 Request 添加到待执行队列里,等暂停状态解除了后再执行 --> GenericRequest.begin()
  5. onSizeReady()-->Engine.load(signature,width,height,dataFetcher,loadProvider,transfor
    mation,transcoder,priority,isMemoryCacheable,diskCacheStrategy,this)
    a. 先构建 EngineKey;
    b. loadFromCache 从缓存中获取 EngineResource,如果缓存中获取到 cache 就调用cb.onResourceReady(cached);
    c. 如果缓存中不存在调用 loadFromActiveResources 从 active 中获取,如果获取到就调用cb.onResourceReady(cached);
    d. 如果是 active 中也不存在,调用 EngineJob.start(EngineRunable),从而调用 decodeFromSource()/decodeFromCache()如果是调用 decodeFromSource()-->ImageVideoFetcher.loadData()-->HttpUrlFetcher() 调用 HttpUrlConnection 进行网络请求资源-->得于 InputStream()后,调用 decodeFromSourceData()-->loadProvider.getSourceDecoder().decode()方法解码-->GifBitmapWrapperResourceDecoder.decode()-->decodeStream()先从流中读取 2 个字节判断是 GIF还是普通图,若是 GIF 调用 decodeGifWrapper()来解码,若是普通静图则调用 decodeBitmapWrapper()来解码-->bitmapDecoder.decode()

Glide 的缓存机制

  • 内存缓存:LruResourceCache(memory) + 弱引用activeResources,Map<Key, WeakReference<EngineResource<?>>> activeResources 正在使用的资源,当 acquired 变量大于 0,说明图片正在使用,放到 activeResources 弱引用缓存中,经过 release() 后,acquired = 0, 说明图片不再使用,会把它放进 LruResourceCache中
  • 磁盘缓存:DiskLruCache,这里分为 Source(原始图片)和 Result(转换后的图片),第一次获取图片肯定网络取,然后存 active\disk 中,再把图片显示出来,第二次读取相同的图片,并加载到相同大小的imageview 中,会先从 memory 中取,没有再去 active 中获取。如果 activity 执行到 onStop 时,图片被回收,active 中的资源会被保存到 memory 中,active 中的资源被回收。当再次加载图片时,会从 memory 中取,再放入 active 中, 并将 memory 中对应的资源回收。 之所以需要 activeResources,它是一个随时可能被回收的资源,memory 的强引用频繁读写可能造成内存激增频繁 GC,而造成内存抖动。资源在使用过程中
    保存在 activeResources 中,而 activeResources 是弱引用,随时被系统回收,不会造成内存过多使用和泄漏

Glide设置内存缓存、设置磁盘缓存策略

    //跳过内存缓存
    Glide.with(this).load(mUrl).skipMemoryCache(true).into(mIv);
    //DiskCacheStrategy.ALL:缓存原图(SOURCE)和处理图(RESULT)
    //DiskCacheStrategy.NONE:什么都不缓存
    //DiskCacheStrategy.SOURCE:只缓存原图(SOURCE)
    //DiskCacheStrategy.RESULT:只缓存处理图(RESULT) —默认值
    Glide.with(this).load(mUrl).diskCacheStrategy(DiskCacheStrategy.ALL).into(mIv);

Glide 内存缓存如何控制大小

Glide 内存缓存最大空间(maxSize) = 每个进程可用最大内存 * 0.4(低配手机是每个进程可用最大内存 * 0.33)
磁盘缓存大小是 250MB int DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024;

LruCache 原理

  • LruCache 中 Lru 算法的实现就是通过 LinkedHashMap 来实现的。LinkedHashMap 继承于HashMap,它使用了一个双向链表来存储 Map 中的 Entry 顺序关系,对于 get、put、remove等操作,LinkedHashMap 除了要做 HashMap 做的事情,还做些调整 Entry 顺序链表的工作。
  • LruCache 中将 LinkedHashMap 的顺序设置为 LRU 顺序来实现 LRU 缓存,每次调用 get(也就是从内存缓存中取图片),则将该对象移到链表的尾端。调用 put 插入新的对象也是存储在链表尾端,这样当内存缓存达到设定的最大值时,将链表头部的对象(近期最少用到的)移除。

LruCache 怎么回收 Bitmap

LruCache 每次添加 Bitmap 图片缓存的时候(put 操作),都会调用 sizeOf 方法,返回 Bitmap的内存大小给 LruCache,然后循环增加这个 size。当这个 Size 内存大小超过初始化设定的cacheMemory 大小时,则遍历 map 集合,把最近最少使用的元素 remove 掉

图片压缩的详细过程

使用 BitmapFactory.Options 设置 inSampleSize 就可以缩小图片。属性值 inSampleSize 表示缩略图大小为原始图片大小的几分之一。即如果这个值为 2,则取出的缩略图的宽和高都是原始图片的 1/2,图片的大小就为原始大小的 1/4。如果知道图片的像素过大,就可以对其 进 行 缩 小 。 那 么 如 何 才 知 道 图 片 过 大 呢 ? 使 用 BitmapFactory.Options 设 置inJustDecodeBounds 为 true 后,再使用 decodeFile()等方法,并不会真正的分配空间,即解码出来的 Bitmap 为 null,但是可计算出原始图片的宽度和高度,即 options.outWidth 和options.outHeight。通过这两个值,就可以知道图片是否过大了。

谈谈你对 Bitmap 的理解,什么时候应该手动调用 bitmap.recycle()

Bitmap 是 android 中经常使用的一个类,它代表了一个图片资源。Bitmap 消耗内存很严重,如果不注意优化代码,经常会出现 OOM 问题,优化方式通常有这么几种:1. 使用缓存;2. 压缩图片;3. 及时回收;至于什么时候需要手动调用 recycle(),这就看具体场景了,原则是当我们不再使用 Bitmap 时,需要回收之。另外,我们需要注意,Android3.0 之前 Bitmap对象与像素数据是分开存放的,Bitmap 对象存在 Java Heap 中而像素数据存放在 NativeMemory 中,这时很有必要调用 recycle 回收内存。但是从 Android3.0 开始,Bitmap 对象和像素数据都是存在 Heap 中,GC 可以回收其内存。Android8.0 开始,Bitmap 对象与像素数据改为分开存放

一张 Bitmap 所占内存以及内存占用的计算

一张图片(Bitmap)占用的内存影响因素:图片原始长和宽、手机屏幕密度、图片存放路径下的密度、单位像素占用字节数
BitmapSize = 图片长度 *(inTargetDensity(手机的 density)/ inDensity(图片存放目录的density)) * 图片宽度 * (inTargetDensity(手机的 density)/ inDensity(图片存放目录的density)) * 单位像素占用的字节数(图片长宽单位是像素)
a. 图片长宽单位是像素:单位像素字节数由其参数 BitmapFactory.Options.inPreferredConfig变量决定,它是Bitmap.Config 类型,包括以下几种值:ALPHA_8 图片只有 alpha 值,占用一个字节:ARGB_4444 一个像素占用 2 个字节,A\R\G\B 各占 4bits;ARGB_8888 一个像素占用 4 个字节,A\R\G\B 各占 8bits(高质量图片格式,Bitmap 默认格式);RGB_565一个像素占用 2 字节,不支持透明和半透明,R 占 5bit, Green 占 6bit, Blue 占用 5bit. 从Android4.0 开始该项无效。
b. inTargetDensity 手机的屏幕密度(跟手机分辨率有关系) inDensity 原始资源密度(mdpi:160;hdpi:240;xhdpi:320;xxhdpi:480;xxxhdpi:640)当 Bitmap 对象在不使用时,应该先调用 recycle(),再将它设置为 null,虽然 Bitmap 在被回收时可通过 BitmapFinalizer 来回收内存。但只有系统垃圾回收时才会回收。Android3.0 之前,Bitmap 内存分配在 Native 堆中,Android3.0 开始,Bitmap 的内存分配在 Dalvik 堆中,即 Java堆中,调用 recycle()并不能立即释放 Native 内存。Android8.0 开始,Bitmap 内存分配在 Native中

Bitmap 如何处理大图,如一张 30M 的大图,如何预防 OOM

避免 OOM 的问题就需要对大图片的加载进行管理,主要通过缩放来减小图片的内存占用。BitmapFactory 提供的加载图片的四类方法(decodeFile、decodeResource、decodeStream、decodeByteArray)都支持 BitmapFactory.Options 参数,通过 inSampleSize 参数就可以很方便地对一个图片进行采样缩放。比如一张 1024 * 1024 的高清图片来说。那么它占有的内存为1024 * 1024 * 4,即 4MB,如果 inSampleSize 为 2,那么采样后的图片占用内存只有 512 * 512 * 4,即 1MB(注意:根据最新的官方文档指出,inSampleSize 的取值应该总是为 2 的指数,即 1、2、4、8 等等,如果外界输入不足为 2 的指数,系统也会默认选择最接近 2 的指数代替,比如 2)
综合考虑。通过采样率即可有效加载图片,流程如下将 BitmapFactory.Options 的inJustDecodeBounds 参数设为 true 并加载图片从 BitmapFactory.Options 中取出图片的原始宽高信息,它们对应 outWidth 和outHeight 参数。根据采样率的规则并结合目标 View 的所需大小计算出采样率 inSampleSize,将 BitmapFactory.Options 的 inJustDecodeBounds 参数设为false,重新加载图片

Bitmap 的使用以及内存优化

位图是相对于矢量图而言的,也称为点阵图,位图由像素组成。图像的清晰度由单位长度内的像素的多少来决定的,在 Android 系统中,位图使用 Bitmap 类来表示,该类位于android.graphics 包中,被 final 所修饰,不能被继承,创建 Bitmap 对象可使用该类的静态方法 createBitmap,也可以借助 BitmapFactory 类来实现。Bitmap 可以获取图像文件信息,如宽高尺寸等,可以进行图像剪切、旋转、缩放等操作。BitmapFactory 是创建 Bitmap 的工具类,能够以文件、字节数组、输入流的形式创建位图对象,BitmapFactory 类提供的都是静态方法可以直接调用,BitmapFactory.Options 类是BitmapFactory 的静态内部类,主要用于设定位图的解析参数。在解析位图时,将位图进行相应的缩放,当位图资源不再使用时,强制资源回收,可以有效避免内存溢出。

缩略图:不加载位图的原有尺寸,而是根据控件的大小呈现图像的缩小尺寸,就是缩略图。将大尺寸图片解析为控件所指的尺寸的思路: 实例化 BitmapFactory.Options 对象来获取解析属性对象,设置 BitmapFactory.Options 的属性 inJustDecodeBounds 为 true 后,再解析位图时并不分配存储空间,但可以计算出原始图片的宽度和高度,即 outWidth 和 outHeight,将这两个数值与控件的宽高尺寸相除,就可以得到缩放比例,即 inSampleSize 的值,然后重新设置 inJustDecodeBounds 为 false,inSampleSize 为计算所得的缩放比例,重新解析位图文件,即可得到原图的缩略图。

获取控件宽高属性的方法:可以利用控件的 getLayoutParams()方法获得控件的 LayoutParams对象,通过 LayoutParams 的 Width 和 Height 属性来得到控件的宽高,同样可以利用控件的setLayoutParams() 方 法 来 动 态 的 设 置 其 宽 高 , 其 中 LayoutParams 是 继 承 于Android.view.viewGroup.LayoutParams,LayoutParams 类是用于 child view(子视图)向 parentview(父视图)传递布局(Layout)信息包,它封装了 Layout 的位置、宽、高等信息。

Bitmap 的内存优化:及时回收 Bitmap 的内存:Bitmap 类有一个方法 recycle( ),用于回收该Bitmap 所占用的内存,当保证某个 Bitmap 不会再被使用(因为 Bitamap 被强制释放后,再次使用它会抛出异常)后,能够在 Activity 的 onStop()方法或 onDestory()方法中将其回收,回收方法 : if (bitmap !=null && !bitmap.isRecycle()) { bitmap.recycle(); bitmap=null; }System.gc( ) ; System.gc()方法可以加快系统回收内存的到来;捕获异常:为了避免应用在分配 Bitmap 内存时出现 OOM 异常以后 Crash 掉,需在对 Bitmap 实例化的过程中进行OutOfMemory 异常的捕获;

缓存通用的 Bitmap 对象:缓存分为硬盘缓存和内存缓存,将常用的 Bitmap 对象放到内存中缓存起来,或将从网络上获取到的数据保存到 SD 卡中。

Bitmap 使用需要注意哪些问题

  • 要选择合适的图片规格(Bitmap 类型):通常我们优化 Bitmap 时,当需要做性能优化或者防止 OOM,我们通常会使用 RGB_565,因为 ALPHA_8 只有透明度,显示一般图片没有意义Bitmap.Config.ARGB_4444 显示图片不清楚,Bitmap.Config.ARGB_8888 占用内存最多。
    1. ALPHA_8 每个像素占用 1byte 内存
    2. ARGB_4444 每个像素占用 2byte 内存
    3. ARGB_8888 每个像素占用 4byte 内存(默认)
    4. RGB_565 每个像素占用 2byte 内存
  • 降 低 采 样 率 : BitmapFactory.Options 参 数 inSampleSize 使 用 , 先 把Options.inJustDecodeBounds 设为 true,只是去读取图片的大小,在拿到图片的大小之后和要显示的大小做比较通过calculateInSampleSize()函数计算 inSampleSize 的具体值,得到值之后。options.inJustDecodeBounds 设为 false 读图片资源。
  • 复用内存:即通过软引用(内存不够的时候才会回收掉),复用内存块,不需要再重新给这个 Bitmap 申请一块新的内存,避免了一次内存的分配和回收,从而改善了运行效率。
  • 使用 recycle()方法及时回收内存。
  • 压缩图片。

Bitmap.recycle()会立即回收么?什么时候会回收?如果没有地方使用Bitmap,为什么 GC 不会立即回收

通过源码可以了解到,加载 Bitmap 到内存里以后,是包含两部分内存区域的。简单的说,一部分是 Java 部分的,一部分是 C 部分的。这个 Bitmap 对象是由 Java 部分分配的,不用的时候系统就会自动回收了。但是那个对应的 C 可用的内存区域,虚拟机是不能直接回收的,这个只能调用底层的功能释放。所以需要调用 recycle()方法来释放 C 部分的内存。Bitmap.recycle()方法用于回收该 Bitmap 所占用的内存,接着将 Bitmap 置空,最后使用System.gc()调用一下系统的垃圾回收器进行回收,调用 System.gc()并不能保证立即开始进行回收过程,而只是为了加快回收的到来。

Bitmap 里有两个获取内存占用大小的方法

getByteCount():API12 加入,代表存储 Bitmap 的像素需要的最少内存。
getAllocationByteCount():API19 加入,代表在内存中为 Bitmap 分配的内存大小,代替了getByteCount()方法。
在不复用 Bitmap 时,getByteCount()和 getAllocationByteCount 返回的结果是一样的。在通过复用 Bitmap 来解码图片时,那么 getByteCount()表示新解码图片占用内存的大小,getAllocationByteCount()表示被复用 Bitmap 真实占用的内存大小

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