Android 面试(2020 )

final关键字

可以用来修饰类、方法和变量(包括成员变量和局部变量)

1.修饰类

当用final修饰一个类时,表明这个类不能被继承

2.修饰方法

“使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义(重写)

因此,如果只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。

注:类的private方法会隐式地被指定为final方法。

3.修饰变量

对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

linklist 和 arraylist

ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。
相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。
LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。

一次完整的HTTP网络请求过程详解

域名解析

TCP三次握手
(客户端发出同步请求报文。
服务端接收到请求报文之后发出同步确认报文。
客服端收到同步确认报文之后发现确认报文,连接建立。)

建立TCP发起HTTP请求

服务器响应HTTP请求

浏览器解析html代码

同时请求html代码中的资源(如js、css、图片等)

总结Https的通信流程 Client发起请求

Server端响应请求,并在之后将证书发送至Client

Client使用认证机构的共钥认证证书,并从证书中取出Server端公钥。

Client使用共钥加密一个随机秘钥,并传到Server

Server使用私钥解密出随机秘钥

通信双方使用随机秘钥最为对称秘钥进行加密解密。

常见线程池

Android中的线程形态有传统的Thread,AsyncTask,HandlerThread和IntentService。

AsyncTask

AsyncTask封装了Thread和Handler,必须在主线程进行调用,它可以在子线程中执行任务,然后将执行的结果传递给主线程并更新UI。但AsyncTask并不适合执行特别耗时的任务。

HandlerThread

HandlerThread继承了Thread,能够使用Handler,实现简单,节省系统资源开销。 实现如下:

HandlerThread thread = new HandlerThread("MyHandlerThread");
thread.start();
mHandler = new Handler(thread.getLooper());
mHandler.post(new Runnable(){...});

IntentService

IntentService是特殊的Service,继承了Service,因为IntentService是一个抽象类,所以必须创建IntentService的子类才能使用。
同时,IntentService是服务,所以在执行时,优先级较高。
IntentService封装了HandlerThread和Handler

线程池分为四种不同的功能

分别是FixedThreadPool、CachedThreadPool、ScheduledThreadPool、SingkeThreadExecutor。

FixedThreadPool
一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。

CachedThreadPool
一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务。

ScheduledThreadPool
可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。

SingkeThreadExecutor 一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。

网络

网络由下往上分为:

物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

HTTP本身是一个协议,但其最终还是基于TCP的
IP 协议对应于网络层,TCP协议对应于传输层,HTTP协议对应于应用层,三者从本质上来说没有可比性,socket则是对TCP/IP协议的封装和应用。

可以说,TPC/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据

TCP是面向链接的,虽然说网络的不安全不稳定特性决定了多少次握手都不能保证连接的可靠性,

但TCP的三次握手在最低限度上(实际上也很大程度上保证了)保证了连接的可靠性;

而UDP不是面向连接的,UDP是无连接的、不可靠的一种数据传输协议,UDP传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,

发送端不知道数据是否会正确接收,当然也不用重发,所以说
也正由于上面的特点,使得UDP的开销更小数据传输速率更高,因为不必进行收发数据的确认,所以UDP的实时性更好。

TCP连接的三次握手:

1、客户端发送报文到服务器 进入并进入SYN_SEND状态 等待服务器确认
2、服务器确认收到报文,同时自己发送报文,此时服务器进入SYN_RECV状态
3、客户端收到服务器的报文,向服务器发送确认包,此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手

类加载过程,双亲委派模型

Java中类加载分为3个步骤:加载、链接、初始化。
加载。加载是将字节码数据从不同的数据源读取到JVM内存,并映射为JVM认可的数据结构,也就是Class对象的过程。数据源可以是Jar文件、Class文件等等。如果数据的格式并不是ClassFile的结构,则会报ClassFormatError。
链接。链接是类加载的核心部分,这一步分为3个步骤:验证、准备、解析。
验证。验证是保证JVM安全的重要步骤。JVM需要校验字节信息是否符合规范,避免恶意信息和不规范数据危害JVM运行安全。如果验证出错,则会报VerifyError。
准备。这一步会创建静态变量,并为静态变量开辟内存空间。
解析。这一步会将符号引用替换为直接引用。
初始化。初始化会为静态变量赋值,并执行静态代码块中的逻辑。
双亲委派模型。
类加载器大致分为3类:启动类加载器、扩展类加载器、应用程序类加载器。
启动类加载器主要加载 jre/lib下的jar文件。
扩展类加载器主要加载 jre/lib/ext 下的jar文件。
应用程序类加载器主要加载 classpath下的文件。
所谓的双亲委派模型就是当加载一个类时,会优先使用父类加载器加载,当父类加载器无法加载时才会使用子类加载器去加载。这么做的目的是为了避免类的重复加载。

sleep和wait、yield的区别

sleep方法是Thread类中的静态方法,wait是Object类中的方法
sleep并不会释放同步锁,而wait会释放同步锁
sleep可以在任何地方使用,而wait只能在同步方法或者同步代码块中使用
sleep中必须传入时间,而wait可以传,也可以不传,不传时间的话只有notify或者notifyAll才能唤醒,传时间的话在时间之后会自动唤醒
它仅仅释放线程所占有的CPU资源,从而让其他线程有机会运行,但是并不能保证某个特定的线程能够获得CPU资源

