Android知识点总结

一 Activity

1 Activity 生命周期

1.1 Activity 的四种状态

running 当前Activity正在运行,获取焦点
paused 当前Activity处于暂停状态,可见,没有焦点
stopped 当前Activity处于暂停状态,完全不可见,内存里的成员变量和状态信息仍在。
killed 当前Activity被销毁后的状态,成员变量和状态信息被一并回收。

1.2 Activity的生命周期

Activity启动 →onCreate()→onStart()→onResume();
点击home键返回桌面→onPause()→onStop();
再次回到原Activity→ onRestart()→onStart()→onResume();
按返回键退出当前Activity→onPause()→onStop()→onDestroy();

2 Android任务栈

优先级:前台>可见>服务>后台>空
前台:正在与用户进行交互的Activity所在的进程
可见:Activity可见但没有在前台所在的进程
服务:Activity在后台开启了服务所在的进程
后台:Activity完全处于后台所在的进程
空:没有任何Activity存在的进程

3. Activity的启动模式

3.1 为什么需要启动模式?

       每次启动一个Activity都会把对应的要启动的Activity的实例放入任务栈中,加入这个Activity被频繁启动,会产生很多的这个Activity的实例,为了杜绝这种内存浪费的行为,Activity的启动模式被创造出来。

3.2 Activity的启动模式

  • 系统模式模式:standard
           标准模式,也是系统的默认模式,启动一个activity就创建一个activity实例,不管这个实例是否存在,谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity的任务栈中。
  • 栈顶复用模式:singleTop
           在这种模式下,如果新的Activity已经位于栈顶,那么此Activity不会被重新创建,同时它的onNewIntent方法被回调,通过此方法的参数我们可以取出当前的请求信息。需要注意,此Activity的onCreate,onStart方法不会被系统调用。如果新Activity不在栈顶,那么新Activity任然会被重新重建。
  • 栈内复用模式:singleTask
           这是一种单实例模式,只要Activity在一个栈中存在,那么多次启动此Activity都不会重新创建实例,系统也会回调onNewIntent方法。
    例如:当前栈内情况为ABC,此时D被以singleTask的模式被启动,当前栈变为ABCD。
    如果当前栈内情况为ADBC,此时D被以singleTask的模式被启动,当前栈变为AD。
  • 单实例模式:singleInstance
           这是一种加强的单实例模式,它除了具有singleTask模式的所有特性外,还加强了一点,那就是具有此种模式的Activity只能单独位于一个任务栈中,比如Activity A是singleInstance模式,A被启动时系统会为它创建一个新的任务栈,A运行在这个单独的任务栈中,后续的请求均不会再创建A,除非这个单独的任务栈被系统销毁了。

二 Fragment

1. 为什么Fragment被称为第五大组件?

Android中的四大组件为Activity,service,ContentProvider,Broadcast。
Fragment因为有生命周期,使用频率不输于四大组件,可灵活加载到Activity中。

1.1 Fragment加载到Activity的两种方式

  • 静态加载:直接在Activity布局文件中指定Fragment。代码如下
<fragment  
    android:name="com.example.myfragment.MyFragment"  
    android:id="@+id/myfragment_1"  
    android:layout_width="wrap_content"  
    android:layout_height="wrap_content"/> 
  • 动态加载:动态加载需要使用到FragmentManager,这种加载方式在开发中是非常常见的,示例代码如下:
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
//将FragmentA从容器中移除掉,减少内存的消耗
fragmentTransaction.remove(fragmentA);
fragmentTransaction.add(R.id.fragment_layout,new FragmentB());
fragmentTransaction.commit();

1.2 Fragment 与ViewPager搭配使用

       通常情况下我们开发应用最常见的使用情况是TabLayout+ViewPager+Fragment的使用方式,这就涉及到两个常用的适配器的使用,一个是FragmentPagerAdapter,另外一个是FragmentStatePagerAdapter,那么它们之间有什么区别呢?其实很简单,FragmentPagerAdapter适用于页面较少的情况,而FragmentStatePagerAdapter适用于页面较多的情况。

2. Fragment的生命周期

Fragment

界面打开
onCreate() 方法执行!
onCreateView() 方法执行!
onActivityCreated() 方法执行!
onStart() 方法执行!

onResume() 方法执行!

按下主屏幕键/锁屏

onPause() 方法执行!
onStop() 方法执行!

重新打开
onStart() 方法执行!
onResume() 方法执行!

按下后退键
onPause() 方法执行!
onStop() 方法执行!
onDestroyView() 方法执行!
onDestroy() 方法执行!
onDetach() 方法执行!

Activity

打开应用
onCreate() 方法执行!
onStart() 方法执行!
onResume() 方法执行!

按下主屏幕键/锁屏
onPause() 方法执行!
onStop() 方法执行!

重新打开应用
onRestart() 方法执行!
onStart() 方法执行!
onResume() 方法执行!

按下后退键
onPause() 方法执行!
onStop() 方法执行!
onDestroy() 方法执行!

在Activity中加入Fragment,对应的生命周期

打开
Fragment onAttach()方法执行
Fragment onCreate() 方法执行!
Fragment onCreateView() 方法执行!
Fragment onViewCreated()方法执行
Activity onCreate() 方法执行!
Fragment onActivityCreated() 方法执行!
Activity onStart() 方法执行!
Fragment onStart() 方法执行!
Activity onResume() 方法执行!
Fragment onResume() 方法执行!

按下主屏幕键/锁屏
Fragment onPause() 方法执行!
Activity onPause() 方法执行!
Fragment onStop() 方法执行!
Activity onStop() 方法执行!

