Butterknife 8.8.1源碼解析

一、本文需要解決的問(wèn)題

我研究Butterknife源碼的目的是為了解決以下幾個(gè)我在使用過(guò)程中所思考的問(wèn)題:

  1. 在很多文章中都提到Butterknife使用編譯時(shí)注解技術(shù)婿奔,什么是編譯時(shí)注解睦尽?
  2. 是完全不調(diào)用findViewById()等方法了嗎器净?
  3. 為什么綁定各種view時(shí)不能使用private修飾?
  4. 綁定監(jiān)聽(tīng)事件的時(shí)候方法命名有限制嗎当凡?

二掌动、初步分析

基于Butterknife 8.8.1版本。
為了更好地分析代碼宁玫,我寫(xiě)了一個(gè)demo:
MainActivity.java:

public class MainActivity extends Activity {

    @BindView(R.id.text)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
    }

    @OnClick(R.id.text)
    public void textClick() {
        Toast.makeText(MainActivity.this, "textview clicked", Toast.LENGTH_LONG);
    }
}

我們從Butterknife.bind()方法,即方法入口開(kāi)始分析:
ButterKnife#bind():

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

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());
    // 8躺埂E繁瘛!
    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);
    }
}

@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 {
      // 3自蕖7鹨础!
      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;
}

代碼還是比較清晰的涌庭,bind()方法的流程:

  1. 首先獲取當(dāng)前activity的sourceView芥被,其實(shí)就是獲取Activity的DecorView,DecorView是整個(gè)ViewTree的最頂層View坐榆,包含標(biāo)題view和內(nèi)容view這兩個(gè)子元素拴魄。我們一直調(diào)用的setContentView()方法其實(shí)就是往內(nèi)容view中添加view元素。
  2. 然后調(diào)用createBinding() --> findBindingConstructorForClass()席镀,重點(diǎn)是
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
BINDINGS.put(cls, bindingCtor);

按照所寫(xiě)的代碼匹中,這里會(huì)加載一個(gè)MainActivity_ViewBinding類(lèi),然后獲取這個(gè)類(lèi)里面的雙參數(shù)(Activity豪诲, View)構(gòu)造方法顶捷,最后放在BINDINGS里面,它是一個(gè)map屎篱,主要作用是緩存服赎。在下次使用的時(shí)候葵蒂,就可以從緩存中獲取到:

Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
    if (debug) Log.d(TAG, "HIT: Cached in binding map.");
    return bindingCtor;
}

三、關(guān)于編譯時(shí)注解

在上面分析過(guò)程中重虑,我們知道最后我們會(huì)去加載一個(gè)MainActivity_ViewBinding類(lèi)践付,而這個(gè)類(lèi)并不是我們自己編寫(xiě)的,而是通過(guò)編譯時(shí)注解(APT - Annotation Processing Tool)的技術(shù)生成的嚎尤。
這一節(jié)將會(huì)介紹一下這個(gè)技術(shù)荔仁。

1、什么是注解

注解其實(shí)很常見(jiàn)芽死,比如說(shuō)Activity自動(dòng)生成的onCreate()方法上面就有一個(gè)@Override注解


image.png
  • 注解的概念:
    能夠添加到 Java 源代碼的語(yǔ)法元數(shù)據(jù)乏梁。類(lèi)、方法关贵、變量遇骑、參數(shù)、包都可以被注解揖曾,可用來(lái)將信息元數(shù)據(jù)與程序元素進(jìn)行關(guān)聯(lián)落萎。
  • 注解的分類(lèi):
    • 標(biāo)準(zhǔn)注解,如Override炭剪, Deprecated练链,SuppressWarnings等
    • 元注解,如@Retention, @Target, @Inherited, @Documented奴拦。當(dāng)我們要自定義注解時(shí)媒鼓,需要使用它們
    • 自定義注解,表示自己根據(jù)需要定義的 Annotation
  • 注解的作用:
    • 標(biāo)記错妖,用于告訴編譯器一些信息
    • 編譯時(shí)動(dòng)態(tài)處理绿鸣,如動(dòng)態(tài)生成java代碼
    • 運(yùn)行時(shí)動(dòng)態(tài)處理,如得到注解信息
2暂氯、運(yùn)行時(shí)注解 vs 編譯時(shí)注解