join的用法

join方法通常是保证线程间顺序调度的一个方法,它是Thread类中的方法。比方说在线程A中执行线程B.join(),这时线程A会进入等待状态,直到线程B执行完毕之后才会唤醒,继续执行A线程中的后续方法。
join方法可以传时间参数,也可以不传参数,不传参数实际上调用的是join(0)。它的原理其实是使用了wait方法

volatile和synchronized的区别

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

Thread和Runnable的联系和区别

继承Thread类,重写Thread的run()方法
实现Runnable接口,重写Runnable的run()方法,并将其作为参数实例化Thread

两者的联系
1、Thread类实现了Runnable接口
2、都需要重写里面run()方法
两者的区别
1、实现Runnable的类更具有健壮性,避免了单继承的局限
2、Runnable更容易实现资源共享,能多个线程同时处理一个资源

final、finally、finalize区别

final可以修饰类、变量和方法。修饰类代表这个类不可被继承。修饰变量代表此变量不可被改变。修饰方法表示此方法不可被重写(override)。
finally是保证重点代码一定会执行的一种机制。通常是使用try-finally或者try-catch-finally来进行文件流的关闭等操作。
finalize是Object类中的一个方法,它的设计目的是保证对象在垃圾收集前完成特定资源的回收。finalize机制现在已经不推荐使用,并且在JDK 9已经被标记为deprecated。

Java中引用类型的区别,具体的使用场景

Java中引用类型分为四类:强引用、软引用、弱引用、虚引用。
强引用:强引用指的是通过new对象创建的引用,垃圾回收器即使是内存不足也不会回收强引用指向的对象。
软引用:软引用是通过SoftRefrence实现的,它的生命周期比强引用短,在内存不足,抛出OOM之前,垃圾回收器会回收软引用引用的对象。软引用常见的使用场景是存储一些内存敏感的缓存,当内存不足时会被回收。
弱引用:弱引用是通过WeakRefrence实现的,它的生命周期比软引用还短,GC只要扫描到弱引用的对象就会回收。弱引用常见的使用场景也是存储一些内存敏感的缓存。
虚引用:虚引用是通过FanttomRefrence实现的,它的生命周期最短,随时可能被回收。如果一个对象只被虚引用引用,我们无法通过虚引用来访问这个对象的任何属性和方法。它的作用仅仅是保证对象在finalize后,做某些事情。虚引用常见的使用场景是跟踪对象被垃圾回收的活动,当一个虚引用关联的对象被垃圾回收器回收之前会收到一条系统通知。

Exception和Error的区别

Exception和Error都继承于Throwable,在Java中,只有Throwable类型的对象才能被throw或者catch,它是异常处理机制的基本组成类型。
Exception和Error体现了Java对不同异常情况的分类。Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应的处理。
Error是指在正常情况下,不大可能出现的情况,绝大部分Error都会使程序处于非正常、不可恢复的状态。既然是非正常,所以不便于也不需要捕获,常见的OutOfMemoryError就是Error的子类。
Exception又分为checked Exception和unchecked Exception。checked Exception在代码里必须显式的进行捕获,这是编译器检查的一部分。unchecked Exception也就是运行时异常,类似空指针异常、数组越界等,通常是可以避免的逻辑错误,具体根据需求来判断是否需要捕获,并不会在编译器强制要求。

Android性能优化

Android中的性能优化在我看来分为以下几个方面:内存优化、布局优化、网络优化、启动优化、安装包优化。

内存优化:下一个问题就是。
布局优化:布局优化的本质就是减少View的层级。常见的布局优化方案如下

在LinearLayout和RelativeLayout都可以完成布局的情况下优先选择RelativeLayout,可以减少View的层级
将常用的布局组件抽取出来使用 < include > 标签
通过 < ViewStub > 标签来加载不常用的布局
使用 < Merge > 标签来减少布局的嵌套层次

网络优化:常见的网络优化方案如下

尽量减少网络请求,能够合并的就尽量合并
避免DNS解析,根据域名查询可能会耗费上百毫秒的时间,也可能存在DNS劫持的风险。可以根据业务需求采用增加动态更新IP的方式,或者在IP方式访问失败时切换到域名访问方式。
大量数据的加载采用分页的方式
网络数据传输采用GZIP压缩
加入网络数据的缓存,避免频繁请求网络
上传图片时,在必要的时候压缩图片

安装包优化:安装包优化的核心就是减少apk的体积,常见的方案如下

使用混淆,可以在一定程度上减少apk体积,但实际效果微乎其微
减少应用中不必要的资源文件,比如图片,在不影响APP效果的情况下尽量压缩图片,有一定的效果
在使用了SO库的时候优先保留v7版本的SO库,删掉其他版本的SO库。原因是在2018年,v7版本的SO库可以满足市面上绝大多数的要求,可能八九年前的手机满足不了,但我们也没必要去适配老掉牙的手机。实际开发中减少apk体积的效果是十分显著的,如果你使用了很多SO库,比方说一个版本的SO库一共10M,那么只保留v7版本,删掉armeabi和v8版本的SO库,一共可以减少20M的体积。

启动优化

