再见友盟!Acra详细分析 -Part 1

概述

Acra是老牌的bug自动采集系统。接入sdk后,可以实现程序崩溃自动发送崩溃日志。
发送自定义的错误日志等功能。具体详细介绍可以参见acra官网地址
整体来看,Acra就是通过sdk收集进程的崩溃日志,然后以http或mail(默认的两类Sender)的方式将数据发送出去。服务器则是一套基于json的restful的接口。
服务端方面不是本次分析重点,暂不进行分析。
本系列文章将基于Acra 4.9.0 RC2源码进行分析。

Backend

服务端方面我们需要先搭建一个server,才能更好的看到我们的崩溃信息,
更直观的看到acra给我们提供了哪些针对崩溃的采集内容。
官方提供了acralyzer以及一些针对acra的第三方开源实现。
关于世面上常用的server端的,该文章做了明确分析,针对不同backend的比较
官方backend acralyzer的搭建非常简单,具体可以参见该文章的server配置部分
项目搭建完成后可以使用通过如下的url对server端进行访问。

查看app崩溃的表结构
http://ip:port/_utils/

utils.png

查看崩溃日志
http://ip:port/acralyzer/_design/acralyzer/index.html#/dashboard/

analyze dashboard.png

关于server端的介绍结束。不是重点。

Client

项目构建

最新版本项目基于Gradle构建,了解Acra历史的肯定知道该项目是存在了很久了.
Android世界中项目最早是基于ant构建,后来是maven,现在是Gradle。
在没有Gradle的编译环境之前,基本上大部分是基于maven构建。
查看最新版本的代码可以看到仍然包含了之前maven的配置文件。
并且使用Gradle编译编译中使用到的version name等配置参数也都是从pom.xml中读取的。
具体可以参看build.gradle中关于版本号的相关配置。

需要注意的是,从github clone下来的项目是无法直接使用Gradle进行编译的。
熟悉Gradle android 编译流程的人应该从build.gradle文件中可以找出错误的原因。
具体的编译文件需要修改的地方为,在build.gradle中开头位置添加编译android项目使用到的plugin。
如下所示:

//此部分添加到build.gradle开头
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}
allprojects {
    repositories {
        jcenter()
    }
}
task clean(type: Delete) {
    delete rootProject.buildDir
}
//此部分添加到build.gradle开头

添加之后,就可以执行gradle build命令打出需要使用的aar包。


项目配置及使用

首先需要注意一点,Acra使用独立进程:acra,进行采集数据的发送,保证当app崩溃时,采集仍然能发送出去。
由于使用独立的进程,所以会导致application被实例化多次,这样就需要注意app自身的某些业务逻辑,不要在application类中执行多次,从而导致app产生bug。
对Acra的相关配置一般在application中进行初始化。

初始化配置

在application中进行初始化配置。

  1. 使用注解初始化
    import org.acra.*;
    import org.acra.annotation.*;

    @ReportsCrashes(
        formUri = "http://www.backendofyourchoice.com/reportpath"
    )
    public class MyApplication extends Application {
        @Override
        protected void attachBaseContext(Context base) {
            super.attachBaseContext(base);
            // 调用init方法,对acra进行初始化.
            ACRA.init(this);
        }
    }
  1. 动态初始化配置
    import org.acra.ACRA;
    import org.acra.configuration.*;

    public class MyApplication extends Application {
        @Override
        protected void attachBaseContext(Context base) {
            super.attachBaseContext(base);
            //使用ConfigurationBuilder构建ACRAConfirueation
            final ACRAConfiguration config = new ConfigurationBuilder(this)
                .setFoo(foo)
                .setBar(bar)
                .build();
            // 传参的方式初始化acra
            ACRA.init(this, config);
        }
    }

一般使用acra我们的目的是采集崩溃,所以需要在manifest中申请网络权限,以保证crash的正常发送。
<uses-permission android:name="android.permission.INTERNET"/>

目标服务器配置

acra中发送crash数据是通过Sender实现的,Sender是通过ReportSenderFactory实例化出来的。
而ReportSenderFactory是可以在初始化时进行配置的。
acra默认提供了email及http 两种sender。
如果自定义Sender则需要两个步骤,

  1. 实现ReportSender接口,用来执行发送报告操作。
  2. 实现ReportSenderFactory接口,用来创建自定义sender。
