@BindView一行代碼背后的故事-ButterKnife

前提

這篇文章呢主要講的是ButterKnife IOC框架背后的故事凡橱,雖然網(wǎng)上很多這樣的帖子,但是這篇細(xì)致到每個字段都會講解(version=8.5.1台汇,原理都一樣可能版本不同苛骨,有些內(nèi)部實行會有些不一樣)就當(dāng)埋點懸念吧。 @BindView一行代碼到底給我做了哪些事情苟呐。這個框架就是為了給我們省去每次的findViewById這一行讓你枯燥又乏味的代碼塊痒芝,到底他在后面都做了哪些故事呢!下面請聽我侃侃道來...

Annotation

哈哈哈哈上來就講原理牵素,不講原理那怎么才能知道背后的故事啊严衬,你說si不si啊笆呆!畢竟是個IOC框架 肯定要說到JAVA Annotation 這東西大家可以在日常的代碼塊經(jīng)惩剑看到的,這篇文章主要講的是ButterKnife背后的故事呢腰奋,我這里就不會詳細(xì)的解釋Annotation,只說這個框架中用的一些抱怔。如果想了解Annotation呢可以參考一下這篇文章:
傳送門Annotation

不論看沒看這篇文章劣坊,我先說一下怎么自定義注解,了解各基本的大概就可以看懂這篇文章了屈留。

元注解(Retention局冰,Target)

@Rentention 這個注解的意思是注解保留的時間,我們可以有以下三個選擇

1.SOURCE 源碼時保留灌危,這類 Annotation 大都用來校驗康二,比如 Override, Deprecated, SuppressWarnings

2.CLASS 肯定意思是編譯時,就是我們在項目java文件在編譯成class 的時候 apt 會自動解析 但需要做的是
- 自定義類繼承AbstractProcessor
- 重寫其中的process函數(shù)

這塊可能會有同學(xué)不理解勇蝙,實際是由apt在編譯時自動查找所有繼承來自AbstractProcessor的類沫勿,然后調(diào)用他們的process 方法去處理(我們這里的ButterKnife在這里就自定義了一個ButterKnifeProcessor 后面會詳細(xì)講解這個類)

3.RUNTIME 運行時保留,程序在運行過程中,使用這些 Annotation, 比如我們常用的 @Test产雹。

@Target表示注解可以用來修飾哪些元素诫惭。可選值包括 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER 等

ButterKnifeProcessor

由于我們的大神JakeWharton 每一個注解都是ClASS,所有java文件在編譯的時候ButterKnifeProcessorprocess就會被調(diào)用蔓挖。好現(xiàn)在我們開始解析源碼夕土。

  @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
  //這一行是根據(jù)env拿到所有帶有相關(guān)注解根據(jù)TypeElement進(jìn)行區(qū)分
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    //依次遍歷生成相應(yīng)的xxx_ViewBinding文件
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();
        
      JavaFile javaFile = binding.brewJava(sdk);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
  }


上面的代碼呢也不是太長,首選我們可以看到第一行創(chuàng)建了一個Map集合存放的key = TypeElement 而TypeElement是由RoundEnvironment通過

TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

如果不太明白Elements的意思

作用:Elements是處理Element的工具類,Element代表程序的元素瘟判,例如包怨绣、類或者方法,可以理解成源代碼;TypeElement代表的是源代碼中的類型元素拷获,例如類篮撑、域、方法等;從TypeElement中能獲取類的名字刀诬,但是你獲取不到類的信息咽扇,例如它的父類,這個需要從TypeMirror獲取陕壹,而TypeMirror需要調(diào)用Element的asType()函數(shù)