异步加载了。但是并不是所有的都可以进行异步处理。这里分情况给出一些建议:
1、比如像友盟,bugly这样的业务非必要的可以的异步加载。
2、比如地图,推送等,非第一时间需要的可以在主线程做延时启动。当程序已经启动起来之后,在进行初始化。
3、对于图片,网络请求框架必须在主线程里初始化了。

Android内存优化

Android的内存优化在我看来分为两点:避免内存泄漏、扩大内存,其实就是开源节流。
其实内存泄漏的本质就是较长生命周期的对象引用了较短生命周期的对象。
常见的内存泄漏:
单例模式导致的内存泄漏。最常见的例子就是创建这个单例对象需要传入一个Context,这时候传入了一个Activity类型的Context,由于单例对象的静态属性,导致它的生命周期是从单例类加载到应用程序结束为止,所以即使已经finish掉了传入的Activity,由于我们的单例对象依然持有Activity的引用,所以导致了内存泄漏。解决办法也很简单,不要使用Activity类型的Context,使用Application类型的Context可以避免内存泄漏。
静态变量导致的内存泄漏。静态变量是放在方法区中的,它的生命周期是从类加载到程序结束,可以看到静态变量生命周期是非常久的。最常见的因静态变量导致内存泄漏的例子是我们在Activity中创建了一个静态变量,而这个静态变量的创建需要传入Activity的引用this。在这种情况下即使Activity调用了finish也会导致内存泄漏。原因就是因为这个静态变量的生命周期几乎和整个应用程序的生命周期一致,它一直持有Activity的引用,从而导致了内存泄漏。
非静态内部类导致的内存泄漏。非静态内部类导致内存泄漏的原因是非静态内部类持有外部类的引用,最常见的例子就是在Activity中使用Handler和Thread了。使用非静态内部类创建的Handler和Thread在执行延时操作的时候会一直持有当前Activity的引用,如果在执行延时操作的时候就结束Activity,这样就会导致内存泄漏。解决办法有两种:第一种是使用静态内部类,在静态内部类中使用弱引用调用Activity。第二种方法是在Activity的onDestroy中调用handler.removeCallbacksAndMessages来取消延时事件。
使用资源未及时关闭导致的内存泄漏。常见的例子有:操作各种数据流未及时关闭,操作Bitmap未及时recycle等等。
使用第三方库未能及时解绑。有的三方库提供了注册和解绑的功能,最常见的就是EventBus了,我们都知道使用EventBus要在onCreate中注册,在onDestroy中解绑。如果没有解绑的话,EventBus其实是一个单例模式,他会一直持有Activity的引用,导致内存泄漏。同样常见的还有RxJava,在使用Timer操作符做了一些延时操作后也要注意在onDestroy方法中调用disposable.dispose()来取消操作。
属性动画导致的内存泄漏。常见的例子就是在属性动画执行的过程中退出了Activity,这时View对象依然持有Activity的引用从而导致了内存泄漏。解决办法就是在onDestroy中调用动画的cancel方法取消属性动画。
WebView导致的内存泄漏。WebView比较特殊,即使是调用了它的destroy方法,依然会导致内存泄漏。其实避免WebView导致内存泄漏的最好方法就是让WebView所在的Activity处于另一个进程中,当这个Activity结束时杀死当前WebView所处的进程即可,我记得阿里钉钉的WebView就是另外开启的一个进程,应该也是采用这种方法避免内存泄漏。
扩大内存,为什么要扩大我们的内存呢?有时候我们实际开发中不可避免的要使用很多第三方商业的SDK,这些SDK其实有好有坏,大厂的SDK可能内存泄漏会少一些,但一些小厂的SDK质量也就不太靠谱一些。那应对这种我们无法改变的情况,最好的办法就是扩大内存。
扩大内存通常有两种方法:一个是在清单文件中的Application下添加largeHeap="true"这个属性,另一个就是同一个应用开启多个进程来扩大一个应用的总内存空间。第二种方法其实就很常见了,比方说我使用过个推的SDK,个推的Service其实就是处在另外一个单独的进程中。
Android中的内存优化总的来说就是开源和节流,开源就是扩大内存,节流就是避免内存泄漏。

Binder机制

在Linux中,为了避免一个进程对其他进程的干扰,进程之间是相互独立的。在一个进程中其实还分为用户空间和内核空间。这里的隔离分为两个部分,进程间的隔离和进程内的隔离。
既然进程间存在隔离,那其实也是存在着交互。进程间通信就是IPC,用户空间和内核空间的通信就是系统调用。
Linux为了保证独立性和安全性,进程之间不能直接相互访问,Android是基于Linux的,所以也是需要解决进程间通信的问题。
其实Linux进程间通信有很多方式,比如管道、socket等等。为什么Android进程间通信采用了Binder而不是Linux已有的方式,主要是有这么两点考虑:性能和安全
性能。在移动设备上对性能要求是比较严苛的。Linux传统的进程间通信比如管道、socket等等进程间通信是需要复制两次数据,而Binder则只需要一次。所以Binder在性能上是优于传统进程通信的。
安全。传统的Linux进程通信是不包含通信双方的身份验证的,这样会导致一些安全性问题。而Binder机制自带身份验证,从而有效的提高了安全性。
Binder是基于CS架构的,有四个主要组成部分。
Client。客户端进程。
Server。服务端进程。
ServiceManager。提供注册、查询和返回代理服务对象的功能。
Binder驱动。主要负责建立进程间的Binder连接,进程间的数据交互等等底层操作。
Binder机制主要的流程是这样的:
服务端通过Binder驱动在ServiceManager中注册我们的服务。
客户端通过Binder驱动查询在ServiceManager中注册的服务。
ServiceManager通过Binder驱动返回服务端的代理对象。
客户端拿到服务端的代理对象后即可进行进程间通信。