再次打开
Activity onRestart() 方法执行!
Activity onStart() 方法执行!
Fragment onStart() 方法执行!
Activity onResume() 方法执行!
Fragment onResume() 方法执行!

按下后退键
Fragment onPause() 方法执行!
Activity onPause() 方法执行!
Fragment onStop() 方法执行!
Activity onStop() 方法执行!
Fragment onDestroyView() 方法执行!
Fragment onDestroy() 方法执行!
Fragment onDetach() 方法执行!
Activity onDestroy() 方法执行!

3. Fragment的通信

3.1 在Fragment中调用Activity中的方法

       在Fragment中调用Activity的方法很简单,Fragment有个getActivity()的方法,比如,在MainActivity中的一个Fragment中获取MainActivity的引用,并调用MainActivity的某个方法methodA()方法你可以这么写:

MainActivity mainActivity = (MainActivity) getActivity();
mainActivity.methodA();

3.2 在Activity中调用Fragment的方法

       在Activity中调用Fragment中的方法是最简单的,我想这里我不用多说吧!直接接口回调即可调用Fragment的任何可访问的方法。

3.3 在Fragment中调用另外一个Fragment的方法

       这个可就需要一定的思维性了,首先要想调用Fragment A的方法,除了这个Fragment A自身可以调用外,这个Fragment A所属的Activity也可以调用,要想另外一个Fragment B调用此Fragment A的方法,Fragment B可以间接通过Activity来进行调用,也就是3.1 和 3.2 的结合。

三 Service

1. Service基础知识

1.1 Service是什么?

       Service(服务)是一个一种可以在后台执行长时间运行操作而没有用户界面的组件。它运行于UI线程,因此不能进行耗时的操作。

1.2 Service和Thread的区别

       Service的运行是在UI线程当中的,是绝对绝对不能进行耗时操作的,而Thread开启的子线程则可以进行耗时操作,但是Thread开启的子线程是不能直接对UI进行操作的,否则极有可能发生直接让程序崩掉,这就是它们的区别。

2. 启动Service的2种方式

2.1 startService()方法开启Service

步骤:
  a.定义一个类继承Service。
  b.在AndroidManifest.xml文件中配置该Service。
  c.使用Context的startService(Intent)方法启动该Service。
  d.不再使用该Service时,调用Context的stopService(Intent)方法停止该Service。

2.2 bindService方法开启Service(Activity与Service绑定)

步骤:
  a.创建BinderService服务端,继承自Service并在类中创建一个实现IBinder接口的实现实例对象并提供公共方法给客户端调用。
  b.从onBind()回调方法返回此Binder实例。
  c.在客户端中,从onServiceConnected回调方法接收Binder,并使用提供的方法调用绑定服务。

3. Service的生命周期

       服务的生命周期有两种,因为服务可以跟Activity绑定起来,也可以不绑定,Activity和服务进行通信的话,是需要把服务和Activity进行绑定的。因此服务的生命周期分为未绑定Activity的和绑定Activity的。

没有绑定Activity的服务生命周期:

启动服务>onCreate()>onStartCommand()>服务运行>onDestory()>服务销毁

绑定Activity的服务生命周期

绑定服务>onCreate()>onBind()>服务运行>onUnBind()>onDestory()>服务被销毁

  1. 通过Intent和startService()方法启动了一个服务,接下来执行onCreate()方法,首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand() 或 onBind() 之前)。如果服务已在运行,则不会调用此方法。

  2. 当另一个组件(如 Activity)通过调用 startService() 请求启动服务时,系统将调用此方法。一旦执行此方法,服务即会启动并可在后台无限期运行。 如果您实现此方法,则在服务工作完成后,需要由您通过调用 stopSelf() 或 stopService() 来停止服务。(如果您只想提供绑定,则无需实现此方法。)

  3. 服务开始处于运行状态。

  4. 某个操作导致服务停止,比如执行了方法stopService(),那么服务接下来会执行onDestory()销毁。服务应该实现此方法来清理所有资源,如线程、注册的侦听器、接收器等。 这是服务接收的最后一个调用。

  5. 服务被完全销毁,下一步就是等待被垃圾回收器回收了。

  6. 通过Intent和bindService()方法启动了一个服务,接下来会执行onCreate()方法,首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand() 或 onBind() 之前)。如果服务已在运行,则不会调用此方法。

  7. 当另一个组件想通过调用 bindService() 与服务绑定(例如执行 RPC)时,系统将调用此方法。在此方法的实现中,您必须通过返回 IBinder 提供一个接口,供客户端用来与服务进行通信。请务必实现此方法,但如果您并不希望允许绑定,则应返回 null。

  8. 服务开始处于运行状态。成功与Activity绑定。

  9. 某个操作导致服务解除绑定,比如执行了方法unbindService(),那么服务接下来会解除与当前Activity的绑定。接下来服务将面临销毁。

  10. 服务执行onDestory()方法被销毁。服务应该实现此方法来清理所有资源,如线程、注册的侦听器、接收器等。 这是服务接收的最后一个调用。

  11. 服务被完全销毁,下一步就是等待被垃圾回收器回收了。