value = BindingSet 這個類意思是什么呢质欲。我們來看看源碼啊 下面貼出的是BindingSet他的Builder

 static final class Builder {
    private final TypeName targetTypeName;
    private final ClassName bindingClassName;
    private final boolean isFinal;
    private final boolean isView;
    private final boolean isActivity;
    private final boolean isDialog;

    private BindingSet parentBinding;
    //存儲(@BindView(id))這個id的
    private final Map<Id, ViewBinding.Builder> viewIdMap = new LinkedHashMap<>();
    private final ImmutableList.Builder<FieldCollectionViewBinding> collectionBindings =
        ImmutableList.builder();
    private final ImmutableList.Builder<ResourceBinding> resourceBindings = ImmutableList.builder();

    private Builder(TypeName targetTypeName, ClassName bindingClassName, boolean isFinal,
        boolean isView, boolean isActivity, boolean isDialog) {
      this.targetTypeName = targetTypeName;
      this.bindingClassName = bindingClassName;
      this.isFinal = isFinal;
      this.isView = isView;
      this.isActivity = isActivity;
      this.isDialog = isDialog;
    }


為什么貼出他的Builder呢,因為這樣更容易理解這個類干嘛的糠馆,他是保存一個類(當(dāng)前的Activity)里面到底有哪些關(guān)于ButterKnife的注解嘶伟。上面的viewIdMap就是用于存儲(@BindView(id))這個id的,我們在看看Builder這個內(nèi)部類的一些方法可能你會更理解他到底在做哪些事情

 //用于@BindView(R.id.test)
 void addField(Id id, FieldViewBinding binding) {
      getOrCreateViewBindings(id).setFieldBinding(binding);
    }

    void addFieldCollection(FieldCollectionViewBinding binding) {
      collectionBindings.add(binding);
    }

    //方法的bind
    boolean addMethod(
        Id id,
        ListenerClass listener,
        ListenerMethod method,
        MethodViewBinding binding) {
      ViewBinding.Builder viewBinding = getOrCreateViewBindings(id);
      if (viewBinding.hasMethodBinding(listener, method) && !"void".equals(method.returnType())) {
        return false;
      }
      viewBinding.addMethodBinding(listener, method, binding);
      return true;
    }
    //用于@BindBitmap @BindDimen...就是一些資源文件的bind
    void addResource(ResourceBinding binding) {
      resourceBindings.add(binding);
    }

從上面的代碼可以看到這個類BuilderSet到底干了些什么事吧又碌,就是把你添加注釋的這個類的信息保存下來九昧,后面做判斷,做代碼的生成毕匀。

說了這么多其實就是解釋process()第一行Map代碼到底是做什么的铸鹰,接下來我們看process()里面的循環(huán)到底干什么的。上面的代碼塊我也寫了一些注釋皂岔,說是生成對應(yīng)的xxx_ViewBinding文件的蹋笼。如何生成的呢?細(xì)心的同學(xué)會注意到那個里面的filer這個東西躁垛,其實這個是在我們初始化的時候的一些工具,下面是ButterKnife初始化的的一些操作

@Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    String sdk = env.getOptions().get(OPTION_SDK_INT);
    if (sdk != null) {
      try {
        this.sdk = Integer.parseInt(sdk);
      } catch (NumberFormatException e) {
        env.getMessager()
            .printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"
                + sdk
                + "'. Falling back to API 1 support.");
      }
    }
    //scan java文件每一個Element
    elementUtils = env.getElementUtils();
    //是用來處理TypeMirror的工具類
    typeUtils = env.getTypeUtils();
    //用來創(chuàng)建生成輔助文件
    filer = env.getFiler();
    try {
      trees = Trees.instance(processingEnv);
    } catch (IllegalArgumentException ignored) {
    }
  }

就是一些初始化操作剖毯。主要就elementUtils,typeUtils教馆,filer這個三個工具的初始化逊谋,具體干嘛的上面代碼我已經(jīng)寫了注釋了。

這個先告一段落(具體如何生成的我后面會講到)土铺。我們知道在java 文件編譯的時候ButterKnifeProcessor靠著process()這個方法生成了隊友的xxx_ViewBinding文件胶滋。那么問題來了板鬓,我們?nèi)绾伟堰@個文件和我們的添加了注解的文件(xxxActivity.java,后面就用xx代替了)綁定在一起呢。

如何綁定xxx_ViewBinding

相信大家用過BindKnife的人都知道镀钓,要在我們的BaseActivity里面或者當(dāng)前的Activity中bind(setContentView或者OnViewCreated之后做這個操作) 和 unBind一下穗熬。這個就是關(guān)鍵。這里就拿@BindView做列舉丁溅。廢話不多說上代碼

  @NonNull @UiThread
  public static Unbinder bind(@NonNull Activity target) {
    //獲取最外層View
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
  }