public class YourOwnSender implements ReportSender {
    @Override
    public void send(Context context, CrashReportData report) throws ReportSenderException {
        // 遍历 CrashReportData 并做发送操作
    }
}

public class YourOwnSenderfactory implements ReportSenderFactory {
    // 由于在SenderService中通过Class.newInstance()来实例化对象
    // 所以需要保证实例化的类的构造函数有一个默认无参的构造函数
    // 自定义的ReportSenderFactory必须包含一个不含参数的构造函数
    public ReportSender create(Context context, ACRAConfiguration config) {
        ...
        return new YourOwnSender(someConfigPerhaps);
    }
}

针对Sender的配置有两种形式,一种为注解,一种为通过代码进行设置。

//注解的方式设置Sender
@ReportCrashes{
   reportSenderFactoryClasses = {
        your.funky.ReportSenderFactory.class, 
        other.funky.ReportSenderFactory.class
   } 
}
public class YourApplication extends Application {
   ...
}

//代码的方式设置Sender
@ReportCrashes{
   ...
}
public class YourApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        final Class<? extends ReportSenderFactory>[] myReportSenderFactoryClasses = ...

        // 初始化一个ConfigurationBuilder,并设置ReportSenderFactoryClasses.
        final ACRAConfiguration config = new ConfigurationBuilder(this)
            .setReportSenderFactoryClasses(myReportSenderFactoryClasses)
            .build();
        ACRA.init(this, config);
    }
}

Acra中默认提供两个Sender

  1. HttpSender
    • 提供了Post及Put两种提交crash到服务器的方式。
    • 提交的类型可以为JSON或Form表单两种方式。

建议使用Put方式进行提交。
Put可以理解成已经知道了某个资源的位置.代表直接更新或创建该资源。
POST为不知道某个资源的位置,由server端来决定对该资源进行何种方式的存储。
所以在此场景下使用Put操作更合适,因为每一条bug实际上就应该对应与数据库中的一条,
只是该条记录还没有上传到服务器。
关于post与put的差别,具体可以查看该文档when should use PUT and when should use POST

  1. EmailIntentSender
    组拼crash Report 通过intent调用系统提供的发送email的app。

流程分析及重点类分析

初始化设置流程

Acra的初始化函数为init,所以使用入口函数ACRA.init()对acra进行初始化。
一般入口函数在application初始化时进行调用。

ACRA.init()

使用ReportsCrashes来初始化Acra。
ACRA提供多个init方法,经过内部调用,最终都会调用参数最多的init方法完成初始化相关逻辑。
下面对重要的init方法进行说明

class ACRA {
    //使用Application的注解进行初始化
    public static void init(Application app){
        //获取application上的注解
        final ReportsCrashes reportsCrashes = 
            app.getClass().getAnnotation(ReportsCrashes.class);
        //ConfigurationBuilder中通过注解获取application上配置的注解信息
        init(app, new ConfigurationBuilder(app).build());
    } 
    //参数 checkReportsOnApplicationStart 表示
    //是否立即执行ErrorReporter.checkReportsOnApplicationStart()方法
    public static void init(Application app, ACRAConfiguration config, boolean checkReportsOnApplicationStart){
        //根据process的名字判断执行当前方法执行时所在的进程是否是发送crash的进程
        final boolean senderServiceProcess = isACRASenderServiceProcess(app);
        //ACRA只支持2.3以上的系统版本,所以预先做判断
        final boolean supportedAndroidVersion = Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO;
        //保存config
        configProxy = config;
        //获取ACRA保存配置的SharedPreferences
        final SharedPreferences prefs = new SharedPreferencesFactory(mApplication, configProxy).create();
        if (!prefs.getBoolean(PREF__LEGACY_ALREADY_CONVERTED_TO_4_8_0, false)) {
            //处理之前的版本的日志文件
        }
        errorReporterSingleton = new ErrorReporter(mApplication, configProxy, prefs, enableAcra, supportedAndroidVersion, !senderServiceProcess);   
        //当在非Sender进程,并设置app启动时发送report的情况下进行检测。
        //当在Sender进程中,不需要进行检测,因为Sender进程中的逻辑自己会进行判断处理
        if (checkReportsOnApplicationStart && !senderServiceProcess) {
        //执行发送的相关业务逻辑
            final ApplicationStartupProcessor startupProcessor = 
                new ApplicationStartupProcessor(mApplication,  config);
                if (config.deleteOldUnsentReportsOnApplicationStart()) {
                    startupProcessor.deleteUnsentReportsFromOldAppVersion();
                }
                if (config.deleteUnapprovedReportsOnApplicationStart()) {
                    startupProcessor.deleteAllUnapprovedReportsBarOne();
                }
                if (enableAcra) {
                    startupProcessor.sendApprovedReports();
                }
         }
    }
}