Service总结:

  1. 被启动的服务的生命周期:如果一个Service被某个Activity 调用 Context.startService 方法启动,那么不管是否有Activity使用bindService绑定或unbindService解除绑定到该Service,该Service都在后台运行。如果一个Service被startService 方法多次启动,那么onCreate方法只会调用一次,onStart将会被调用多次(对应调用startService的次数),并且系统只会创建Service的一个实例(因此你应该知道只需要一次stopService调用)。该Service将会一直在后台运行,而不管对应程序的Activity是否在运行,直到被调用stopService,或自身的stopSelf方法。当然如果系统资源不足,android系统也可能结束服务。

  2. 被绑定的服务的生命周期:如果一个Service被某个Activity 调用 Context.bindService 方法绑定启动,不管调用 bindService 调用几次,onCreate方法都只会调用一次,同时onStart方法始终不会被调用。当连接建立之后,Service将会一直运行,除非调用Context.unbindService 断开连接或者之前调用bindService 的 Context 不存在了(如Activity被finish的时候),系统将会自动停止Service,对应onDestroy将被调用。

  3. 被启动又被绑定的服务的生命周期:如果一个Service又被启动又被绑定,则该Service将会一直在后台运行。并且不管如何调用,onCreate始终只会调用一次,对应startService调用多少次,Service的onStart便会调用多少次。调用unbindService将不会停止Service,而必须调用 stopService 或 Service的 stopSelf 来停止服务。

  4. 当服务被停止时清除服务:当一个Service被终止(1、调用stopService;2、调用stopSelf;3、不再有绑定的连接(没有被启动))时,onDestroy方法将会被调用,在这里你应当做一些清除工作,如停止在Service中创建并运行的线程。

四 Broadcast

1. 广播的概念

1.1 定义

在Android中,它是一种广泛运用在应用程序之间传输信息的机制,Android中我们发送广播内容是一个Intent,这个Intent中可以携带我们要发送的数据。

1.2 广播的使用场景

a.同一app内有多个进程的不同组件之间的消息通信。

b.不同app之间的组件之间消息的通信。

1.3 广播的种类

标准广播:context.sendBroadcast(Intent)方法发送的广播,不可被拦截

有序广播:context.sendOrderBroadcast(Intent)方法发送的广播,可被拦截

本地广播:localBroadcastManager.sendBroadcast(Intent),只在app内传播

2. 广播接收器

广播接收器是专门用来接收广播信息的,它可分为静态注册和动态注册:

  • 静态注册:注册完成一直在运行。

首先你要创建一个广播接收器类,实例代码如下:

public class BootCompleteReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show();
    }
}

另外,静态的广播接收器一定要在AndroidManifest.xml文件中注册才可以使用,AndroidManifest.xml文件中注册静态广播代码如下:

          <receiver 
               android:name=".BootCompleteReceiver" >
               <intent-filter>
                   <action android:name="android.intent.action.BOOT_COMPLETED" />
               </intent-filter>
           </receiver>
  • 动态注册:跟随Activity的生命周期。
    新建一个类,让它继承自BroadcastReceiver,并重写父类的onReceive()方法就行了。这样有广播到来时,onReceive()方法就会得到执行,具体的逻辑就可以在这个方法中处理。
  • 动态注册广播接收器的优点以及缺点:
    动态注册的广播接收器可以自由地控制注册与注销,在灵活性方面有很大优势,但是它也存在着一个缺点,即必须要在程序启动之后才能接收到广播,因为注册的逻辑是写在onCreate()方法中的。那么有没有广播能在程序未启动的情况下就能接收到广播呢?静态注册的广播接收器就可以做到。

3. 广播内部实现机制

  1. 自定义广播接收者BroadcastReceiver,并且重写onReceiver()方法。

  2. 通过Binder机制向AMS(Activity Manager Service)进行注册。

  3. 广播发送者通过Binder机制向AMS发送广播。

  4. AMS查找符合条件(IntentFilter/Permission等)的BroadcastReceiver,将广播发送到相应的BroadcastReceiver(一般情况下是Activity)的消息队列中。

  5. 消息循环执行拿到此广播,回调BroadcastReceiver中的onReceiver()方法。

4. 本地广播

本地广播的发送和注册广播接收器都需要使用到LocalBroadcastManager类,如下所示为本地广播的发送和本地广播接收器注册的代码:
  本地广播的发送:

public static void sendLocalBroadcast(Context context,String action){

    Intent intent = new Intent(action);
    LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
    localBroadcastManager.sendBroadcast(intent);

}

本地广播的接收器的注册:

IntentFilter intentFilter = new IntentFilter();
    LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);

    intentFilter.addAction(new BroadcastUtil().action_next);
    nasbr = new NextAndStartBroadcastReceiver();
    localBroadcastManager.registerReceiver(nasbr, intentFilter);//注册本地广播接收器

特点:

1. 使用它发送的广播将只在自身app内传播,因此你不必担心泄漏隐私的数据。

2. 其他app无法对你的app发送该广播,因此你的app根本不可能收到非自身app发送的该广播,因此你不必担心有安全漏洞可以利用。

3. 比系统广播更加高效。

内部实现机制:

1. LocalBroadcast高效的原因:因为它内部是通过Handler实现的,它的sendBroadcast()方法含义并非和系统的sendBroadcast()一样,它的sendBroadcast()方法其实就是通过Handler发送了一个Message而已。

2. LocalBroadcast安全的原因:既然它是通过Handler实现广播发送的,那么相比系统广播通过Binder机制实现那肯定更加高效,同时使用Handler来实现,别的app无法向我们应用发送该广播,而我们app内部发送的广播也不会离开我们的app。

LocalBroadcast内部协作主要是靠两个Map集合:mReceivers和mActions,当然还有一个List集合mPendingBroadcasts,这个主要存储待接收的广播对象。

五 WebView