下面的是上方代碼createBinding的具體實現(xiàn)

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    //獲取當(dāng)前這個類
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    //通過這個類然后找到對于的xxx_ViewBinding文件的構(gòu)造方法
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

    if (constructor == null) {
      return Unbinder.EMPTY;
    }

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
        //初始化這個xxx_ViewBinding文件
      return constructor.newInstance(target, source);
    } catch (IllegalAccessException e) {
      ....
    }
  }

上面的代碼我已經(jīng)寫了注釋了唤蔗,可以看到最主要的代碼是findBindingConstructorForClass這個方法找到我們的這個當(dāng)前的這個Activity對于的xxx_ViewBinding 然后獲取他的構(gòu)造方法,然后初始窟赏,那我們進(jìn)入這個方法看看到底做了哪些操作妓柜。

 @Nullable @CheckResult @UiThread
  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
  //從集合中獲取這個xxx_ViewBinding的構(gòu)造函數(shù)(這個map用于緩存用下次就不需要下面的操作來獲取了)
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    //獲取clsName
    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 {
    //通過反射獲取這個xxx_ViewBinding的class然后獲取他的構(gòu)造函數(shù)
    //細(xì)心的同學(xué)可以看到這里面接受了兩個參數(shù),一個是這個cls的父類和當(dāng)前最外層的view
      Class<?> bindingClass = Class.forName(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);
    }
    //上面如果沒有從map集合中獲取涯穷,通過反射回去的會添加到集合中方便下次直接獲取棍掐。就是緩存的意思
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

上面的代碼看到了嗎?每行的注釋都有拷况,可以看到他是通過反射的方式拿到這個xxx_ViewBinding文件然后獲取構(gòu)造他的構(gòu)造方法的作煌。然后通過BINDINGS這個集合來做緩存,減少耗時操作畢竟用反射都很耗時的赚瘦。

接下來我們來看看生成的到底是一個什么樣的文件xxx_ViewBinding

public class CameraActivityRep_ViewBinding implements Unbinder {
  private CameraActivityRep target;

  @UiThread
  public CameraActivityRep_ViewBinding(CameraActivityRep target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public CameraActivityRep_ViewBinding(CameraActivityRep target, View source) {
    this.target = target;
    //就是findviewById
    target.modelPanorama = Utils.findRequiredViewAsType(source, R.id.model_panorama, "field 'modelPanorama'", ImageView.class);
    target.modelCapture = Utils.findRequiredViewAsType(source, R.id.model_capture, "field 'modelCapture'", ImageView.class);
   
  }

  @Override
  @CallSuper
  public void unbind() {
    CameraActivityRep target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;

    target.modelPanorama = null;
    target.modelCapture = null;
   
  }

看到這里我們終于看到了我們的findViewById在哪里了在他的生成文件的構(gòu)造函數(shù)中進(jìn)行的findViewById粟誓,ButterKnife.bind(this);這個的作用就是findViewById的作用,通過bind的方法獲取生成的xxx_ViewBinding文件起意,然后通過反射獲取構(gòu)造函數(shù)鹰服,到構(gòu)造函數(shù)的初始化。在構(gòu)造函數(shù)里面做了findViewById的操作揽咕。

其實大伙可能說我明明沒看到findViewById就看到了Utils.findRequiredViewAsType(source, R.id.model_panorama, "field 'modelPanorama'", ImageView.class)這行代碼悲酷,好我們接下來繼續(xù)看這個utils到底干了啥是不是findViewById

 public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
      Class<T> cls) {
      //MD我咋還沒看到呢繼續(xù)往下看
    View view = findRequiredView(source, id, who);
    return castView(view, id, who, cls);
  }

MD我咋還沒看到呢繼續(xù)往下看


 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.");
  }

好了同學(xué)們知道了吧,小伙子隱藏的可真深啊亲善。

mdzz 一句findViewById 引發(fā)了這么多東西 這就是@BindView背后不可告知的秘密有興趣的同學(xué)可以接著往下讀设易,看看他是如何生成xxx_ViewBinding的

如何生成xxx_ViewBinding

我們繼續(xù)回講一下剛剛的ButterKnifeProcessor那個process()這個方法不知道還記不記得里面的代碼我們就在貼一遍吧

  @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
  //這一行是根據(jù)env拿到所有帶有相關(guān)注解根據(jù)TypeElement進(jìn)行區(qū)分
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    //依次遍歷生成相應(yīng)的xxx_ViewBinding文件
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();
        
      JavaFile javaFile = binding.brewJava(sdk);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
  }