ConfigurationBuilder

主要用来封装构造ACRAConfiguration的相关属性。
提供了两种方式来设置相关属性的值。

  1. 构造函数通过注解的方式,获取Application中定义注解的值,进行设置。
  2. 通过set方法,设置每个不同的配置项。

获取属性值之后,通过调用build()方法,创建ACRAConfiguration对象。

//通过app的注解所配置的值对builder对象本身进行初始化
public ConfigurationBuilder(@NonNull Application app) 
{
    //.....
}
//构建ACRAConfiguration对象
public ACRAConfiguration build() {
    return new ACRAConfiguration(this);
}

....
//对外提供的设置相关属性的方法
public ConfigurationBuilder setHttpHeaders(@NonNull Map<String, String> headers) {
    this.httpHeaders.clear();
    this.httpHeaders.putAll(headers);
    return this;
} 

可能有些同学不太清楚注解的相关知识,可以参考该文章注解知识的介绍

ACRAConfiguration

用来保存ACRA涉及到的所有配置。

SharedPreferencesFactory

用来获取ACRA所使用的SharedPreferences的文件。
通过这层封装可以对sp进行一些自定义的设置,比如sp的名字。

public class SharedPreferencesFactory {
    //获取默认sharedPreferences的流程为
    //1.如果通过builder或ReportsCrashes配置所构建的类生成的config文件,
    //  包含sp相关配置,则使用该配置项。
    //2.如果不满足1的条件,则通过android api PreferenceManager返回默认的sp文件
    public SharedPreferences create() {
        if (context == null) {
        //..
        } else if (!"".equals(config.sharedPreferencesName())) {
            return context.getSharedPreferences(
                config.sharedPreferencesName(), config.sharedPreferencesMode()
            );
        } else {
            return PreferenceManager.getDefaultSharedPreferences(context);
        }
    }
}

ErrorReporter

ACRA最核心的类,该类用来捕获crash相关的信息,以及发送crash信息。
Android平台如果想要捕获java层代码的crash需要设置application Thread的UncaughtExceptionHandler。
ACRA会将ErrorReporter设置为Application Thread的UncaughtExceptionHandler。
从而实现对异常的捕获。

这里有一点需要注意的,Thread中的defaultUncaughtHandler为一个对象,
所以多次设置该属性,则会使用最后一个作为异常捕获的类。
比如现在市面上比较火的umeng等相关包含崩溃采集功能sdk。
使用的时候,需要注意查看文档或反编译其源码,查看sdk是怎么实现该部分功能的。
否则容易造成先设置的异常捕获类,无法被执行。