图片加载如何避免OOM

我们知道内存中的Bitmap大小的计算公式是:长所占像素 * 宽所占像素 * 每个像素所占内存。想避免OOM有两种方法:等比例缩小长宽、减少每个像素所占的内存。
等比缩小长宽。我们知道Bitmap的创建是通过BitmapFactory的工厂方法,decodeFile()、decodeStream()、decodeByteArray()、decodeResource()。这些方法中都有一个Options类型的参数,这个Options是BitmapFactory的内部类,存储着BItmap的一些信息。Options中有一个属性:inSampleSize。我们通过修改inSampleSize可以缩小图片的长宽,从而减少BItmap所占内存。需要注意的是这个inSampleSize大小需要是2的幂次方,如果小于1,代码会强制让inSampleSize为1。
减少像素所占内存。Options中有一个属性inPreferredConfig,默认是ARGB_8888,代表每个像素所占尺寸。我们可以通过将之修改为RGB_565或者ARGB_4444来减少一半内存。

Android5.0~10.0之间大的变化

MaterialDesign设计风格
支持64位ART虚拟机(5.0推出的ART虚拟机,在5.0之前都是Dalvik。他们的区别是:Dalvik,每次运行,字节码都需要通过即时编译器转换成机器码(JIT)。ART,第一次安装应用的时候,字节码就会预先编译成机器码(AOT))
通知详情可以用户自己设计

Android 6.0新特性
动态权限管理
支持快速充电的切换
支持文件夹拖拽应用
相机新增专业模式

Android 7.0新特性
多窗口支持
V2签名
增强的Java8语言模式
夜间模式

Android 8.0(O)新特性
优化通知:通知渠道 (Notification Channel) 通知标志 休眠 通知超时 通知设置 通知清除
画中画模式:清单中Activity设置android:supportsPictureInPicture
后台限制
自动填充框架
系统优化等等优化很多

Android 9.0(P)新特性
室内WIFI定位
“刘海”屏幕支持
安全增强等等优化很多

Android 10.0(Q)新特性
夜间模式:包括手机上的所有应用都可以为其设置暗黑模式。
桌面模式:提供类似于PC的体验,但是远远不能代替PC。
屏幕录制:通过长按“电源”菜单中的"屏幕快照"来开启。

Android中常用布局分为传统布局和新型布局

传统布局(编写XML代码、代码生成):
框架布局(FrameLayout):
线性布局(LinearLayout):
绝对布局(AbsoluteLayout):
相对布局(RelativeLayout):
表格布局(TableLayout):
约束布局(ConstrainLayout)

Glide的设计微妙在于:

Glide的生命周期绑定:可以控制图片的加载状态与当前页面的生命周期同步,使整个加载过程随着页面的状态而启动/恢复,停止,销毁
Glide的缓存设计:通过(三级缓存,Lru算法,Bitmap复用)对Resource进行缓存设计
Glide的完整加载过程:采用Engine引擎类暴露了一系列方法供Request操作

A 启动 B,A 和 B 的生命周期函数调用顺序?

A onPause
B onCreate
B onStart
B onResume
A onStop

单个 Activity + Fragment 的启动?

onCreate onStart onResume onPause onStop onDestry

onAttech onCreate onCreateView onActivityCreated onStart onResume onPause onStop onDestryView onDestry onDetach

Java基础- ==和equals和hashCode的区别

对于==,如果作用于基本数据类型,则直接比较其存储的“值”是否相等,如果作用于引用类型的变量,则比较的是所指向的对象的地址。
对于 equals 方法,注意:equals不能作用于基本数据类型,如果没有对equals进行重写,则比较的是 引用类所指向的地址。如果重写了,比较的就是对象的内容。
hashCode用来鉴定两个对象是否相等,Object类中的hashCode方法返回对象在内存中地址转换成的一个int值,所以如果没有重写hashCode方法,任何对象的hashCode方法是不相等的。

int、char、long 各占多少字节数

byte 是 字节
bit 是 位
1 byte = 8 bit
char在java中是2个字节,java采用unicode,2个字节来表示一个字符
short 2个字节
int 4个字节
long 8个字节
float 4个字节
double 8个字节

int 与 integer 区别

Integer是int的包装类,int则是java的一种基本数据类型。
Integer变量必须实例化后才能使用,而int变量不需要。
Integer是对象的引用,当new一个Integer时,实际上生成一个指针指向此对象,而int则是直接存储数据值。
Integer默认值是null,int的默认值是0。

String、StringBuffer、StringBuilder区别

String:
字符串常量 不适用于经常要改变值得情况,每次改变相当于生成一个新的对象

StringBuffer:
字符串变量 (线程安全)

StringBuilder:
字符串变量(线程不安全) 确保单线程下可用,效率略高于StringBuffer

进程和线程的区别

进程是cpu资源分配的最小单位,
线程是cpu调度的最小单位。