由于上面已經(jīng)講了這個方法里面的一些參數(shù)東西,我這里就不重復(fù)了蛹头,之說一些關(guān)鍵點

1.獲取帶有注解的所有Element然后把每個TypeElement對應(yīng)的BindingSet一一對應(yīng)存儲在Map中

findAndParseTargets(env)

2.生成對應(yīng)得xxx_ViewBinding文件

JavaFile javaFile = binding.brewJava(sdk);
javaFile.writeTo(filer);

我們可以看到這兩點亡嫌,我們先說第一個吧。既然是方法掘而,肯定要往方法里面走了,看看源碼在做一些什么東西于购。

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

    scanForRClasses(env);

    ......
    
    // 找到每個帶有 @BindView element 添加到集合中.
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      // we don't SuperficialValidation.validateElement(element)
      // so that an unresolved View type can be generated by later processing rounds
      try {
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    
    
    .......
    
    // 就是把一個<TypeElement, BindingSet.Builder> -> TypeElement, BindingSet
    //其實就是把一個activity的所有帶有@bind的注解存在在BindingSet中
    //然后返回給process()加工成文件
    Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
        new ArrayDeque<>(builderMap.entrySet());
    Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
    while (!entries.isEmpty()) {
      Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();

      TypeElement type = entry.getKey();
      BindingSet.Builder builder = entry.getValue();

      TypeElement parentType = findParentType(type, erasedTargetNames);
      if (parentType == null) {
        bindingMap.put(type, builder.build());
      } else {
        BindingSet parentBinding = bindingMap.get(parentType);
        if (parentBinding != null) {
          builder.setParent(parentBinding);
          bindingMap.put(type, builder.build());
        } else {
          // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
          entries.addLast(entry);
        }
      }
    }

    return bindingMap;
  }

