ButterKnife源碼解析


image.png

序言

繼上次介紹了EventBus之后稀并,今天我們來介紹另外一個(gè)好用的三方庫(kù)ButterKnife仅颇,一個(gè)依賴注入框架。該庫(kù)的作者是被稱為Android之神的JakeWarton碘举,我們所熟知的OKHttp忘瓦、Retrofit、ButterKnife殴俱、RxAndroid、RxBinding都是他的杰作枚抵。
在使用ButterKnife之前线欲,我們寫過最多的代碼大概是:

TextView tv = (TextView) findViewById(R.id.tv);

而有了ButterKnife之后,我們就可以告別findViewById()汽摹,ButterKnife會(huì)自動(dòng)幫我們生成這些代碼李丰。接下來,抱著膜拜大神的態(tài)度逼泣,讓我們一起學(xué)習(xí)ButterKnife趴泌。

ButterKnife的使用

ButterKnife的使用流程如下:

  1. 在工程目錄下的build.gradle文件中添加ButterKnife插件路徑
classpath "com.jakewharton:butterknife-gradle-plugin:8.6.0"
  1. 在module的build.gradle文件中添加依賴和注解解析器:
api 'com.jakewharton:butterknife:8.8.1'
kapt 'com.jakewharton:butterknife-compiler:8.6.0'
  1. 接下來在Activity或者Fragment的onCreate/onCreateView方法中綁定當(dāng)前對(duì)象:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Unbinder binder = ButterKnife.bind(this);
}
  1. 然后在需要注入的View上加注解舟舒,此處以BindView注解為例:
@BindView(R.id.rbStock)
RadioButton rbStock;
//省略其他控件
  1. 現(xiàn)在,就可以在代碼中使用這些控件嗜憔,而不需要通過findViewById初始化這些控件秃励。
  2. 最后,在Activity的onDestroy方法中從ButterKnife解綁:
@Override
protected void onDestroy() {
    super.onDestroy();
    binder.unbind();
}

到這里吉捶,ButterKnife的使用方法介紹完了夺鲜,可以看到使用過程比較簡(jiǎn)單。

源碼分析

1. ButterKnife綁定Activity/Fragment

我們從ButterKnife綁定Activity入手呐舔,來分析原理币励。
這里,我們用ButterKnife綁定項(xiàng)目的MainActivity:

ButterKnife.bind(this);

跟蹤ButterKnife的bind方法:

@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
}

這里會(huì)獲取Activity對(duì)應(yīng)Window的DecorView珊拼,然后調(diào)用createBinding方法:

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
    
    if (constructor == null) {
      return Unbinder.EMPTY;
    }
    
    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target, source);
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InstantiationException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InvocationTargetException e) {
      Throwable cause = e.getCause();
      if (cause instanceof RuntimeException) {
        throw (RuntimeException) cause;
      }
      if (cause instanceof Error) {
        throw (Error) cause;
      }
      throw new RuntimeException("Unable to create binding instance.", cause);
    }
}

著重看一下findBindingConstructorForClass方法:

@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //noinspection unchecked
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
      if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
    } catch (ClassNotFoundException e) {
      if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
      bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
    } catch (NoSuchMethodException e) {
      throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
    }
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}

該方法接收一個(gè)Class對(duì)象食呻,即我們的MainActivity對(duì)應(yīng)的Class對(duì)象。首先會(huì)從BINDINGS中尋找是否存在構(gòu)造器澎现,BINDINGS定義如下:

@VisibleForTesting
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();

它是一個(gè)Map仅胞,鍵是一個(gè)Class對(duì)象,值是一個(gè)構(gòu)造器昔头,至于是什么的構(gòu)造器下面會(huì)揭曉答案饼问。
繼續(xù)上面的分析,如果在BINDINGS中找到構(gòu)造器揭斧,則直接返回構(gòu)造器莱革。如果沒有找到,就校驗(yàn)這個(gè)Class對(duì)象是否合法讹开,如果合法盅视,就執(zhí)行下面一句代碼:

Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");

加載一個(gè)名為MainActivity_ViewBinding的類,并且獲取它的構(gòu)造器旦万,并保存在BINDINGS中闹击,下次再bind這個(gè)類的時(shí)候,就直接從BINDINGS中獲取構(gòu)造器成艘,提升性能赏半。
MainActivity_ViewBinding是通過APT(編譯時(shí)解析技術(shù))生成的類。后面會(huì)介紹具體的生成過程淆两。