1. WebView常见的坑

  1. API 16之前版本存在远程代码执行漏洞,该漏洞源自于程序没有正确限制使用WebView.addJavascriptInterface方法,攻击者可以使用Java Reflection API利用该漏洞执行任意Java对象和方法。

  2. WebView的销毁和内存泄漏问题。WebView的完全销毁是件麻烦事,一旦销毁流程不正确,极易容易导致内存泄漏。

  3. jsbridge
      通过javascript构建一个桥,桥的两端一个端是客户端,一端是服务端,它可以让本地端调用远端的web代码,也可以让远端调用本地的代码。

  4. WebViewClient.onPageFinished(不靠谱) –> WebChromeClient.onProgressChanged(靠谱)

  5. 后台耗电(性能优化)
      WebView开启网页时会自己开启线程,如果没有合理的销毁,那么残余线程就会一直运行,so这会非常耗电的,解决方案:有一种暴力方式就是Activity的onDestroy中调用System.exit()方法把虚拟机关闭,也可以结合自己应用的WebView的情况设计出一个温柔的方案。

  6. WebView硬件加速导致的页面渲染问题
      WebView硬件加速偶尔导致页面白块,页面闪烁,但是加载速度比较快,解决方案:关闭硬件加速。

2. WebView的内存泄漏问题

原因:WebView会关联一个Activity,WebView执行的操作是在新线程当中回收,时间Activity没有办法确认,Activity的生命周期和WebView线程生命周期不一致导致WebView一直执行,因为WebView内部持有Activity的引用,导致Activity一直不能被回收,原理类似于匿名内部类持有外部类的引用一样。那么如何解决呢?解决方案如下:

  1. 独立进程,简单暴力,涉及到进程间通信。(开发过程中常用)

  2. 动态添加WebView,对传入WebView中使用Context弱引用,动态添加WebView意思在布局中创建一个ViewGroup用来放置WebView,Activity创建add进来,Activity停止时remove掉。

六 Bainder机制

  1. 通常情况下,Binder是一种通信机制。

  2. 对于Server来说,Binder指的是Binder本地对象/对于Client来说,Binder指的是Binder的代理对象。

  3. 对于传输过程而言,Binder是可以进行跨进程传递的对象。

  4. AIDL是Binder机制的一个实例。

七 Handler机制

1. 定义

Handler是可以通过发送和处理Message和Runnable对象来关联相应线程的MessageQueue。通常我们认为它是一种异步机制。

  1. 可以让对应的Message和Runnable在未来的某个时间点进行相应的处理。
  2. 让自己想要的耗时操作在子线程中完成,让更新UI的操作在主线程中完成,而子线程与主线程之间的通信就是靠Handler来完成。

2. Handler的使用方法

  • post(Runnable)
  • sendMessage(Message)

3. Handler内部实现机制

Handler机制也可叫异步消息机制,它主要由4个部分组成:Message,Handler,MessageQueue,Looper,在上面我们已经接触到了Message和Handler,接下来我们对4个成员进行着重的了解:

  1. Message
      Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。使用Message的arg1和arg2便可携带int数据,使用obj便可携带Object类型数据。

  2. Handler
      Handler顾名思义就是处理者的意思,它只要用于在子线程发送消息对象Message,在UI线程处理消息对象Message,在子线程调用sendMessage方法发送消息对象Message,而发送的消息经过一系列地辗转之后最终会被传递到Handler的handleMessage方法中,最终在handleMessage方法中消息对象Message被处理。

  3. MessageQueue
      MessageQueue就是消息队列的意思,它只要用于存放所有通过Handler发送过来的消息。这部分消息会一直存放于消息队列当中,等待被处理。每个线程中只会有一个MessageQueue对象,请牢记这句话。其实从字面上就可以看出,MessageQueue底层数据结构是队列,而且这个队列只存放Message对象。

  4. Looper
      Looper是每个线程中的MessageQueue的管家,调用Looper的loop()方法后,就会进入到一个无限循环当中,然后每当MesssageQueue中存在一条消息,Looper就会将这条消息取出,并将它传递到Handler的handleMessage()方法中。每个线程只有一个Looper对象。

Handler机制流程图如下:


Handler机制流程图

4. Handler引起的内存泄漏以及解决方法

原因:静态内部类持有外部类的匿名引用,导致外部activity无法得到释放。
解决方法:handler内部持有外部的弱引用,并把handler改为静态内部类,在activity的onDestory()中调用handler的removeCallback()方法。

八 IntentService机制

1.IntentService是什么?

它的优先级高于Service。
  IntentService是继承处理异步请求的一个类,在IntentService内有一个工作线程来处理耗时操作,启动IntentServiced的方式和启动传统的Service一样,同时,当任务执行完成后,IntentService会自动停止,而不需要我们手动去控制或stopSelf()。另外,可以启动IntentService多次,而每一个耗时操作会以工作队列的方式在IntentService的onHandlerIntent回调方法中执行,并且,每次只执行一个工作线程,执行完第一个在执行第二个。

  1. 它本质是一种特殊的Service,继承自Service并且本身就是一个抽象类。
  2. 它内部是由HandlerThread和Handler实现异步操作。

2.IntentService的使用方法

创建IntentService时,只需要实现onHandlerIntent和构造方法,onHandlerIntent为异步方法,可以执行耗时操作。

九 AsyncTask 机制

1.AsyncTask是什么?

它本质上是一个封装了线程池和Handler的异步框架。