public class ErrorReporter implements Thread.UncaughtExceptionHandler {
    ErrorReporter(
        @NonNull Application context, @NonNull ACRAConfiguration config, 
        @NonNull SharedPreferences prefs,boolean enabled, 
        boolean supportedAndroidVersion, boolean listenForUncaughtExceptions)
    {
        ...
        //通过ConfigurationCollector获取系统的相关环境信息
         if (config.getReportFields().contains(ReportField.INITIAL_CONFIGURATION)) {
            initialConfiguration = ConfigurationCollector.collectConfiguration(this.context);
        } else {
            initialConfiguration = null;
        }
        //获取系统时间,崩溃发生时上传
        final Calendar appStartDate = new GregorianCalendar();
        crashReportDataFactory = new CrashReportDataFactory(
            this.context, config, prefs, appStartDate, initialConfiguration);
        final Thread.UncaughtExceptionHandler defaultExceptionHandler;
        //listenForUncaughtExceptions为Acra初始化流程中传过来的。
        //如果当前运行的进程是Sender进程则不监听崩溃。
        //如果当前运行的进程是app主进程则对崩溃进行监听
        if (listenForUncaughtExceptions) {
            defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
            Thread.setDefaultUncaughtExceptionHandler(this);
        } else {
            defaultExceptionHandler = null;
        }
        //记录最后的activity
        final LastActivityManager lastActivityManager = new LastActivityManager(this.context);
        //用来保存针对崩溃的一些用户自定义的信息
        final ReportPrimer reportPrimer = getReportPrimer(config);
        
        reportExecutor = new ReportExecutor(
            context, config, crashReportDataFactory, 
            lastActivityManager, defaultExceptionHandler, reportPrimer);
        reportExecutor.setEnabled(enabled);
    }
    
    //崩溃采集需要实现UncaughtExceptionHandler为接口。
    @Override
    public void uncaughtException(@Nullable Thread t, @NonNull Throwable e) {
        //未开启crash采集时,使用之前默认的ExceptionHandler处理
        if (!reportExecutor.isEnabled()) {
            reportExecutor.handReportToDefaultExceptionHandler(t, e);
            return;
        }
        try {
            ACRA.log.e(LOG_TAG, "ACRA caught a " + e.getClass().getSimpleName() +
                " for " + context.getPackageName(), e);
            if (ACRA.DEV_LOGGING) ACRA.log.d(LOG_TAG, "Building report");
            performDeprecatedReportPriming();
            // 生成并发送report
            new ReportBuilder()
                .uncaughtExceptionThread(t)
                .exception(e)
                .endApplication()
                .build(reportExecutor);
        } catch (Throwable fatality) {
            // ACRA failed. Prevent any recursive call to ACRA.uncaughtException(), let the native reporter do its job.
            ACRA.log.e(LOG_TAG, "ACRA failed to capture the error - handing off to native error reporter" , fatality);
            reportExecutor.handReportToDefaultExceptionHandler(t, e);
        }
    }
    
}

参见代码可以知道,acra通过设置默认ExceptionHandler来捕获异常。
并把自己设置为处理对象。

LastActivityManager

是用来记录最后展示的Activity的,通过application.registerActivityLifecycleCallbacks来实现记录功能的。ACRA可以在崩溃的时候弹出Dialog,所以需要记住最后的Activity。

ReportExecutor

主要业务逻辑关注execute()方法.
该类主要负责调用CrashReportDataFactory采集数据,
调用CrashReportPersister对崩溃数据进行持久化,
调用SenderServiceStarter运行Service发送的报告。

ApplicationStartupProcessor

封装一些App启动时可能执行的任务

class ApplicationStartupProcessor{
    void deleteUnsentReportsFromOldAppVersion(){
        //app版本更新后,一般会修掉老的崩溃等问题,
        //所以当老版本更新到新版本后,可以将老版本记录的日志全部删除掉
    }
    
    void deleteAllUnapprovedReportsBarOne(){
        //unapproved的文件夹内的文件,只保留最新创建的日志文件,其他的全部删除掉。
    }
    
    void sendApprovedReports(){
        //调用SenderServiceStarter开启Service进行崩溃日志的发送。
    }
    
}

ReportLocator

关于ACRA对日志文件位置的处理主要是ReportLocator来设置的。
acra内部使用文件对崩溃日志进行保存,该类用来获取文件夹的名字。
内部有两个文件夹acra-unapproved(未处理),acra-approved(处理过)分别用来保存未处理及处理过的崩溃文件。


采集内容

崩溃采集,必然需要采集崩溃及手机的相关信息。
ACRA中涉及到崩溃相关信息的主要有如下一些类。
ReportBuilder,ReportPrimer,CrashReportDataFactory,CrashReportData,
LogCatCollector,DropBoxCollector,ReportUtils,UUID,
Installation,ConfigurationCollector,DumpSysCollector,ReflectionCollector,
DisplayManagerCollector,DeviceFeaturesCollector,settingsCollector,
LogFileCollector,MediaCodecListCollector,ThreadCollector.
ACRA获取全部数据,涉及到的类比较多。下面逐个分析。