再回到createBinding方法断箫,獲取到MainActivity_ViewBinding的構(gòu)造器之后,會(huì)調(diào)用構(gòu)造器的newInstance方法構(gòu)建一個(gè)MainActivity_ViewBinding對(duì)象秋冰。我們看一下MainActivity_ViewBinding的定義:

public class MainActivity_ViewBinding implements Unbinder {
    private MainActivity target;
    
    @UiThread
    public MainActivity_ViewBinding(MainActivity target) {
      this(target, target.getWindow().getDecorView());
    }
    
    @UiThread
    public MainActivity_ViewBinding(MainActivity target, View source) {
      this.target = target;
    
      target.rbStock = Utils.findRequiredViewAsType(source, R.id.rbStock, "field 'rbStock'", RadioButton.class);
      //省略其他控件初始化代碼
    }
    
    @Override
    @CallSuper
    public void unbind() {
      MainActivity target = this.target;
      if (target == null) throw new IllegalStateException("Bindings already cleared.");
      this.target = null;
    
      target.rbStock = null;
      //省略重復(fù)其他控置空代碼
    }
}

這個(gè)類的構(gòu)造方法中初始化了帶@BindView注解的控件仲义,我們看一下具體初始化過程:

public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
      Class<T> cls) {
    View view = findRequiredView(source, id, who);
    return castView(view, id, who, cls);
}

public static View findRequiredView(View source, @IdRes int id, String who) {
    View view = source.findViewById(id);
    if (view != null) {
      return view;
    }
    String name = getResourceEntryName(source, id);
    throw new IllegalStateException("Required view '"
        + name
        + "' with ID "
        + id
        + " for "
        + who
        + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
        + " (methods) annotation.");
}

在findRequiredView方法中,我們看見了熟悉的findViewById方法,到這里相信大家都清楚了:ButterKnife的原理是通過APT技術(shù)在編譯過程生成了一個(gè)XX_ViewBinding類埃撵,之后在綁定的時(shí)候赵颅,會(huì)創(chuàng)建XX_ViewBinding對(duì)象,而XX_ViewBinding的構(gòu)造方法中會(huì)幫我們出初始化這些注解修飾的控件暂刘。

MainActivity_ViewBinding實(shí)現(xiàn)了Unbinder接口饺谬,我們看一下接口定義:

public interface Unbinder {
    @UiThread void unbind();
    
    Unbinder EMPTY = new Unbinder() {
      @Override public void unbind() { }
    };
}

很簡(jiǎn)單,只有一個(gè)unbind方法鸳惯,而MainActivity_ViewBinding在unbind方法中把注解對(duì)應(yīng)的控件和target都置為null商蕴,釋放資源。就是使用過程第6步在onDestroy中會(huì)調(diào)用該方法芝发。

2. 編譯時(shí)生成_ViewBinding類

接下來绪商,我們看一下怎樣通過APT技術(shù)生成MainActivity_ViewBinding類。了解APT的同學(xué)會(huì)知道辅鲸,主要是通過AbstractProcessor類在編譯過程中處理注解格郁。
下面,我們就看一下ButterKnife對(duì)應(yīng)注解處理類ButterKnifeProcessor