2.AsyncTask的基本用法

  1. 3个参数:Params, Progress 和 Result
  • Params表示用于AsyncTask执行任务的参数的类型
  • Progress表示在后台线程处理的过程中,可以阶段性地发布结果的数据类型
  • Result表示任务全部完成后所返回的数据类型
  1. 5个方法
  • onPreExecute
    该方法是运行在主线程中的。在AsyncTask执行了execute()方法后就会在UI线程上执行onPreExecute()方法,该方法在task真正执行前运行,我们通常可以在该方法中显示一个进度条,从而告知用户后台任务即将开始。
  • doInBackground
    该方法是运行在单独的工作线程中的,而不是运行在主线程中。
  • onProgressUpdate
    当我们在doInBackground中调用publishProgress(Progress…)方法后,就会在UI线程上回调onProgressUpdate方法。
  • onPostExecute
    当doInBackgroud方法执行完毕后,就表示任务完成了,doInBackgroud方法的返回值就会作为参数在主线程中传入到onPostExecute方法中,这样就可以在主线程中根据任务的执行结果更新UI。

3.AsyncTask机制的原理

  1. 它本质上是一个静态的线程池,AsyncTask派生出的子类可以实现不同的异步任务,这些任务都是提交到静态的线程池中执行。

  2. 线程池中的工作线程执行doInBackground(mParams)方法执行异步的任务。

  3. 当任务状态改变后,工作线程向UI线程发送消息,AsyncTask内部的InternalHandler响应这些消息,并调用相关的回调函数。

4.AsyncTask注意事项:

  1. 内存泄漏:

静态内部类持有外部类的匿名引用,导致外部对象无法得到释放,解决方法很简单,让内部持有外部的弱引用即可解决

  1. 生命周期
      在Activity的onDestory()中及时对AsyncTask进行回收,调用其cancel()方法来保证程序的稳定性。

  2. 结果丢失
      当内存不足时,当前的Activity被回收,由于AsyncTask持有的是回收之前Activity的引用,导致AsyncTask更新的结果对象为一个无效的Activity的引用,这就是结果丢失。

  3. 并行或串行
      在1.6(Donut)之前: 在第一版的AsyncTask,任务是串行调度。一个任务执行完成另一个才能执行。由于串行执行任务,使用多个AsyncTask可能会带来有些问题。所以这并不是一个很好的处理异步(尤其是需要将结果作用于UI试图)操作的方法。1.6-2.3: 所有的任务并发执行,这会导致一种情况,就是其中一条任务执行出问题了,会引起其他任务出现错误。3.0之后AsyncTask又修改为了顺序执行,并且新添加了一个函数 executeOnExecutor(Executor),如果您需要并行执行,则只需要调用该函数,并把参数设置为并行执行即可。

十 HandlerThread机制

1.HandlerThread的产生背景

开启子线程进行耗时操作,多次创建和销毁子线程是很耗费资源的,但是木有关系,谷歌考虑了这点为我们专门开发出了HandlerThread机制。

2.HandlerThread是什么?

本质:Handler + Thread + Looper,是一个Thread内部有Looper。

  1. HandlerThread本质上是一个线程类,它继承了Thread。

  2. HandlerThread有自己内部的Looper对象,可以进行Looper循环。

  3. 通过获取HandlerThread的Looper对象传递给Handler对象,可以在handlerMessage方法中执行异步任务。

  4. 优点是不会有堵塞,减少对性能的消耗,缺点是不能进行多任务的处理,需要等待进行处理,处理效率较低。

  5. 与线程池注重并发不同,HandlerThread是一个串行队列,HandlerThread背后只有一个线程。

十一 IntentService机制

1.IntentService是什么?

它的优先级高于Service。
  IntentService是继承处理异步请求的一个类,在IntentService内有一个工作线程来处理耗时操作,启动IntentServiced的方式和启动传统的Service一样,同时,当任务执行完成后,IntentService会自动停止,而不需要我们手动去控制或stopSelf()。另外,可以启动IntentService多次,而每一个耗时操作会以工作队列的方式在IntentService的onHandlerIntent回调方法中执行,并且,每次只执行一个工作线程,执行完第一个在执行第二个。

  • 它本质是一种特殊的Service,继承自Service并且本身就是一个抽象类。

  • 它内部是由HandlerThread和Handler实现异步操作。

2.IntentService的使用方法

创建IntentService时,只需要实现onHandlerIntent和构造方法,onHandlerIntent为异步方法,可以执行耗时操作。

十二 View绘制机制

1. View树的绘制流程

measure(测量)→layout(布局)→draw(绘制)

2. Measure过程

Measure过程

measure过程主要就是从顶层父View向子View递归调用view.measure方法(measure中又回调onMeasure方法)的过程。具体measure核心主要有如下几点:

MeasureSpec(View的内部类)测量规格为int型,值由高2位规格模式specMode和低30位具体尺寸specSize组成。其中specMode只有三种值:

  • MeasureSpec.EXACTLY //确定模式,父View希望子View的大小是确定的,由specSize决定;
  • MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;
  • MeasureSpec.UNSPECIFIED //未指定模式,父View完全依据子View的设计值来决定;

View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。

最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。

ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。

只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。

View的布局大小由父View和子View共同决定。

使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。

3. Layout过程