进程之间不能共享资源,而线程共享所在进程的地址空间和其它资源。

一个进程内可拥有多个线程,进程可开启进程,也可开启线程。

一个线程只能属于一个进程,线程可直接使用同进程的资源,线程依赖于进程而存在。

静态属性和静态方法是否可以被继承?是否可以被重写?以及原因?

可继承 不可重写 而是被隐藏

如果子类里面定义了静态方法和属性,那么这时候父类的静态方法或属性称之为"隐藏"。如果你想要调用父类的静态方法和属性,直接通过父类名.方法或变量名完成。

Java中实现多态的机制是什么?

方法的重写Overriding和重载Overloading是Java多态性的不同表现

重写
Overriding是父类与子类之间多态性的一种表现

重载
Overloading是一个类中多态性的一种表现.

Object类的equal和hashCode方法重写,为什么?

首先equals与hashcode间的关系是这样的:

如果两个对象相同(即用equals比较返回true),那么它们的hashCode值一定要相同;

如果两个对象的hashCode相同,它们并不一定相同(即用equals比较返回false)。

由于为了提高程序的效率才实现了hashcode方法,先进行hashcode的比较,如果不同,那没就不必在进行equals的比较了,这样就大大减少了equals比较的次数,这对比需要比较的数量很大的效率提高是很明显的

List,Set,Map的区别

Set
是最简单的一种集合。集合中的对象不按特定的方式排序,并且没有重复对象。 Set接口主要实现了两个实现类:
HashSet:
HashSet类按照哈希算法来存取集合中的对象,存取速度比较快

TreeSet :
TreeSet类实现了SortedSet接口,能够对集合中的对象进行排序。

List
是其元素以线性方式存储,集合中可以存放重复对象。

ArrayList() :
代表长度可以改变得数组。可以对元素进行随机的访问,向ArrayList()中插入与删除元素的速度慢。

LinkedList():
在实现中采用链表数据结构。插入和删除速度快,访问速度慢。

Map
是一种把键对象和值对象映射的集合,它的每一个元素都包含一对键对象和值对象。 Map没有继承于Collection接口 从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。

HashMap:
Map基于散列表的实现。插入和查询键值对的开销是固定的。可以通过构造器设置容量capacity和负载因子load factor,以调整容器的性能。

LinkedHashMap:
类似于HashMap,但是迭代遍历它时,取得键值对的顺序是其插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点。而在迭代访问时发而更快,因为它使用链表维护内部次序。

TreeMap :
基于红黑树数据结构的实现。查看键或键值对时,它们会被排序(次序由Comparabel或Comparator决定)。TreeMap的特点在 于,你得到的结果是经过排序的。TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子树。

WeakHashMao :
弱键(weak key)Map,Map中使用的对象也被允许释放: 这是为解决特殊问题设计的。如果没有map之外的引用指向某个“键”,则此“键”可以被垃圾收集器回收。

HashMap和HashTable的区别

HashMap
不是线程安全的,效率高一点、方法不是Synchronize的要提供外同步,有containsvalue和containsKey方法。

hashtable
是线程安全,不允许有null的键和值,效率稍低,方法是是Synchronize的。有contains方法方法。Hashtable继承于Dictionary 类

HashSet与HashMap怎么判断集合元素重复?

HashSet
不能添加重复的元素,当调用add(Object)方法时候,

首先会调用Object的hashCode方法判hashCode是否已经存在,如不存在则直接插入元素;如果已存在则调用Object对象的equals方法判断是否返回true,如果为true则说明元素已经存在,如为false则插入元素。

run()和start()方法区别

这个问题经常被问到,但还是能从此区分出面试者对Java线程模型的理解程度。start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

如何控制某个方法允许并发访问线程的个数?

semaphore.acquire()请求一个信号量,这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)

semaphore.release()释放一个信号量,此时信号量个数+1

谈谈wait/notify关键字的理解

等待对象的同步锁,需要获得该对象的同步锁才可以调用这个方法,否则编译可以通过,但运行时会收到一个异常:IllegalMonitorStateException。

调用任意对象的 wait()方法导致该线程阻塞,该线程不可继续执行,并且该对象上的锁被释放。

唤醒在等待该对象同步锁的线程(只唤醒一个,如果有多个在等待),注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。

调用任意对象的notify()方法则导致因调用该对象的wait()方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。

如何保证线程安全?

1.synchronized;

2.Object方法中的wait,notify;

3.ThreadLocal机制 来实现的。

Java中堆和栈有什么不同?

为什么把这个问题归类在多线程和并发面试题里?因为栈是一块和线程紧密相关的内存区域。每个线程都有自己的栈内存,用于存储本地变量,方法参数和栈调用,一个线程中存储的变量对其它线程是不可见的。而堆是所有线程共享的一片公用内存区域。对象都在堆里创建,为了提升效率线程会从堆中弄一个缓存到自己的栈,如果多个线程使用该变量就可能引发问题,这时volatile变量就可以发挥作用了,它要求线程从主存中读取变量的值。
1.堆内存用来存放由new创建的对象和数组。
2.栈内存用来存放方法或者局部变量等
3.堆是先进先出,后进后出
4.栈是后进先出,先进后出

相同点:

1.都是属于Java内存的一种
2.系统都会自动去回收它,但是对于堆内存一般开发人员会自动回收它