ReportBuilder

对throwable,message,自定义信息,以及exception的简单封装。
主要方法为build(),通过build方法调用ReportExecutor.execute()方法,
在ReportExcutor中进行真正的crash采集以及调用发送Service

ReportPrimer

用来设置崩溃时候,用户需要保存的一些用户自定义的信息。
比如崩溃时候在此类中设置一些用户账号等相关信息。
该类中设置的相关内容会一起发送到服务端,从而更好的定位一些崩溃信息。

CrashReportDataFactory,CrashReportData

CrashReportDataFactory类用来实例化CrashReportData。
其中最重要的方法为createCrashData()方法,使用该方法来组拼CrashReportData。
CrashReportData继承EnumMap,其中保存的数据的key为各种上传时候的key,
对应的值为崩溃的相关信息。后面的流程中该类中的值会通过CrashReportPersister类写入file文件。

LogCatCollector

用来获取logcat日志中的相关信息,执行Logcat命令,读取命令输出信息。

class LogCatCollector{
    public String collectLogCat(){
        //根据所传参数不同组拼不同的logcat命令
        //主要组拼出的命令为
        //1.logcat -t 100 -v time 
        //2.logcat -t 100 -v time -b radio
        //1.logcat -t 100 -v time -b events
    }
}
logcat -b events

05-18 19:45:46.158 31191 31191 I auditd  : type=1400 audit(0.0:505001): avc: denied { search } for comm="PerfFgMonitor" name="1711" dev="proc" ino=18618 scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:r:radio:s0 tclass=dir permissive=0

logcat -b radio

05-18 19:44:39.343  1711  1785 D RILJ    : [9679]< RIL_REQUEST_GET_CELL_INFO_LIST [CellInfoWcdma:{mRegistered=YES mTimeStampType=oem_ril mTimeStamp=1283159923921792ns CellIdentityWcdma:{ mMcc=460 mMnc=1 mLac=53529 mCid=101852154 mPsc=438} CellSignalStrengthWcdma: ss=8 ber=99}] [SUB0]
05-18 19:44:39.345  1711  1975 D GsmSST  : [GsmSST] SST.getAllCellInfo(): X size=1 list=[CellInfoWcdma:{mRegistered=YES mTimeStampType=oem_ril mTimeStamp=1283159923921792ns CellIdentityWcdma:{ mMcc=460 mMnc=1 mLac=53529 mCid=101852154 mPsc=438} CellSignalStrengthWcdma: ss=8 ber=99}]
05-18 19:44:39.346  1711  1975 D GsmSST  : [GsmSST] getCellLocation(): X ret WCDMA info=[53529,101852154,438]
05-18 19:44:43.068  1711  1927 D SubscriptionController: [getPhoneId]- no sims, returning default phoneId=2147483647

其实相信大部分人不太清楚logcat的相关命令。
针对以上的三条命令做如下解释

logcat -t 100 -v time
-t 限制打印100行内容
-v time 设置日志输出格式。打印日志的为:打印日期->触发时间->优先级(E,W,V)->tag->出问题进程的pid
关于日志输出格式的介绍参见此处日志输出格式

logcat -b [options] 切换打印log的内容级别

  • radio radio/telephony相关log
  • events events-related相关log
  • main 默认的log

DropBoxCollector

通过DropBoxManager读取系统系统的日志信息
DropBoxManager,很多人应该也没接触过。
android系统实际上是有三种日志打印的。log EventLog DropBox,关于三种log的介绍参见此处。
三种log的介绍
关于DropBoxManager的相关内容可以参见此处。dropboxManager介绍

class DropBoxCollector{
    public String read(){
        //通过DropBoxService获取系统的DropBoxManager
        //读取所有预先定义的不同tag对应的日志内容
    }
}

ReportUtils

封装的各种工具类,用来获取系统相关的信息

public getAvailableInternalMemorySize(){
    //通过StatFs类获取可用内存block数量及每个block的size
    //block_size * free_block_count = 可用内存数
}