一般有些人提到注解潮模,普遍就會(huì)覺(jué)得性能低下。但是真正使用注解的開(kāi)源框架卻很多例如ButterKnife痴施,Retrofit等等擎厢。所以注解是好是壞呢?
首先晾剖,并不是注解就等于性能差锉矢。更確切的說(shuō)是運(yùn)行時(shí)注解這種方式,由于它的原理是java反射機(jī)制齿尽,所以的確會(huì)造成較為嚴(yán)重的性能問(wèn)題沽损。
但是像Butterknife這個(gè)框架,它使用的技術(shù)是編譯時(shí)注解循头,它不會(huì)影響app實(shí)際運(yùn)行的性能(影響的應(yīng)該是編譯時(shí)的效率)绵估。
一句話總結(jié):

  • 運(yùn)行時(shí)注解就是在應(yīng)用運(yùn)行的過(guò)程中炎疆,動(dòng)態(tài)地獲取相關(guān)類(lèi),方法国裳,參數(shù)等信息形入,由于使用java反射機(jī)制,性能會(huì)有問(wèn)題缝左;
  • 編譯時(shí)注解由于是在代碼編譯過(guò)程中對(duì)注解進(jìn)行處理亿遂,通過(guò)注解獲取相關(guān)類(lèi),方法渺杉,參數(shù)等信息蛇数,然后在項(xiàng)目中生成代碼,運(yùn)行時(shí)調(diào)用是越,其實(shí)和直接運(yùn)行手寫(xiě)代碼沒(méi)有任何區(qū)別耳舅,也就沒(méi)有性能問(wèn)題了。
    這樣我們就解決了第一個(gè)問(wèn)題倚评。
3浦徊、如何使用編譯時(shí)注解技術(shù)

這里要借助到一個(gè)類(lèi):AbstractProcessor

public class TestProcessor extends AbstractProcessor  
{    
    @Override  
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)  
    {  
        // TODO Auto-generated method stub  
        return false;  
    }    
}  

重點(diǎn)是process()方法,它相當(dāng)于每個(gè)處理器的主函數(shù)main()天梧,可以在這里寫(xiě)相關(guān)的掃描和處理注解的代碼盔性,他會(huì)幫助生成相關(guān)的Java文件。后面我們可以具體看一下Butterknife中的使用呢岗。

四纯出、進(jìn)一步分析MainActivity_ViewBinding

我們了解了編譯時(shí)注解的基本概念之后,我們先看一下MainActivity_ViewBinding類(lèi)具體實(shí)現(xiàn)了什么敷燎。
在編寫(xiě)完demo之后,需要先build一下項(xiàng)目箩言,之后可以在build/generated/source/apt/debug/包名/下面找到這個(gè)類(lèi)硬贯,如圖所示:



接上面的分析,到最后會(huì)通過(guò)反射的方式去調(diào)用MainActivity_ViewBinding的構(gòu)造方法陨收。我們直接看這個(gè)類(lèi)的構(gòu)造方法:

@UiThread
public MainActivity_ViewBinding(final MainActivity target, View source) {
    this.target = target;

    View view;
    // 1
    view = Utils.findRequiredView(source, R.id.text, "field 'textView' and method 'textClick'");
    // 2
    target.textView = Utils.castView(view, R.id.text, "field 'textView'", TextView.class);
    // 3
    view2131165290 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
        @Override
        public void doClick(View p0) {
            target.textClick();
        }
    });
}
1饭豹、findRequiredView()
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.");
  }

看到這里我們已經(jīng)解決了第二個(gè)問(wèn)題:到最后還是會(huì)調(diào)用findViewById()方法,并沒(méi)有完全舍棄這個(gè)方法务漩,這里的source代表著在上面代碼中傳入的MainActivity的DecorView拄衰。大家可以嘗試一下將Activity轉(zhuǎn)化為Fragment的情況~

2、Util.castView

在這里饵骨,我們解決了第三個(gè)問(wèn)題翘悉,綁定各種view時(shí)不能使用private修飾,而是需要用public或default去修飾居触,因?yàn)槿绻捎胮rivate修飾的話妖混,將無(wú)法通過(guò)對(duì)象.成員變量方式獲取到我們需要綁定的View老赤。
Util#castView():