@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {
    private static final String OPTION_SDK_INT = "butterknife.minSdk";
    static final Id NO_ID = new Id(-1);
    static final String VIEW_TYPE = "android.view.View";
    static final String ACTIVITY_TYPE = "android.app.Activity";
    static final String DIALOG_TYPE = "android.app.Dialog";
    private static final String COLOR_STATE_LIST_TYPE = "android.content.res.ColorStateList";
    private static final String BITMAP_TYPE = "android.graphics.Bitmap";
    private static final String DRAWABLE_TYPE = "android.graphics.drawable.Drawable";
    private static final String TYPED_ARRAY_TYPE = "android.content.res.TypedArray";
    private static final String NULLABLE_ANNOTATION_NAME = "Nullable";
    private static final String STRING_TYPE = "java.lang.String";
    private static final String LIST_TYPE = List.class.getCanonicalName();
    private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(OnCheckedChanged.class, OnClick.class, OnEditorAction.class, OnFocusChange.class, OnItemClick.class, OnItemLongClick.class, OnItemSelected.class, OnLongClick.class, OnPageChange.class, OnTextChanged.class, OnTouch.class);
    private static final List<String> SUPPORTED_TYPES = Arrays.asList("array", "attr", "bool", "color", "dimen", "drawable", "id", "integer", "string");
    private Elements elementUtils;
    private Types typeUtils;
    private Filer filer;
    private Trees trees;
    private int sdk = 1;
    private final Map<QualifiedId, Id> symbols = new LinkedHashMap();

    public ButterKnifeProcessor() {
    }

    //初始化一些參數(shù)和工具類
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        String sdk = (String)env.getOptions().get("butterknife.minSdk");
        if (sdk != null) {
            try {
                this.sdk = Integer.parseInt(sdk);
            } catch (NumberFormatException var5) {
                env.getMessager().printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '" + sdk + "'. Falling back to API 1 support.");
            }
        }

        this.elementUtils = env.getElementUtils();
        this.typeUtils = env.getTypeUtils();
        this.filer = env.getFiler();

        try {
            this.trees = Trees.instance(this.processingEnv);
        } catch (IllegalArgumentException var4) {
            ;
        }

    }

    //獲取該P(yáng)rocessor能處理的注解集合
    private Set<Class<? extends Annotation>> getSupportedAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet();
        annotations.add(BindArray.class);
        annotations.add(BindBitmap.class);
        annotations.add(BindBool.class);
        annotations.add(BindColor.class);
        annotations.add(BindDimen.class);
        annotations.add(BindDrawable.class);
        annotations.add(BindFloat.class);
        annotations.add(BindInt.class);
        annotations.add(BindString.class);
        annotations.add(BindView.class);
        annotations.add(BindViews.class);
        annotations.addAll(LISTENERS);
        return annotations;
    }

    //主要的處理方法独悴,分析將從這里開始
    public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
        Map<TypeElement, BindingSet> bindingMap = this.findAndParseTargets(env);
        Iterator var4 = bindingMap.entrySet().iterator();

        while(var4.hasNext()) {
            Entry<TypeElement, BindingSet> entry = (Entry)var4.next();
            TypeElement typeElement = (TypeElement)entry.getKey();
            BindingSet binding = (BindingSet)entry.getValue();
            JavaFile javaFile = binding.brewJava(this.sdk);

            try {
                javaFile.writeTo(this.filer);
            } catch (IOException var10) {
                this.error(typeElement, "Unable to write binding for type %s: %s", typeElement, var10.getMessage());
            }
        }

        return false;
    }
    
    //省略很多方法
}
    

我們就直接從process方法開始分析例书,這個(gè)方法的邏輯比較簡(jiǎn)單,通過findAndParseTargets方法找到所有使用ButterKnife的類刻炒,然后根據(jù)注解信息生成對(duì)應(yīng)的_ViewBinding類决采。
可以把這段代碼分為兩個(gè)過程:處理注解并記錄信息生成_ViewBinding類

a. 處理注解并記錄信息

先看一下findAndParseTargets方法:

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, Builder> builderMap = new LinkedHashMap();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet();
    this.scanForRClasses(env);

    //省略處理其他注解的代碼
    
    var4 = env.getElementsAnnotatedWith(BindView.class).iterator();

    while(var4.hasNext()) {
        element = (Element)var4.next();

        try {
            this.parseBindView(element, builderMap, erasedTargetNames);
        } catch (Exception var12) {
            this.logParsingError(element, BindView.class, var12);
        }
    }

    //省略處理其他注解的代碼

    var4 = LISTENERS.iterator();

    while(var4.hasNext()) {
        Class<? extends Annotation> listener = (Class)var4.next();
        this.findAndParseListener(env, listener, builderMap, erasedTargetNames);
    }

    Deque<Entry<TypeElement, Builder>> entries = new ArrayDeque(builderMap.entrySet());
    LinkedHashMap bindingMap = new LinkedHashMap();

    while(!entries.isEmpty()) {
        Entry<TypeElement, Builder> entry = (Entry)entries.removeFirst();
        TypeElement type = (TypeElement)entry.getKey();
        Builder builder = (Builder)entry.getValue();
        TypeElement parentType = this.findParentType(type, erasedTargetNames);
        if (parentType == null) {
            bindingMap.put(type, builder.build());
        } else {
            BindingSet parentBinding = (BindingSet)bindingMap.get(parentType);
            if (parentBinding != null) {
                builder.setParent(parentBinding);
                bindingMap.put(type, builder.build());
            } else {
                entries.addLast(entry);
            }
        }
    }

    return bindingMap;
}