有三个线程T1,T2,T3,怎么确保它们按顺序执行?

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。

同步和异步的区别 阻塞和非阻塞的区别

同步/异步关注的是消息通信机制 (synchronous communication/ asynchronous communication) 。

所谓同步,就是在发出一个调用时,在没有得到结果之前, 该调用就不返回。
异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果
阻塞/非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

泛型

通配符的出现是为了指定泛型中的类型范围。
<?>被称作无限定的通配符:<?>提供了只读的功能
<? extends T>被称作有上限的通配符:代表类型 T 及 T 的子类
<? super T>被称作有下限的通配符:表示包括T在内的任何T的父类

泛型擦除

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
打印的结果为 true 是因为 List<String>和 List<Integer>在 jvm 中的 Class 都是 List.class。泛型信息被擦除了
利用反射,我们绕过编译器去调用
List<Integer>[] li2 = new ArrayList<Integer>[];
List<Boolean> li3 = new ArrayList<Boolean>[];
这两行代码是无法在编译器中编译通过的。原因还是类型擦除带来的影响。
List<?>[] li3 = new ArrayList<?>[10];
li3[1] = new ArrayList<String>();
List<?> v = li3[1];
借助于无限定通配符却可以

SparseArray和ListArray的区别

SparseArray是android里为<Interger,Object>这样的Hashmap而专门写的类,目的是提高内存效率,其核心是折半查找函数

SparseArray有两个优点:1.避免了自动装箱
2.数据结构不会依赖于外部对象映射。我们知道HashMap 采用一种所谓的“Hash 算法”来决定每个元素的存储位置,存放的都是数组元素的引用,通过每个对象的 hash值来映射对象。而SparseArray则是用数组数据结构来保存映射,然后通过折半查找来找到对象。但其实一般来说,SparseArray执行效率比HashMap要慢一点,因为查找需要折半查找,而添加删除则需要在数组中执行,而HashMap都是通过外部映射。但相对来说影响不大,最主要是SparseArray不需要开辟内存空间来额外存储外部映射,从而节省内存。
List特点:元素有放入顺序,元素可重复
Map特点:元素按键值对存储,无放入顺序
Set特点:元素无放入顺序,元素不可重复(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的)

Java容器特点

List接口有三个实现类:LinkedList,ArrayList,Vector
LinkedList:底层基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还存储下一个元素的地址。链表增删快,查找慢
ArrayList和Vector的区别:ArrayList是非线程安全的,效率高;Vector是基于线程安全的,效率低
Set接口有两个实现类:HashSet(底层由HashMap实现),LinkedHashSet
SortedSet接口有一个实现类:TreeSet(底层由平衡二叉树实现)
Query接口有一个实现类:LinkList
Map接口有三个实现类:HashMap,HashTable,LinkeHashMap
HashMap非线程安全,高效,支持null;HashTable线程安全,低效,不支持null
SortedMap有一个实现类:TreeMap
其实最主要的是,list是用来处理序列的,而set是用来处理集的。Map是知道的,存储的是键值对
set 一般无序不重复.map kv 结构 list 有序

Gradle插件怎么做
https怎么实现防抓包

服务器配置的证书被打包到了客户端程序,客户端校验服务器返回的https证书的时候会先和本地证书做匹配。

做证书检测,禁止中间人代理 ssl pining 证书验证,通过引入自定义证书,然后给OkHttp设置sslSocketFactory可以有效的防止抓包,但是.cer放到assets下很容易被反编译,可以通过jdk下的命令keytool -printcert -rfc -file srca.cer导出字符串,然后通过

怎么中断一个线程?
public class InterruptedDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread() {
 
            @Override
            public void run() {
                while(true) {
                    System.out.println("The thread is waiting for interrupted!");
                    //中断处理逻辑
                    if(Thread.currentThread().isInterrupted()) {
                        System.out.println("The thread is interrupted!");
                        break;
                    }
                    //Thread.yield();
                }
            }
        };
        t1.start();
        t1.interrupt();//中断线程
        //System.out.println("The Thread is interrupted!");
    }
}

因此,通过上面的代码就可以看到,使用Thread.interrupt()方法处理现场中断,需要使用Thread.isInterrupted()判断线程是否被中断,然后进入中断处理逻辑代码。

AsyncTask原理

AsyncTask封装了Thread和Handler

onPreExecute
在主线程执行,在异步任务执行之前被调用,一般用于一些准备工作
doInBackground
在线程池中执行,此方法用于执行异步任务,params参数表示异步任务的输入参数,在此方法中可以通过publishProgress方法来更新任务的进度,publishProgress会调用onProgressUpdate方法,此外此方法需要返回计算结果给onPostExecute
onProgressUpdate
在主线程执行,异步任务结束后调用,返回结果
onPostExecute
在主线程中执行,在异步任务结束后,此方法被调用,其中result参数是后台任务的返回值

OkHttp原理(说了那些拦截器)怎么实现多路复用的

https://juejin.im/post/5e69a4bf6fb9a07cd74f6ab8

Handler的机制,android内部是怎么实现发送延时消息

用系统开机时间+延时时间得到一个时间T1,当手机当前时间到了T1的话,就会把消息发送出去。但有可能UI线程被阻塞了,所以到了T1时间,也不能确保100%这个Message被发出去的