public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
    try {
      return cls.cast(view);
    } catch (ClassCastException e) {
      String name = getResourceEntryName(view, id);
      throw new IllegalStateException("View '"
          + name
          + "' with ID "
          + id
          + " for "
          + who
          + " was of the wrong type. See cause for more info.", e);
    }
}

這里直接調(diào)用Class.cast強(qiáng)制轉(zhuǎn)換類(lèi)型,將View轉(zhuǎn)化為我們需要的view(TextView)制市。

3抬旺、
view2131165290 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
    @Override
    public void doClick(View p0) {
        target.textClick();
    }
});

這里會(huì)生成一個(gè)成員變量來(lái)保存我們需要綁定的View,重點(diǎn)是下面它會(huì)調(diào)用setOnClickListener()方法祥楣,傳入的是DebouncingOnClickListener:

/**
 * A {@linkplain View.OnClickListener click listener} that debounces multiple clicks posted in the
 * same frame. A click on one button disables all buttons for that frame.
 */
public abstract class DebouncingOnClickListener implements View.OnClickListener {
    static boolean enabled = true;

    private static final Runnable ENABLE_AGAIN = new Runnable() {
        @Override public void run() {
            enabled = true;
        }
    };

    @Override 
    public final void onClick(View v) {
        if (enabled) {
            enabled = false;
            v.post(ENABLE_AGAIN);
            doClick(v);
        }
    }

    public abstract void doClick(View v);
}

這個(gè)DebouncingOnClickListener是View.OnClickListener的一個(gè)子類(lèi)开财,作用是防止一定時(shí)間內(nèi)對(duì)view的多次點(diǎn)擊,即防止快速點(diǎn)擊控件所帶來(lái)的一些不可預(yù)料的錯(cuò)誤误褪。個(gè)人認(rèn)為這個(gè)類(lèi)寫(xiě)的非常巧妙责鳍,既完美解決了問(wèn)題,又寫(xiě)的十分優(yōu)雅振坚,一點(diǎn)都不臃腫薇搁。
這里抽象了doClick()方法,實(shí)現(xiàn)代碼中是直接調(diào)用了target.textClick()渡八,這里解決了第四個(gè)問(wèn)題:綁定監(jiān)聽(tīng)事件的時(shí)候方法命名是沒(méi)有限制的啃洋,不一定需要嚴(yán)格命名為onClick,也不一定需要傳入View參數(shù)屎鳍。

五宏娄、MainActivity_ViewBinding的生成

上文提到,MainActivity_ViewBinding類(lèi)是通過(guò)編譯時(shí)注解技術(shù)生成的逮壁,我們找到Butterknife相關(guān)的繼承于AbstractProcessor的類(lèi)孵坚,ButterKnifeProcessor,我們直接看process()方法:

public final class ButterKnifeProcessor extends AbstractProcessor {
    @Override 
    public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
        // 1
        Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

        for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
            TypeElement typeElement = entry.getKey();
            BindingSet binding = entry.getValue();

            JavaFile javaFile = binding.brewJava(sdk, debuggable);
            try {
                javaFile.writeTo(filer);
            } catch (IOException e) {
                error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
            }
        }

        return false;
    }
}

1窥淆、findAndParseTargets()
這個(gè)方法的作用是處理所有的@BindXX注解卖宠,我們直接看處理@BindView的部分:

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    // 省略代碼
    // Process each @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);
        }
    }
    // 省略代碼
}

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
      Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Start by verifying common generated code restrictions.
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);

    // Verify that the target type extends from View.
    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, VIEW_TYPE) && !isInterface(elementType)) {
        if (elementType.getKind() == TypeKind.ERROR) {
            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 {
            error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
                  BindView.class.getSimpleName(), qualifiedName, simpleName);
            hasError = true;
        }
    }

    if (hasError) {
        return;
    }

    // Assemble information on the field.
    int id = element.getAnnotation(BindView.class).value();

    BindingSet.Builder builder = builderMap.get(enclosingElement);
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    if (builder != null) {
      String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
      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 {
      builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }

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

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

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

代碼邏輯是處理獲取相關(guān)注解的信息,比如綁定的資源id等等忧饭,然后通過(guò)獲取BindingSet.Builder類(lèi)的實(shí)例來(lái)創(chuàng)建一一對(duì)應(yīng)的關(guān)系扛伍,這里有一個(gè)判斷,如果builderMap存在相應(yīng)實(shí)例則直接取出builder词裤,否則通過(guò)getOrCreateBindingBuilder()方法生成一個(gè)新的builder刺洒,最后調(diào)用builder.addField()方法。

后續(xù)的話返回到findAndParseTargets()方法的最后一部分:

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    // bindView()     

    // Associate superclass binders with their subclass binders. This is a queue-based tree walk
    // which starts at the roots (superclasses) and walks to the leafs (subclasses).
    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;
}