這個(gè)方法很長(zhǎng)坟奥,主要的作用就是找到項(xiàng)目中所有使用ButterKnife注解的類树瞭,分別處理他們,最后把他們的信息保存在一個(gè)Map中:

LinkedHashMap<ElementType, BindingSet> bindingMap = new LinkedHashMap();

這個(gè)Map以ElementType(使用注解的類)為key爱谁,值則是一個(gè)BindingSet對(duì)象(記錄了類中用到的注解及作用對(duì)象等信息)晒喷。后面就是通過這個(gè)Map生成對(duì)應(yīng)的_ViewBinding類。
接下來访敌,我們分析一下注解BindView的處理過程凉敲,主要是通過parseBindView方法處理:

private void parseBindView(Element element, Map<TypeElement, Builder> builderMap, Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement)element.getEnclosingElement();
    //檢查是否滿足使用注解的條件
    boolean hasError = this.isInaccessibleViaGeneratedCode(BindView.class, "fields", element) || this.isBindingInWrongPackage(BindView.class, element);
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
        TypeVariable typeVariable = (TypeVariable)elementType;
        elementType = typeVariable.getUpperBound();
    }

    Name qualifiedName = enclosingElement.getQualifiedName();
    Name simpleName = element.getSimpleName();
    //不滿足條件,拋出異常
    if (!isSubtypeOfType(elementType, "android.view.View") && !this.isInterface(elementType)) {
        if (elementType.getKind() == TypeKind.ERROR) {
            this.note(element, "@%s field with unresolved type (%s) must elsewhere be generated as a View or interface. (%s.%s)", BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
        } else {
            this.error(element, "@%s fields must extend from View or be an interface. (%s.%s)", BindView.class.getSimpleName(), qualifiedName, simpleName);
            hasError = true;
        }
    }
    //滿足條件寺旺,開始解析注解
    if (!hasError) {
        int id = ((BindView)element.getAnnotation(BindView.class)).value();
        //從緩存builderMap中查找注解使用類對(duì)應(yīng)的builder對(duì)象
        Builder builder = (Builder)builderMap.get(enclosingElement);
        QualifiedId qualifiedId = this.elementToQualifiedId(element, id);
        String existingBindingName;
        if (builder != null) {
            existingBindingName = builder.findExistingBindingName(this.getId(qualifiedId));
            if (existingBindingName != null) {
                this.error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)", BindView.class.getSimpleName(), id, existingBindingName, enclosingElement.getQualifiedName(), element.getSimpleName());
                return;
            }
        } else {
            //如果緩存中沒有找到注解使用類對(duì)應(yīng)的builder對(duì)象爷抓,則創(chuàng)建這個(gè)對(duì)象,并且保存在builderMap中
            builder = this.getOrCreateBindingBuilder(builderMap, enclosingElement);
        }

        existingBindingName = simpleName.toString();
        TypeName type = TypeName.get(elementType);
        boolean required = isFieldRequired(element);
        //把注解BindView修飾的字段添加到builder中
        builder.addField(this.getId(qualifiedId), new FieldViewBinding(existingBindingName, type, required));
        erasedTargetNames.add(enclosingElement);
    }
}

代碼中關(guān)鍵的地方給出了注釋阻塑,各位同學(xué)可以根據(jù)注釋自行理解代碼蓝撇。其他注解的處理方式跟注解BindView類似,這里不做介紹叮姑。就這樣唉地,把類和它對(duì)應(yīng)的注解信息都保存到Map中。

b. 生成_ViewBinding類

我們?cè)賮砜匆幌聀rocess方法:

public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    Map<TypeElement, BindingSet> bindingMap = this.findAndParseTargets(env);
    Iterator var4 = bindingMap.entrySet().iterator();

    while(var4.hasNext()) {
        Entry<TypeElement, BindingSet> entry = (Entry)var4.next();
        TypeElement typeElement = (TypeElement)entry.getKey();
        BindingSet binding = (BindingSet)entry.getValue();
        JavaFile javaFile = binding.brewJava(this.sdk);

        try {
            javaFile.writeTo(this.filer);
        } catch (IOException var10) {
            this.error(typeElement, "Unable to write binding for type %s: %s", typeElement, var10.getMessage());
        }
    }

    return false;
}

通過第一步得到了bindingMap對(duì)象传透,接下來遍歷bindingMap耘沼,通過BindingSet的brewJava方法生成對(duì)應(yīng)的ViewBinding類:

JavaFile brewJava(int sdk) {
    return JavaFile.builder(this.bindingClassName.packageName(), this.createType(sdk)).addFileComment("Generated code from Butter Knife. Do not modify!", new Object[0]).build();
}

static BindingSet.Builder newBuilder(TypeElement enclosingElement) {
    TypeMirror typeMirror = enclosingElement.asType();
    boolean isView = ButterKnifeProcessor.isSubtypeOfType(typeMirror, "android.view.View");
    boolean isActivity = ButterKnifeProcessor.isSubtypeOfType(typeMirror, "android.app.Activity");
    boolean isDialog = ButterKnifeProcessor.isSubtypeOfType(typeMirror, "android.app.Dialog");
    TypeName targetType = TypeName.get(typeMirror);
    if (targetType instanceof ParameterizedTypeName) {
        targetType = ((ParameterizedTypeName)targetType).rawType;
    }

    String packageName = MoreElements.getPackage(enclosingElement).getQualifiedName().toString();
    String className = enclosingElement.getQualifiedName().toString().substring(packageName.length() + 1).replace('.', '$');
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding", new String[0]);
    boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
    return new BindingSet.Builder((TypeName)targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
}

這里就不深入介紹生成ViewBinding類的過程,它使用到了javapoet技術(shù)朱盐,javapoet技術(shù)是square公司開源的群嗤,專門用來生成java源碼,很多使用APT技術(shù)的庫(kù)都是使用到j(luò)avapoet技術(shù)兵琳。

總結(jié)

到這里狂秘,ButterKnife的原理我們就介紹完了,各位同學(xué)按照本文思路躯肌,對(duì)著ButterKnife源碼進(jìn)行學(xué)習(xí)者春。
如果各位同學(xué)使用過kotlin,就知道kotlin中不需要ButterKnife清女,kotlin提供了插件钱烟,可以直接獲取xml中的控件,還提供了一個(gè)功能強(qiáng)大又易用的庫(kù)anko嫡丙,有興趣的小伙伴可以了解一下拴袭。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市曙博,隨后出現(xiàn)的幾起案子拥刻,更是在濱河造成了極大的恐慌,老刑警劉巖父泳,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件般哼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡尘吗,警方通過查閱死者的電腦和手機(jī)逝她,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來睬捶,“玉大人黔宛,你說我怎么就攤上這事∏苊常” “怎么了臀晃?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)介劫。 經(jīng)常有香客問我徽惋,道長(zhǎng),這世上最難降的妖魔是什么座韵? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任险绘,我火速辦了婚禮踢京,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宦棺。我一直安慰自己瓣距,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布代咸。 她就那樣靜靜地躺著蹈丸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪呐芥。 梳的紋絲不亂的頭發(fā)上逻杖,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音思瘟,去河邊找鬼荸百。 笑死,一個(gè)胖子當(dāng)著我的面吹牛滨攻,可吹牛的內(nèi)容都是我干的管搪。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼铡买,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼更鲁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起奇钞,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤澡为,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后景埃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體媒至,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年谷徙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拒啰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡完慧,死狀恐怖谋旦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情屈尼,我是刑警寧澤册着,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站脾歧,受9級(jí)特大地震影響甲捏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鞭执,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一司顿、第九天 我趴在偏房一處隱蔽的房頂上張望芒粹。 院中可真熱鬧,春花似錦大溜、人聲如沸是辕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至旁蔼,卻和暖如春锨苏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背棺聊。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工伞租, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人限佩。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓葵诈,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親祟同。 傳聞我的和親對(duì)象是個(gè)殘疾皇子作喘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345