ViewStub为什么能实现延迟加载

它没有进行测量和绘制,只在调flate或是setVisibility时才会加载进布局

HashMap、HashTable、ConCurrentHashMap区别比较

(1)HashMap是非线程安全的,HashTable是线程安全的。
(2)HashMap的键和值都允许有null存在,而HashTable则都不行。
(3)因为线程安全、哈希效率的问题,HashMap效率比HashTable的要高。
它是线程安全的HashMap的实现。

HashTable里使用的是synchronized关键字,这其实是对对象加锁,锁住的都是对象整体,当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间

ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的syn关键字锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。

(2)HashMap的键值对允许有null,但是ConCurrentHashMap都不允许

synchronized 和 volatile 、ReentrantLock 、CAS 的区别
JVM 类加载机制、垃圾回收算法对比、Java 虚拟机结构等

https://juejin.im/post/5d22f4e8e51d455a68490bf4

Java 线程有哪些状态,有哪些锁,各种锁的区别

https://blog.csdn.net/songzi1228/article/details/100535928

TCP 有哪些状态。

https://zhuanlan.zhihu.com/p/81144898

浏览器输入一个 URL,按下回车网络传输的流程?

URL 解析
DNS 查询
TCP 连接
处理请求
接受响应
渲染页面

Router 原理,如何实现组件间通信,组件化平级调用数据方式
app打包

1、通过aapt打包res资源文件,生成R.java、resources.arsc和res文件(二进制 & 非二进制如res/raw和pic保持原样)
2、处理.aidl文件,生成对应的Java接口文件
3、通过Java Compiler编译R.java、Java接口文件、Java源文件,生成.class文件
4、通过dex命令,将.class文件和第三方库中的.class文件处理生成classes.dex
5、通过apkbuilder工具,将aapt生成的resources.arsc和res文件、assets文件和classes.dex一起打包生成apk
6、通过Jarsigner工具,对上面的apk进行debug或release签名
7、通过zipalign工具,将签名后的apk进行对齐处理。

APP 启动流程;

https://juejin.im/post/5d9d948de51d45782c23fabc
Launcher进程请求AMS
AMS发送创建应用进程请求
Zygote进程接受请求并孵化应用进程
应用进程启动ActivityThread

Java中线程的状态
  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。
Android事件分发机制A 嵌套 B ,B 嵌套 C,从 C 中心按下,一下滑出到 A,事件分发的过程
A -> onInterceptTouchEvent
B -> dispatchTouchEvent
B -> onInterceptTouchEvent
C -> dispatchTouchEvent
C -> onInterceptTouchEvent
C -> onTouchEvent ACTION_DOWN
B -> onTouchEvent ACTION_DOWN
A -> onTouchEvent ACTION_DOWN

假设我们在View B的onTouchEvent中返回true

A -> dispatchTouchEvent
A -> onInterceptTouchEvent
B -> dispatchTouchEvent
B -> onInterceptTouchEvent
C -> dispatchTouchEvent
C -> onInterceptTouchEvent
C -> onTouchEvent ACTION_DOWN
B -> onTouchEvent ACTION_DOWN

假设我们在View B的onInterceptTouchEvent中返回true

A -> dispatchTouchEvent
A -> onInterceptTouchEvent
B -> dispatchTouchEvent
B -> onInterceptTouchEvent
B -> onTouchEvent ACTION_DOWN
A -> onTouchEvent ACTION_DOWN

https://www.jianshu.com/p/46bbe7a0f994

卡顿的底层原理是什么?如何理解16毫秒刷新一次?假如界面没有更新操作,View会每16毫秒draw一次吗?

卡顿是由于主线程有耗时操作,导致View绘制掉帧,屏幕每16毫秒会刷新一次,也就是每秒会刷新60次,人眼能感觉到卡顿的帧率是每秒24帧。所以解决卡顿的办法就是:耗时操作放到子线程、View的层级不能太多、要合理使用include、ViewStub标签等等这些,来保证每秒画24帧以上。

自定义View