整个layout过程比较容易理解,从上面分析可以看出layout也是从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。具体layout核心主要有以下几点:

  • View.layout方法可被重载,ViewGroup.layout为final的不可重载,ViewGroup.onLayout为abstract的,子类必须重载实现自己的位置逻辑。

  • measure操作完成后得到的是对每个View经测量过的measuredWidth和measuredHeight,layout操作完成之后得到的是对每个View进行位置分配后的mLeft、mTop、mRight、mBottom,这些值都是相对于父View来说的。

  • 凡是layout_XXX的布局属性基本都针对的是包含子View的ViewGroup的,当对一个没有父容器的View设置相关layout_XXX属性是没有任何意义的(前面《Android应用setContentView与LayoutInflater加载解析机制源码分析》也有提到过)。

  • 使用View的getWidth()和getHeight()方法来获取View测量的宽高,必须保证这两个方法在onLayout流程之后被调用才能返回有效值。

4.Draw过程

绘制过程就是把View对象绘制到屏幕上,整个draw过程需要注意如下细节:

  • 如果该View是一个ViewGroup,则需要递归绘制其所包含的所有子View。

  • View默认不会绘制任何内容,真正的绘制都需要自己在子类中实现。

  • View的绘制是借助onDraw方法传入的Canvas类来进行的。

  • 区分View动画和ViewGroup布局动画,前者指的是View自身的动画,可以通过setAnimation添加,后者是专门针对ViewGroup显示内部子视图时设置的动画,可以在xml布局文件中对ViewGroup设置layoutAnimation属性(譬如对LinearLayout设置子View在显示时出现逐行、随机、下等显示等不同动画效果)。

  • 在获取画布剪切区(每个View的draw中传入的Canvas)时会自动处理掉padding,子View获取Canvas不用关注这些逻辑,只用关心如何绘制即可。

  • 默认情况下子View的ViewGroup.drawChild绘制顺序和子View被添加的顺序一致,但是你也可以重载ViewGroup.getChildDrawingOrder()方法提供不同顺序。

十三 Android部分事件分发机制

1. 为什么有事件分发机制

Android上面的View是树形结构,View可能会重叠在一起,当我们点击的地方有多个View都可以响应的时候,这个点击事件应该给谁呢?为了解决这个问题,就有了事件分发机制。

2. 3个重要的有关事件分发的方法

  • dispatch TouchEvent
    用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级的dispatchTouchEvent方法影响,表示是否消耗此事件。
  • onInterceptTouchEvent
    在上述方法dispatchTouchEvent内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
  • onTouchEvent
    同样也会在dispatchTouchEvent内部调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
    伪代码
public boolean dispatchTouchEvent(MotionEvent ev){

    boolean consume = false;//记录返回值

    if(onInterceptTouchEvent(ev)){//判断是否拦截此事件

        consume = onTouchEvent(ev);//如果当前确认拦截此事件,那么就处理这个事件 

    }else{

        consume = child.dispatchToucnEvent(ev);//如果当前确认不拦截此事件,那么就将事件分发给下一级

    }

    return consume;

}

通过上述伪代码,我们可以得知点击事件的传递规则:对于一个根ViewGroup而言,点击事件产生后,首先会传递给它,这时它的dispatchTouch就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前的事件,接着事件就会交给这个ViewGroup处理,即它的onTouch方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此直到事件被最终处理。

当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调。这时事件处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么当前View的onTouchEvent方法不会被调用。由此可见,给View设置的onTouchListener的优先级比onTouchEvent要高。在onTouchEvent方法中,如果当前设置的有onClickListener,那么它的onClick方法会被调用。可以看出,平时我们常用的OnClickListener,其优先级最低,即处于事件传递的尾端。

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity–>Window–>View,即事件总数先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View,顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依次类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理, 即Activity的onTouchEvent方法会被调用。这个过程其实很好理解,我们可以换一种思路,假设点击事件是一个难题,这个难题最终被上级领导分给了一个程序员去处理(这是事件分发过程),结果这个程序员搞不定(onTouchEvent返回了false),现在该怎么办呢?难题必须要解决,那就只能交给水平更高的上级解决(上级的onTouchEvent被调用),如果上级再搞不定,那就只能交给上级的上级去解决,就这样难题一层层地向上抛,这是公司内部一种常见的处理问题的过程。

关于事件传递机制还需要注意以下:

  1. 同一见事件序列是从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件的序列以down开始,中间含有数量不定的move事件,最终以up事件结束。

  2. 正常情况下,一个事件序列只能被一个View拦截且消耗。这一条的原因可以参考(3),因为一旦一个元素拦截了某个事件,那么同一个事件序列的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如
    一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。

  3. 某个View一旦决定拦截,那么这个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceptTouchEvent不会被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否拦截了。

  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一件序列中的其他事件都不会再交给它处理,并且事件 将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短时间内上级就不敢再把事件交给这个程序员做了,二者是类似的道理。

  5. 如果View不消耗ACTION_DOWN以外的事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。

  6. ViewGroup默认不拦截任何事件。Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false。

  7. View没有onInterceptTouchEvent方法,一旦点击事件传递给它,那么它的onTouchEvent方法就会被调用。

  8. View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView的clickable属性默认为false。

  9. View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true。

  10. onClick会发生的前提是当前View是可点击的,并且它接收到了down和up事件。

  11. 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

3. 事件分发的流程

Activity–>PhoneWindow–>DecorView–>ViewGroup–>…–>View(View树最底部的View)


事件分发

十三 ListView

1. ListView是什么?

ListView就是能用一个数据集合以动态滚动的方式展示到用户界面上的View。

2. ListView的适配器

  • ArrayAdapter:支持泛型操作,最简单的一个Adapter,只能展现一行文字
  • SimpleAdapter:同样具有良好扩展性的一个Adapter,可以自定义多种效果
  • BaseAdapter:抽象类,实际开发中我们会继承这个类并且重写相关方法,用得最多的一个Adapter