這里會(huì)生成一個(gè)bindingMap吼砂,key為T(mén)ypeElement逆航,代表注解元素類(lèi)型,value為BindSet類(lèi)渔肩,通過(guò)上述的builder.build()生成因俐,BindingSet類(lèi)中存儲(chǔ)了很多信息,例如綁定view的類(lèi)型,生成類(lèi)的className等等女揭,方便我們后續(xù)生成java文件蚤假。最后回到process方法:

@Override 
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();

      JavaFile javaFile = binding.brewJava(sdk, debuggable);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
}

最后通過(guò)brewJava()方法生成java代碼。
這里使用到的是javapoet吧兔。javapoet是一個(gè)開(kāi)源庫(kù)磷仰,通過(guò)處理相應(yīng)注解來(lái)生成最后的java文件,這里是項(xiàng)目地址傳送門(mén)境蔼,具體技術(shù)不再分析灶平。

這篇文章會(huì)同步到我的個(gè)人日志,如有問(wèn)題箍土,請(qǐng)大家踴躍提出逢享,謝謝大家!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末吴藻,一起剝皮案震驚了整個(gè)濱河市瞒爬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沟堡,老刑警劉巖侧但,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異航罗,居然都是意外死亡禀横,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)粥血,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)柏锄,“玉大人,你說(shuō)我怎么就攤上這事复亏≈和蓿” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵缔御,是天一觀的道長(zhǎng)茫舶。 經(jīng)常有香客問(wèn)我,道長(zhǎng)刹淌,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任讥耗,我火速辦了婚禮有勾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘古程。我一直安慰自己蔼卡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布挣磨。 她就那樣靜靜地躺著雇逞,像睡著了一般荤懂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上塘砸,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天节仿,我揣著相機(jī)與錄音,去河邊找鬼掉蔬。 笑死廊宪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的女轿。 我是一名探鬼主播箭启,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蛉迹!你這毒婦竟也來(lái)了傅寡?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤北救,失蹤者是張志新(化名)和其女友劉穎荐操,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體扭倾,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡淀零,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了膛壹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片驾中。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖模聋,靈堂內(nèi)的尸體忽然破棺而出肩民,到底是詐尸還是另有隱情,我是刑警寧澤链方,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布持痰,位于F島的核電站,受9級(jí)特大地震影響祟蚀,放射性物質(zhì)發(fā)生泄漏工窍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一前酿、第九天 我趴在偏房一處隱蔽的房頂上張望患雏。 院中可真熱鬧,春花似錦罢维、人聲如沸淹仑。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)匀借。三九已至颜阐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間吓肋,已是汗流浹背凳怨。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蓬坡,地道東北人猿棉。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像屑咳,于是被迫代替她去往敵國(guó)和親萨赁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,163評(píng)論 25 707
  • 俗話說(shuō)的好“不想偷懶的程序員兆龙,不是好程序員”杖爽,我們?cè)谌粘i_(kāi)發(fā)android的過(guò)程中,在前端activity或者fr...
    蛋西閱讀 4,966評(píng)論 0 14
  • 本文主要介紹Android之神JakeWharton的一個(gè)注解框架紫皇,聽(tīng)說(shuō)現(xiàn)在面試官現(xiàn)在面試都會(huì)問(wèn)知不知道JakeW...
    Zeit丶閱讀 981評(píng)論 4 6
  • 我在歸途中慰安,而你,正在離開(kāi)聪铺。我們終究不是一個(gè)世界的人化焕,我們的距離也越來(lái)越遠(yuǎn)……先把我拽進(jìn)河里的是你,掉在河里出不來(lái)...
    00素安閱讀 118評(píng)論 0 1