View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定View的测量宽/高,layout确定View的最终宽/高和四个顶点的位置,而draw则将View绘制到屏幕上
绘制背景 background.draw(canvas)
绘制自己(onDraw)
绘制 children(dispatchDraw)
绘制装饰(onDrawScollBars
https://juejin.im/post/5bab9f85e51d450e452abdaf

invalidate()和postInvalidate()的区别 ?

invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用需要配合handler;而postInvalidate()可在子线程中直接调用。
SurfaceView和View的区别?
surfaceView是在一个新起的单独线程中可以重新绘制画面而View必须在UI的主线程中更新画面

Scroller是怎么实现View的弹性滑动?

在MotionEvent.ACTION_UP事件触发时调用startScroll()方法,该方法并没有进行实际的滑动操作,而是记录滑动相关量(滑动距离、滑动时间)
接着调用invalidate/postInvalidate()方法,请求View重绘,导致View.draw方法被执行
当View重绘后会在draw方法中调用computeScroll方法,而computeScroll又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法来进行第二次重绘,和之前流程一样,如此反复导致View不断进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,直到整个滑动过成结束

单例为什么要双重检查null
service

https://juejin.im/entry/59ffba72f265da432b4a345d

startService / stopService

生命周期顺序:onCreate->onStartCommand->onDestroy

bindService / unbindService

生命周期顺序:onCreate->onBind->onUnBind->onDestroy
在什么情况下使用 startService 或 bindService 或 同时使用

startService 和 bindService?使用场景

①如果你只是想要启动一个后台服务长期进行某项任务那么使用 startService 便可以了。

②如果你想要与正在运行的 Service 取得联系,那么有两种方法,一种是使用 broadcast ,另外是使用 bindService ,前者的缺点是如果交流较为频繁,容易造成性能上的问题,并且 BroadcastReceiver 本身执行代码的时间是很短的(也许执行到一半,后面的代码便不会执行),而后者则没有这些问题,因此我们肯定选择使用 bindService(这个时候你便同时在使用 startService 和 bindService 了,这在 Activity 中更新 Service 的某些运行状态是相当有用的)。

Service与Thread的区别:

Thread:Thread 是程序执行的最小单元,可以用 Thread 来执行一些异步的操作。

Service:Service 是android的一种机制,当它运行的时候如果是Local Service,那么对应的 Service 是运行在主进程的 main 线程上的。如果是Remote Service,那么对应的 Service 则是运行在独立进程的 main 线程上。

Thread 的运行是独立的,也就是说当一个 Activity 被 finish 之后,如果没有主动停止 Thread 或者 Thread 里的 run 方法没有执行完毕的话,Thread 也会一直执行。因此这里会出现一个问题:当 Activity 被 finish 之后,不再持有该 Thread 的引用,也就是不能再控制该Thread。另一方面,没有办法在不同的 Activity 中对同一 Thread 进行控制。
例如:如果 一个Thread 需要每隔一段时间连接服务器校验数据,该Thread需要在后台一直运行。这时候如果创建该Thread的Activity被结束了而该Thread没有停止,那么将没有办法再控制该Thread,除非kill掉该程序的进程。
这时候如果创建并启动一个 Service ,在 Service 里面创建、运行并控制该 Thread,这样便解决了该问题(因为任何 Activity 都可以控制同一个Service,而系统也只会创建一个对应 Service 的实例)。
因此可以把 Service 想象成一种消息服务,可以在任何有 Context 的地方调用 Context.startService、Context.stopService、Context.bindService、Context.unbindService来控制它,也可以在 Service 里注册 BroadcastReceiver,通过发送 broadcast 来达到控制的目的,这些都是 Thread 做不到的。

EventBus 事件线程切换原理

https://www.jianshu.com/p/3eb7050ca248

双重检验锁
public class Singleton {
    private static Singleton instance;
    private Singleton (){}
 
    public static Singleton getInstance() {
     if (instance == null) {
         instance = new Singleton();
     }
     return instance;
    }
}

这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。


public static Singleton getSingleton() {
    if (instance == null) {                         //Single Checked
        synchronized (Singleton.class) {
            if (instance == null) {                 //Double Checked
                instance = new Singleton();
            }
        }
    }
    return instance ;
}

第一个if 控制阻塞条件 第二个控制实例创建条件

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

给 instance 分配内存
调用 Singleton 的构造函数来初始化成员变量
将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
我们只需要将 instance 变量声明成 volatile 就可以了。

禁止指令重排序优化

单例模式:某个类只能有一个实例,提供一个全局的访问点。

简单工厂:一个工厂类根据传入的参量决定创建出那一种产品类的实例。

工厂方法:定义一个创建对象的接口,让子类决定实例化那个类。

抽象工厂:创建相关或依赖对象的家族,而无需明确指定具体类。

建造者模式:封装一个复杂对象的构建过程,并可以按步骤构造。

原型模式:通过复制现有的实例来创建新的实例。

适配器模式:将一个类的方法接口转换成客户希望的另外一个接口。

组合模式:将对象组合成树形结构以表示“”部分-整体“”的层次结构。

装饰模式:动态的给对象添加新的功能。

代理模式:为其他对象提供一个代理以便控制这个对象的访问。

亨元(蝇量)模式:通过共享技术来有效的支持大量细粒度的对象。

外观模式:对外提供一个统一的方法,来访问子系统中的一群接口。

桥接模式:将抽象部分和它的实现部分分离,使它们都可以独立的变化。

模板模式:定义一个算法结构,而将一些步骤延迟到子类实现。

解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器。

策略模式:定义一系列算法,把他们封装起来,并且使它们可以相互替换。

状态模式:允许一个对象在其对象内部状态改变时改变它的行为。

观察者模式:对象间的一对多的依赖关系。

备忘录模式:在不破坏封装的前提下,保持对象的内部状态。

中介者模式:用一个中介对象来封装一系列的对象交互。

命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化。

访问者模式:在不改变数据结构的前提下,增加作用于一组对象元素的新功能。

责任链模式:将请求的发送者和接收者解耦,使的多个对象都有处理这个请求的机会。

迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构。

Java中父类和子类的代码执行顺序。

1.执行父类静态代码块和静态变量(静态代码块先于静态变量)
2.执行子类静态代码块和静态变量
3.执行父类非静态代码块和非静态变量(静态代码块先于静态变量)
4.执行父类构造函数
5.执行子类非静态代码块和非静态变量
6.执行子类构造函数

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

推荐阅读更多精彩内容