BaseAdapter是开发中最常用的适配器ArrayAdapter, SimpleAdapter 都继承于BaseAdapter。BaseAdapter可以完成自己定义的Adapter,可以将任何复杂组合的数据和资源,以任何你想要的显示效果展示给用户。

继承BaseAdapter之后,需要重写以下四个方法:getCount,getItem,getItemId,getView。

系统在绘制ListView之前,将会先调用getCount方法来获取Item的个数。每绘制一个Item就会调用一次getView方法,在getView中引用事先定义好的layout布局确定显示的效果并返回一个View对象作为一个Item显示出来。

这两个方法是自定ListView显示效果中最为重要的,同时只要重写好了这两个方法,ListView就能完全按开发者的要求显示。而getItem和getItemId方法将会在调用ListView的响应方法的时候被调用到。

3. ListView的recycleBin机制

在某一时刻,我们看到ListView中有许多View呈现在UI上,这些View对我们来说是可见的,这些可见的View可以称作OnScreen的View,即在屏幕中能看到的View,也可以叫做ActiveView,因为它们是在UI上可操作的。

当触摸ListView并向上滑动时,ListView上部的一些OnScreen的View位置上移,并移除了ListView的屏幕范围,此时这些OnScreen的View就变得不可见了,不可见的View叫做OffScreen的View,即这些View已经不在屏幕可见范围内了,也可以叫做ScrapView,Scrap表示废弃的意思,ScrapView的意思是这些OffScreen的View不再处于可以交互的Active状态了。ListView会把那些ScrapView(即OffScreen的View)删除,这样就不用绘制这些本来就不可见的View了,同时,ListView会把这些删除的ScrapView放入到RecycleBin中存起来,就像把暂时无用的资源放到回收站一样。

当ListView的底部需要显示新的View的时候,会从RecycleBin中取出一个ScrapView,将其作为convertView参数传递给Adapter的getView方法,从而达到View复用的目的,这样就不必在Adapter的getView方法中执行LayoutInflater.inflate()方法了。

RecycleBin中有两个重要的View数组,分别是mActiveViews和mScrapViews。这两个数组中所存储的View都是用来复用的,只不过mActiveViews中存储的是OnScreen的View,这些View很有可能被直接复用;而mScrapViews中存储的是OffScreen的View,这些View主要是用来间接复用的。

4. ListView优化

  • convertView重用机制

  • ViewHolder机制

  • 三级缓冲/滑动监听事件

优化一:在Adapter中的getView方法中使用ConvertView,即ConvertView的复用,不需要每次都inflate一个View出来,这样既浪费时间,又浪费内存。

优化二:使用ViewHolder,不要在getView方法中写findViewById方法,因为getView方法会执行很多遍,这样也可以节省时间,节约内存。

优化三:使用分页加载,讲真实际开发中,ListView的数据肯定不止几百条,成千上万条数据你不可能一次性加载出来,所以这里需要用到分页加载,一次加载几条或者十几条,但是如果数据量很大的话,像qq,微信这种,如果顺利加载到最后面的话,那么你的list中也会有几万甚至几十万的数据,这样可能也会导致OOM,所以你的数据集List中也不能有那么多数据,所以每加载一页的时候你可以覆盖前一页的数据。

优化四:如果数据当中有图片的话,使用第三方库来加载(也就是缓存),如果你的能力强大到能自己维护的话,那也不是不可以。

优化五:当你手指在滑动列表的时候,尽可能的不加载图片,这样的话滑动就会更加流畅。

十四 动画

1.Android动画的分类

1.1 补间动画

a.渐变动画支持四种类型:平移(Translate)、旋转(Rotate)、缩放(Scale)、不透明度

b. 只是显示的位置变动,View的实际位置未改变,表现为View移动到其他地方,点击事件仍在原处才能响应。

c. 组合使用步骤较复杂。

d. View Animation 也是指此动画。

1.2 帧动画

a. 用于生成连续的Gif效果图。

b. DrawableAnimation也是指此动画

1.3 属性动画

a.支持对所有View能更新的属性的动画(需要属性的setXxx()和getXxx())。

b. 更改的是View实际的属性,所以不会影响其在动画执行后所在位置的正常使用。

c. Android3.0(API11)及以后出现的功能,3.0之前的版本可使用github第三方开源库nineoldandroids.jar进行支持。

2.补间动画,帧动画,属性动画优缺点

2.1 补间动画优缺点

缺点:当平移动画执行完停在最后的位置,结果焦点还在原来的位置(控件的属性没有真的被改变)
优点:相对于逐帧动画来说,补间动画更为连贯自然

2.2 帧动画优缺点

缺点:效果单一,逐帧播放需要很多图片,占用控件较大
优点:制作简单

2.3 属性动画优缺点

缺点:(3.0+API出现)向下兼容问题。
优点:易定制,效果强。

十五 自定义View

1. 自定义View的几种方式

  • 对原View进行扩展方式

  • 多个View的组合方式

  • 重写View的方式

2.自定义View需要重写的方法。

  • onMesure(测量)
  • onLayout(布局)
  • onDraw(绘制)

十六 Context

Context的中文翻译为:语境; 上下文; 背景; 环境,在开发中我们经常说称之为“上下文”,那么这个“上下文”到底是指什么意思呢?在语文中,我们可以理解为语境,在程序中,我们可以理解为当前对象在程序中所处的一个环境,一个与系统交互的过程。比如微信聊天,此时的“环境”是指聊天的界面以及相关的数据请求与传输,Context在加载资源、启动Activity、获取系统服务、创建View等操作都要参与。