public getTotalInternalMemorySize(){
    //通过StatFs类获取所有内存block数量及每个block的size
    //block_size * total_block_count = 总内存数
}

public getDeviceId(){
    //通过TelephonyManager获取deviceId
    //GSM手机对应与IMEI
    //CDMA手机对应与ESN或MEID
}

public getApplicationFilePath(){
    //通过context.getFilesDir()获取当前app的绝对路径
    //'/data/user/0/yftx.net.oschina.git.gradlesample/files'
}

public getLocalIpAddress(){
    //通过NetworkInterface 获取当前设备的ip
}

public getTimeString(){
    //通过Calendar类获取当前时间
}

UUID

java.util包中提供的类,用来生成唯一字符串的类。

Installation

用来生成唯一身份串的类。

class Installation{
    void id(){
        //获取的id用来标记用户的身份。
        //具体算法可以参见android blog中的解释。
        //http://android-developers.blogspot.com/2011/03/identifying-app-installations.html
    }
}

ConfigurationCollector

通过反射系统的Configuration类,获取系统相关参数。

class ConfigurationCollector{
    void collectConfiguration(Context context){
        //通过 context.getResources().getConfiguration()获取configration对象,
        //并用反射获取该类中的相关信息
    }
}

DumpSysCollector

通过执行dumpsys meminfo xxxpid 来分析内存
关于dumpsys的介绍参见此:dumsys相关介绍

class DumpSysCollector{
    void collectMemInfo(){
    //执行dumsys 相关命令
    }
}

ReflectionCollector

相当于Util类,主要通过反射获取传过来的类的一些信息。

class ReflectionCollector{
    void collectConstants(){
        //通过反射获取系统的相关信息
        //acra中主要获取Build,Build.Version中的相关数据
    }
}

DisplayManagerCollector

主要用来获取手机显示相关的数据

class DisplayManagerCollector{
    void collectDisplays(){
        //通过Display类获取屏幕宽,高,方向等显示相关的参数
    }
}

DeviceFeaturesCollector

通过PackageManager获取系统相关特性。比如glEsVersion等

class DeviceFeaturesCollector{
    void getFeatures(){
        //通过PackageManager获取系统相关特性。比如glEsVersion等
    }
}

SettingsCollector

使用反射获取android.provider.Settings.x中的相关内容。

class SettingsCollector{
    void collectSystemSettings(){
        //获取系统Settings类中的相关信息
    }
    
    void collectSecureSettings(){
        //获取Settings.Secure中的相关信息
    }
    
    void collectGlobalSettings(){
        //获取Settings.Global中的相关信息
    }
}

LogFileCollector

获取用户自己保存的相关的log文件,使用该接口可以让acra结合logback-android这类类库相结合。
很多做android的同学都没有做过java web开发,并且android的Log接口也还算好用,再加上客户端编程和服务端编程系统的不同,所以可能理解不了logback-android这样库的意义。
实际上logback-android这类库主要就是可以指定log输出的位置,以及log的打印级别。
关于java开发中log的重要性可以参见此文章,java log的意义

MediaCodecListCollector

主要用来获取系统支持哪些音视频类型等媒体相关的。

ThreadCollector

获取崩溃线程的相关信息。

class ThreadCollector{
    void collect(Thread t){
        //获取线程t的相关信息,id,name,priority,groupName
    }
}

ACRA中用到的其他一些获取异常的方法

getStackTracehash(Throwable th){
    //通过组拼Error的className及MethodName生成的字符串
    //获取该字符串的hash值
    //服务端可以根据该值做崩溃分类
}

结语

本部分内容主要包括

  1. ACRA如何配置(服务端,客户端的配置)
  2. 崩溃信息相关内容如何采集,涉及到的关键类。

后面的部分会继续分析如何将生成的file发送到服务端。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,355评论 6 343
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,565评论 25 707
  • 寻找一种易于理解的一致性算法(扩展版) 摘要 Raft 是一种为了管理复制日志的一致性算法。它提供了和 Paxos...
    yflau阅读 876评论 0 1
  • 阿雷的鱿鱼阅读 193评论 0 0