這里代碼其實比這長多了袍睡,這里我就拿BindView注解講吧,其他的都類似的肋僧。把其他的都刪了不然代碼實在太長斑胜,我們可以看到上面的代碼通過env.getElementsAnnotatedWith(BindView.class)找到帶有@BindView的element然后遍歷循環(huán)控淡,然后接下來他通過一個方法parseBindView把這些Element做了一個些整理就是把一個Acitvity里面的所有注解對應(yīng)起來。我們來看看到底做了那些事情

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
      Set<TypeElement> erasedTargetNames) {
      //這句代碼的意思就是獲取拿到一個標(biāo)識(xxxAcitivty的意思)
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    
    ....
    
    // 獲取@BindView(R.id.test)獲取這個id的
    int id = element.getAnnotation(BindView.class).value();
    //拿到這個標(biāo)識對應(yīng)的BindingSet止潘,在BindingSet里面有個map存這個act里面有多少@bindview注解
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder != null) {
      String existingBindingName = builder.findExistingBindingName(getId(id));
      //如果發(fā)現(xiàn)這個id已經(jīng)存進(jìn)去了掺炭,直接return
      if (existingBindingName != null) {
        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 {
    //發(fā)現(xiàn)buildSet 的map中并沒有存這個id 那我們就把他添加進(jìn)去
      builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }

    String name = element.getSimpleName().toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

    builder.addField(getId(id), new FieldViewBinding(name, type, required));

    // Add the type-erased version to the valid binding targets set.
    erasedTargetNames.add(enclosingElement);
  }

上面解釋也寫了很多,我這邊就大概的講一下這個方法干嘛的凭戴,首先呢我們拿到傳進(jìn)來的builderMap涧狮,這個map對應(yīng)的是key = TypeElement(相當(dāng)于當(dāng)前Act的一個標(biāo)識) value = BindingSet.Builder(存儲著這個Act里面的所有注解),然后我們根據(jù)傳進(jìn)來的element 到map中查找看看這個Element對應(yīng)的BuildSet里面是否包含這個id么夫,如果包含了直接返回者冤,沒有的話拿到這個Element對應(yīng)的BuildSet 往里面添加這個id(通過 builder.addField)

這個就是BindView干的一些事情。這里就在總結(jié)一下上面的東西

  • 每一個TypeElement相當(dāng)于一個(Activity档痪,fragment涉枫,dialog)
  • 每一個BindingSet存儲了TypeElement里面所有包含注解的信息

這里就告一段落了,那我們看看代碼的生成腐螟,拿到BindSet生成對于的xxx_ViewBinding文件

binding.brewJava(sdk).writeTo(filer)

  JavaFile brewJava(int sdk) {
    return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
    //顧名思義添加注釋的意思
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  }

  public void writeTo(Filer filer) throws IOException {
    String fileName = packageName.isEmpty()
        ? typeSpec.name
        : packageName + "." + typeSpec.name;
    List<Element> originatingElements = typeSpec.originatingElements;
    JavaFileObject filerSourceFile = filer.createSourceFile(fileName,
        originatingElements.toArray(new Element[originatingElements.size()]));
    try (Writer writer = filerSourceFile.openWriter()) {
      writeTo(writer);
    } catch (Exception e) {
      try {
        filerSourceFile.delete();
      } catch (Exception ignored) {
      }
      throw e;
    }
  }

這里面代碼也挺多了我就不一一進(jìn)去講解了愿汰,這里用的是javapoet來進(jìn)行代碼的寫入的,感興趣的同學(xué)可以看一看多藝技不壓身乐纸。

ending

臥槽寫完了咋感覺頭懵懵的衬廷,但是還是希望這篇文章帶給你的是知識的提升而不是時間的浪費(畢竟寫了幾小時呢)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市锯仪,隨后出現(xiàn)的幾起案子泵督,更是在濱河造成了極大的恐慌,老刑警劉巖庶喜,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件小腊,死亡現(xiàn)場離奇詭異,居然都是意外死亡久窟,警方通過查閱死者的電腦和手機(jī)秩冈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來斥扛,“玉大人入问,你說我怎么就攤上這事∠“洌” “怎么了芬失?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長匾灶。 經(jīng)常有香客問我棱烂,道長,這世上最難降的妖魔是什么阶女? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任颊糜,我火速辦了婚禮哩治,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘衬鱼。我一直安慰自己业筏,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布鸟赫。 她就那樣靜靜地躺著蒜胖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惯疙。 梳的紋絲不亂的頭發(fā)上翠勉,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機(jī)與錄音霉颠,去河邊找鬼对碌。 笑死,一個胖子當(dāng)著我的面吹牛蒿偎,可吹牛的內(nèi)容都是我干的朽们。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼诉位,長吁一口氣:“原來是場噩夢啊……” “哼骑脱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起苍糠,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤叁丧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后岳瞭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拥娄,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年瞳筏,在試婚紗的時候發(fā)現(xiàn)自己被綠了稚瘾。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡姚炕,死狀恐怖摊欠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情柱宦,我是刑警寧澤些椒,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站掸刊,受9級特大地震影響免糕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一说墨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧苍柏,春花似錦尼斧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至熄捍,卻和暖如春烛恤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背余耽。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工缚柏, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人碟贾。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓币喧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親袱耽。 傳聞我的和親對象是個殘疾皇子杀餐,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 什么是注解注解分類注解作用分類 元注解 Java內(nèi)置注解 自定義注解自定義注解實現(xiàn)及使用編譯時注解注解處理器注解處...
    Mr槑閱讀 1,079評論 0 3
  • 前面寫了Android 開發(fā):由模塊化到組件化(一),很多小伙伴來問怎么沒有Demo啊?之所以沒有立刻放demo的...
    涅槃1992閱讀 8,034評論 4 37
  • 俗話說的好“不想偷懶的程序員,不是好程序員”朱巨,我們在日常開發(fā)android的過程中史翘,在前端activity或者fr...
    蛋西閱讀 4,966評論 0 14
  • 葛老師是我大學(xué)同學(xué),九十年代冀续,我們一起從師范屒矸恚科學(xué)校畢業(yè),她分回了家鄉(xiāng)沥阳,先是在一個叫大溪的農(nóng)村中學(xué)教書跨琳,三年后,調(diào)...
    楚山青青閱讀 732評論 0 0
  • 第一次來這里 適應(yīng)一下 好的話以后在這里發(fā)文喲
    草籽布拉格閱讀 212評論 0 0