Context提供了关于应用环境全局信息的接口。它是一个抽象类,它的执行被Android系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源(应用程序环境变量等)的上下文。就是说,它描述一个应用程序环境的信息(即上下文);是一个抽象类,Android提供了该抽象类的具体实现类;通过它我们可以获取应用程序的资源和类(包括应用级别操作,如启动Activity,发广播,接受Intent等)。

Context.png

Context的作用域:


Context的作用域.png

十七 ContentProvider

1. 简介

内容提供者(Content Provider)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。目前,使用内容提供者是Android实现跨程序共享数据的标准方式。
不同于文件存储和SharedPreferences存储中的两种全局可读可写操作模式,内容提供者可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会泄露的风险。

2.通过内容提供者访问其他程序的数据

内容提供者的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供者来给我们的程序提供外部访问接口。如果一个程序通过内容提供者对其数据提供外部访问接口,那么任何其他的应用程序就都可以对这部分数据进行访问。Android系统中自带的电话簿,短信,媒体库等程序都提供了类似的访问接口,这就使得第三方应用程序可以充分利用这部分数据来实现更好的功能。下面我们就来看看如何通过内容提供者访问其他程序的数据:

2.1 ContentResolver的基本用法

想要访问内容提供者中共享的数据,就一定要借助CotentResolver类,可以通过Context中的getContentResolver()方法获取该类的实例。ContentResolver中提供了一系列的方法用于对数据进行CRUD(增删改查)操作,其中insert()方法用于添加数据,update()方法用于数据的更新,delete()方法用于数据的删除,query()方法用于数据的查询。这好像SQLite数据库操作有木有?
  不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri的参数代替,这个参数被称作内容URI。内容URI给内容提供者中的数据建立了唯一的标识符,它主要由两部分组成:authority和path。authority是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名为com.example.app,那么该程序对应的authority就可以命名为com.example.app.provider。path则是用于对同一应用程序中不同的表做区分的,通常都会添加到authority的后面。比如某个程序的数据库里存在两张表:table1和table2,这时就可以将path分别命名为/table1和/table2,然后把authority和path进行组合,内容的URI就变成了com.example.app.provider/table1和com.example.app.provider/table2。不过目前还是很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议声明。因此,内容URI最标准的格式写法如下:

content://com.example.app.provider/table1
content://com.example.app.provider/table2

在得到内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入。解析的方法也相当简单,代码如下所示:

Uri uri = new Uri.parse("content://com.example.app.provider/table1");

只需要调用Uri的静态方法parse()就可以把内容URI字符串解析成URI对象。
  现在,我们可以通过这个Uri对象来查询table1表中的数据了。代码如下所示:

Cursor cursor = getContentResolver()
                .query(
                       uri,projection,selection,selectionArgs,sortOrder
                 );

query()方法接收的参数跟SQLiteDatabase中的query()方法接收的参数很像,但总体来说这个稍微简单一些,毕竟这是在访问其他程序中的数据,没必要构建复杂的查询语句。下标对内容提供者中的query的接收的参数进行了详细的解释:

查询完成仍然会返回一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个读取出来了。读取的思路仍然是对这个Cursor对象进行遍历,然后一条一条的取出数据即可,代码如下:

if(cursor != null){//注意这里一定要进行一次判空,因为有可能你要查询的表根本不存在
    while(cursor.moveToNext()){
        String column1 = cursor.getString(cursor.getColumnIndex("column1"));
        int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
    }
}

增加,删除,修改

//增加数据
ContentValues values = new ContentValues();
values.put("Column1","text");
values.put("Column2","1");
getContextResolver.insert(uri,values);

//删除数据
getContextResolver.delete(uri,"column2 = ?",new String[]{ "1" });

//更新数据
ContentValues values = new ContentValues();
values.put("Column1","改数据");
getContextResolver.update(uri,values,"column1 =  ? and column2 = ?",new String[]{"text","1"});

3. 创建自己的内容提供者

前面已经提到过,如果要想实现跨程序共享数据的功能,官方推荐的方式就是使用内容提供器,可以新建一个类去继承ContentProvider类的方式来创建一个自己的内容提供器。ContentProvider类有6个抽象方法,我们在使用子类继承它的时候,需要将这6个方法全部重写。新建MyProvider继承字ContentProvider类,代码如下所示:

public class MyProvider extends ContentProvider {

    @Override
    public boolean onCreate() {
        return false;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        return null;
    }//查询

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }//添加

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        return 0;
    }//更新

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }//删除

    @Override
    public String getType(Uri uri) {
        return null;
    }
}
  1. onCreate()方法:
      初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作。返回true表示内容提供器初始化成功,返回false则表示失败。注意,只有当存在ContentResolver尝试访问我们的程序中的数据时,内容提供器才会被初始化。

  2. query()方法:
      从内容提供器中查询数据。使用uri参数来确定查询的哪张表,projection参数用于确定查询的哪一列,selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。

  3. insert()方法:
      向内容提供器中添加一条数据。使用uri参数来确定要添加的表,待添加的数据保存在values参数中。添加完成后,返回一个用于表示这条新纪录的URI。

  4. update()方法:
      更新内容提供器中已有的数据。使用uri参数来确定更新哪一张表中的数据,新数据保存着values参数当中,selection和selectionArgs参数用于约束更新哪些行,受影响的行数将作为返回值返回。

  5. delete()方法:
      从内容提供器中删除数据。使用uri参数来确定删除哪一张表中的数据,selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。

  6. getType()方法:
      根据传入的内容URI来返回相应的MIME类型。

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