談?wù)凙ndroid AOP技術(shù)方案

理解AOP

之前幾篇文章我們詳細介紹了AOP的幾種技術(shù)方案偿凭,由于AOP技術(shù)復(fù)雜多樣,實際需求也不盡相同弯囊,那么我們應(yīng)該如何做技術(shù)選型呢?

本篇將會對現(xiàn)有的AOP技術(shù)做一個統(tǒng)一的介紹胶果,尤其側(cè)重在Android方向的落地,希望對你有所幫助霎烙,文中內(nèi)容蕊连、示例大都來自工作總結(jié),如有偏頗不妥尝蠕,歡迎指正。

這里先統(tǒng)一一下基本名詞载庭,以便表述看彼。

  • 切面: 對一類行為的抽象廊佩,是切點的集合,比如在用戶訪問所有模塊前做的權(quán)限認證靖榕。
  • 切點: 描述切面的具體的一個業(yè)務(wù)場景标锄。
  • 通知(Advice)類型: 通常分為切點前、切點后和切點內(nèi)茁计,比如在方法前織入代碼是指切點前鸯绿。

AOP是一種面向切面編程的技術(shù)的統(tǒng)稱,AOP框架最終都會圍繞class字節(jié)碼的操作展開簸淀,無論是對字節(jié)碼的操作增刪改瓶蝴,為方便描述,我們統(tǒng)稱為代碼的織入租幕。

雖然AOP翻譯過來叫面向切面編程舷手,但在實際使用過程中,切面可能退化成了一個劲绪,比如我們想統(tǒng)計app的冷啟動時間男窟,這就非常具體了。如果我們用AOP的技術(shù)實現(xiàn)統(tǒng)計所有函數(shù)的耗時時間贾富,自然能統(tǒng)計到類似啟動這個階段的時間歉眷。

從狹義來看實現(xiàn)AOP技術(shù)的框架必須是能將切面編程抽象成上層可以直接使用的工具或API,但當(dāng)我們將切面降維后颤枪,最終面向的就是切點而已汗捡。換句話說,只要能將代碼織入到某個點那這種技術(shù)就一定可以實現(xiàn)AOP畏纲,這樣AOP技術(shù)所涵蓋的領(lǐng)域就得以拓展扇住,因為從狹義的角度看目前只有AspectJ符合這個標(biāo)準(zhǔn)。

從廣義上來講盗胀,AOP技術(shù)可以是任何能實現(xiàn)代碼織入的技術(shù)或框架艘蹋,對代碼的改動最終都會體現(xiàn)在字節(jié)碼上,而這類技術(shù)也可以叫做字節(jié)碼增強票灰,通用名詞理解即可女阀。

下面我們將介紹一些常用的AOP技術(shù)。

首先屑迂,從織入的時機的角度看浸策,可以分為源碼階段、class階段屈糊、dex階段的榛、運行時織入。

對于前三項源碼階段逻锐、class階段夫晌、dex織入雕薪,由于他們都發(fā)生在class加載到虛擬機前,我們統(tǒng)稱為靜態(tài)織入晓淀,
而在運行階段發(fā)生的改動所袁,我們統(tǒng)稱為動態(tài)織入。

常見的技術(shù)框架如下表:

織入時機 技術(shù)框架
靜態(tài)織入 APT凶掰,AspectJ燥爷、ASM、Javassit
動態(tài)織入 java動態(tài)代理懦窘,cglib前翎、Javassit

靜態(tài)織入發(fā)生在編譯器,因此幾乎不會對運行時的效率產(chǎn)生影響畅涂;動態(tài)織入發(fā)生在運行期港华,可直接將字節(jié)碼寫入內(nèi)存,并通過反射完成類的加載午衰,所以效率相對較低立宜,但更靈活。

動態(tài)織入的前提是類還未被加載臊岸,你不能將一個已經(jīng)加載的類經(jīng)過修改再次加載橙数,這是ClassLoader的限制。但是可以通過另一個ClassLoader進行加載帅戒,虛擬機允許兩個相同類名的class被不同的ClassLoader加載灯帮,在運行時也會被認為是兩個不同的類,因此需要注意不能相互賦值蜘澜, 不然會拋出ClassCastException施流。

java動態(tài)代理响疚、cglib只會創(chuàng)建新的代理類而不是對原有類的字節(jié)碼直接修改鄙信,Javassit可修改原有字節(jié)碼。

其實利用反射或者hook技術(shù)同樣可以實現(xiàn)代碼行為的改變忿晕,但由于這類技術(shù)并沒有真正的改變原有的字節(jié)碼装诡,所以暫不在談?wù)摲秶鷥?nèi),比如xposed践盼,dexposed鸦采。


其次,我們需要關(guān)注這些框架具備哪切面編程的能力咕幻,這有助于幫助我做技術(shù)選型渔伯,由于AspectJ、ASM 肄程、Javassit是相對比較完善的AOP框架锣吼,因此只對三者進行比較选浑。

能力 AspectJ ASM Javassit
切面抽象 ?
切點抽象 ?
通知類型抽象 ? ? ?

其中:

  • 切面抽象:具備篩選過濾class的能力,比如我們想為Activity的所有生命周期織入代碼玄叠,那你是不是首先需要具備過濾Activity及其子類的能力古徒。

  • 切點抽象:具體到某個class,是否具備方法读恃、字段隧膘、注解訪問的能力。

  • 通知類型抽象:是否直接支持在方法前寺惫、后疹吃、中直接織入代碼。

當(dāng)然不具備能力不代表不能做AOP編程西雀,可以通過其他方法解決互墓,只是易用性的問題。

下面我們將開始對上述框架逐一介紹蒋搜,Let' go~~~

APT

APT(Annotation Processing Tool)即注解處理器篡撵,在Gradle 版本>=2.2后被annotationProcessor取代。

它用來在編譯時掃描和處理注解豆挽,掃描過程可使用 auto-service 來簡化尋找注解的配置育谬,在處理過程中可生成java文件(創(chuàng)建java文件通常依賴 javapoet 這個庫)。常用于生成一些模板代碼或運行時依賴的類文件帮哈,比如常見的ButterKnife膛檀、Dagger、ARouter娘侍,它的優(yōu)點是簡單方便咖刃。

以ButterKnife為例:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.toolbar)
    Toolbar toolbar;

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

}

一句簡單的ButterKnife.bind(this)是如何實現(xiàn)控件的賦值的?

事實上@Bind注解在編譯期會生成一個MainActivity_ViewBinding類憾筏,而ButterKnife.bind(this)這次調(diào)用最終會通過反射創(chuàng)建出MainActivity_ViewBinding對象嚎杨,并把activity的引用傳遞給它。

# ButterKnife
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
    ...
    //創(chuàng)建xxx_binding對象并把activity傳入
    return constructor.newInstance(target, source);
}

private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    ...
    try {
      //運行時通過反射加載在編譯階段生成的類
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
    } 
    ...
    return bindingCtor;
}

這樣最終在MainActivity_ViewBinding的構(gòu)造函數(shù)中完成控件的賦值氧腰。

public class MainActivity_ViewBinding<T extends MainActivity> implements Unbinder {
  protected T target;
  public MainActivity_ViewBinding(final T target, Finder finder, Object source) {
    ...
    //為控件賦值 其中優(yōu)化了控件的查找
    target.toolbar = finder.findRequiredViewAsType(source, R.id.toolbar, "field 'toolbar'", Toolbar.class);
    ...
  }
}

為了在此類中能訪問到MainActivity中聲明的屬性枫浙,為此ButterKnife框架要求,使用@Bind注解聲明的屬性不能是private的古拴。

可以看到ButterKnife中仍然用到了反射箩帚,這是為了統(tǒng)一API使用ButterKnife.bind(...)作出的犧牲,而Dagger則會通過Component黄痪,Module的名字通過動態(tài)生成不同的方法名紧帕,因此使用之前需要對工程進行build。

之所以會這樣桅打,是因為APT技術(shù)的不足是嗜,通常只是用來創(chuàng)建新的類轻纪,而不能對原有類進行改動,在不能改動的情況下叠纷,只能通過反射實現(xiàn)動態(tài)化刻帚。

AspectJ

AspectJ是一種嚴格意義上的AOP技術(shù),因為它提供了完整的面向切面編程的注解涩嚣,這樣讓使用者可以在不關(guān)心字節(jié)碼原理的情況下完成代碼的織入崇众,因為編寫的切面代碼就是要織入的實際代碼。

AspectJ實現(xiàn)代碼織入有兩種方式航厚,一是自行編寫.ajc文件顷歌,二是使用AspectJ提供的@Aspect、@Pointcut等注解幔睬,二者最終都是通過ajc編譯器完成代碼的織入眯漩。

舉個簡單的例子,假設(shè)我們想統(tǒng)計所有view的點擊事件麻顶,使用AspectJ只需要寫一個類即可赦抖。

@Aspect
public class MethodAspect {
    private static final String TAG = "MethodAspect5";

    //切面表達式,聲明需要過濾的類和方法 
    @Pointcut("execution(* android.view.View.OnClickListener+.onClick(..))")
    public void callMethod() {
    }

    //before表示在方法調(diào)用前織入
    @before("callMethod()")
    public void beforeMethodCall(ProceedingJoinPoint joinPoint) {
        //編寫業(yè)務(wù)代碼
    }
}

注解簡明直觀辅肾,上手難度近乎為0队萤。

常用的函數(shù)耗時統(tǒng)計工具Hugo,就是AspectJ的一個實際應(yīng)用矫钓,Android平臺Hujiang開源的AspectJX插件靈感也來自于Hugo要尔,詳情見舊文Android 函數(shù)耗時統(tǒng)計工具之Hugo

AspectJ雖然好用新娜,但也存在一些嚴重的問題赵辕。

  • 重復(fù)織入、不織入
  • 不支持Java8

AspectJ切面表達式支持繼承語法概龄,雖然方便了開發(fā)还惠,但存在致命的問題,就是在繼承樹上的類可能都會織入代碼旁钧,這在多數(shù)業(yè)務(wù)場景下是不適用的吸重,比如無埋點。

另外使用java8語法編寫的代碼歪今,不會被進入切面范圍,也就無法織入代碼颜矿。

更多詳情參見舊文 Android AspectJ詳解 寄猩。

ASM

ASM是非常底層的面向字節(jié)碼編程的AOP框架,理論上可以實現(xiàn)任何關(guān)于字節(jié)碼的修改骑疆,非常硬核田篇。許多字節(jié)碼生成API底層都是用ASM實現(xiàn)替废,常見比如Groovy、cglib泊柬,因此在Android平臺下使用ASM無需添加額外的依賴椎镣。完整的學(xué)習(xí)ASM必須了解字節(jié)碼和JVM相關(guān)知識。

比如要織入一句簡單的日志輸出

Log.d("tag", " onCreate");

使用ASM編寫是下面這個樣子兽赁,沒錯因為JVM是基于棧的状答,函數(shù)的調(diào)用需要參數(shù)先入棧,然后執(zhí)行函數(shù)入棧刀崖,最后出棧惊科,總共四條JVM指令。

mv.visitLdcInsn("tag");
mv.visitLdcInsn("onCreate");
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);

可以看出ASM與AspectJ有很大的不同亮钦,AspectJ織入的代碼就是實際編寫的代碼馆截,但ASM必須使用其提供的API編寫指令。一行java代碼可能對應(yīng)多行ASM API代碼蜂莉,因為一行java代碼背后可能隱藏這多個JVM指令蜡娶。

你不必擔(dān)心不會編寫ASM代碼,官方提供了ASM Bytecode Outline插件可以直接將java代碼生成ASM代碼映穗。

ASM的實際使用場景非常廣泛翎蹈,我們以Matrix為例。

Matrix是微信開源的一個APM框架男公,其中TraceCanary子模塊用于監(jiān)測幀率低荤堪、卡頓、ANR等場景枢赔,具備函數(shù)耗時統(tǒng)計的功能澄阳。

為了實現(xiàn)函數(shù)的耗時統(tǒng)計,通常的做法都是在函數(shù)執(zhí)行開始和結(jié)束為止進行插樁踏拜,最后以兩個插樁點的時間差為函數(shù)的執(zhí)行時間碎赢。

# -> MethodTracer.TraceMethodAdapter
@Override
protected void onMethodEnter() {
    TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
    if (traceMethod != null) {
        traceMethodCount.incrementAndGet();
        mv.visitLdcInsn(traceMethod.id);
        //入口插樁
        mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
    }
}

@Override
protected void onMethodExit(int opcode) {
    TraceMethod traceMethod = mCollectedMethodMap.get(methodName);
    ...
    traceMethodCount.incrementAndGet();
    mv.visitLdcInsn(traceMethod.id);
    //出口插樁
    mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
}

總體上就是每個方法的開頭和結(jié)尾處各添加一行代碼,然后交由TraceMethod進行統(tǒng)計和計算速梗。

詳情見舊文Matrix系列文章(一) 卡頓分析工具之Trace Canary肮塞。

接下來,我們分析一下ASM的不足姻锁。

  • 切面代碼需要硬編碼枕赵,通常是手動寫過濾條件,不夠靈活位隶,試想一下如何用ASM實現(xiàn)統(tǒng)計所有Activity的生命周期方法拷窜。
  • 很難實現(xiàn)在方法調(diào)用前后織入新的代碼,而在AspectJ中一個call關(guān)鍵字就解決了。

更多詳情參見舊文 Android ASM框架詳解 篮昧。

javassit

javassit是一個開源的字節(jié)碼創(chuàng)建赋荆、編輯類庫,現(xiàn)屬于Jboss web容器的一個子模塊懊昨,特點是簡單窄潭、快速,與AspectJ一樣酵颁,使用它不需要了解字節(jié)碼和虛擬機指令嫉你,這里是官方文檔

javassit核心的類庫包含ClassPool材义,CtClass 均抽,CtMethod和CtField。

  • ClassPool:一個基于HashMap實現(xiàn)的CtClass對象容器其掂。
  • CtClass:表示一個類油挥,可從ClassPool中通過完整類名獲取。
  • CtMethods:表示類中的方法款熬。
  • CtFields :表示類中的字段深寥。

javassit API簡潔直觀,比如我們想動態(tài)創(chuàng)建一個類贤牛,并添加一個helloWorld方法惋鹅。

ClassPool pool = ClassPool.getDefault();
//通過makeClass創(chuàng)建類
CtClass ct = pool.makeClass("test.helloworld.Test");//創(chuàng)建類
//為ct添加一個方法
CtMethod helloMethod = CtNewMethod.make("public void helloWorld(String des){ System.out.println(des);}",ct);
ct.addMethod(helloMethod);
//寫入文件
ct.writeFile();
//加載進內(nèi)存
// ct.toClass();

然后,我們想在helloWorld方法前后織入代碼殉簸。

ClassPool pool = ClassPool.getDefault();
//獲取class
CtClass ct = pool.getCtClass("test.helloworld.Test");
//獲取helloWorld方法
CtMethod m = ct.getDeclaredMethod("helloWorld");
//在方法開頭織入
m.insertBefore("{ System.out.print(\"before insert\");");
//在方法末尾織入 可使用this關(guān)鍵字
m.insertAfter("{System.out.println(this.x); }");
//寫入文件
ct.writeFile();

javassit的語法直觀簡潔的特點闰集,使得在很多開源項目中都有它的身影。

比如QQ zone的熱修復(fù)方案般卑,當(dāng)時遇到的問題是補丁包加載做odex優(yōu)化時武鲁,由于差分的patch包并不依賴其他dex,導(dǎo)致補丁包中的類被打上is_preverfied標(biāo)簽(這有助于運行時提升性能)蝠检,但在補丁運行時實際會去引用其他dex中的類沐鼠,就會拋出錯誤java.lang.IllegalAccessError:Class ref pre-verified class resovled to unexpected implement

當(dāng)時qq空間團隊的解決方案是在編譯階段為對所有類的構(gòu)造方法進行插樁叹谁,引用一個事先定義好的AnalyseLoad類饲梭,然后干預(yù)分包過程,讓這個類處于一個獨立的dex中焰檩,這樣就避免了上述問題憔涉。

這里用的AOP方案就是javassit,詳情見 QQ空間補丁方案解析

還有最近開源的插件化框架shadow,shadow框架中的一個需求是,插件包具備獨立運行的能力,當(dāng)運行插件工程時浪腐,插件中Activity的父類ShadowActivity繼承Activity,當(dāng)插件作為子模塊加載到插件中時ShadowActivity不必繼承系統(tǒng)Activity顿乒,只是作為一個代理類就夠了议街。此時shadow團隊封裝了JavassistTransform,在編譯期動態(tài)修改Activity的父類璧榄。

詳見 調(diào)試研究Shadow對字節(jié)碼編輯的正確姿勢 特漩。

動態(tài)代理

動態(tài)代理是代理模式的一種實現(xiàn),用于在運行時動態(tài)增強原始類的行為骨杂,實現(xiàn)方式是運行時直接生成class字節(jié)碼并將其加載進虛擬機涂身。

JDK本身就提供一個Proxy類用于實現(xiàn)動態(tài)代理。
我們通常使用下面的API創(chuàng)建代理類搓蚪。

# java.lang.reflect.Proxy
public static Object newProxyInstance(ClassLoader loader,
    Class<?>[] interfaces, 
    InvocationHandler h)

其中在InvocationHandler實現(xiàn)類中定義核心切點代碼蛤售。

public class InvocationHandlerImpl implements InvocationHandler {

    /** 被代理的實例 */
    private Object mObj = null;

    public InvocationHandlerImpl(Object obj){
        this.mObj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //前切入點
        Object result = method.invoke(this.mObj, args);
        //后切入點
        return result;
    }
}

這樣在前后切入點的位置可以編寫要織入的代碼。

在我們常用的Retrofit框架中就用到了動態(tài)代理妒潭。Retrofit提供了一套易于開發(fā)網(wǎng)絡(luò)請求的注解悴能,而在注解中聲明的參數(shù)正是通過代理包裝之后發(fā)出的網(wǎng)絡(luò)請求。

# Retrofit.create
public <T> T create(final Class<T> service) {
    ...
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
       new InvocationHandler() {
         private final Platform platform = Platform.get();
         private final Object[] emptyArgs = new Object[0];

         @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
             throws Throwable {
           // If the method is a method from Object then defer to normal invocation.
           if (method.getDeclaringClass() == Object.class) {
             return method.invoke(this, args);
           }
           if (platform.isDefaultMethod(method)) {
             return platform.invokeDefaultMethod(method, service, proxy, args);
           }
           //代理
           return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
         }
    });
}

java動態(tài)代理最大的問題是只能代理接口雳灾,而不能代理普通類或者抽象類漠酿,這是因為默認創(chuàng)建的代理類繼承Porxy,而java又不支持多繼承谎亩,這一點極大的限制了動態(tài)代理的使用場景炒嘲,cglib可代理普通類。

更多詳情參見 設(shè)計模式之代理模式 匈庭。

總結(jié)

最后我們總結(jié)一下 上述AOP框架的特點及優(yōu)劣勢夫凸,你可以根據(jù)自身需求進行技術(shù)選型。

技術(shù)框架 特點 開發(fā)難度 優(yōu)勢 不足
APT 常用于通過注解減少模板代碼嚎花,對類的創(chuàng)建于增強需要依賴其他框架寸痢。 ★★ 開發(fā)注解簡化上層編碼。 使用注解對原工程具有侵入性紊选。
AspectJ 提供完整的面向切面編程的注解啼止。 ★★ 真正意義的AOP,支持通配兵罢、繼承結(jié)構(gòu)的AOP献烦,無需硬編碼切面。 重復(fù)織入卖词、不織入問題巩那,不支持java8
ASM 面向字節(jié)碼指令編程吏夯,功能強大。 ★★★ 高效即横,ASM5開始支持java8噪生。 切面能力不足,部分場景需硬編碼东囚。
Javassit API簡潔易懂跺嗽,快速開發(fā)。 上手快页藻,新人友好桨嫁,具備運行時加載class能力。 切點代碼編寫需注意class path加載問題份帐。
java動態(tài)代理 運行時擴展代理接口功能璃吧。 運行時動態(tài)增強。 僅支持代理接口废境,擴展性差畜挨,使用反射性能差。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末彬坏,一起剝皮案震驚了整個濱河市朦促,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌栓始,老刑警劉巖务冕,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異幻赚,居然都是意外死亡禀忆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門落恼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來箩退,“玉大人,你說我怎么就攤上這事佳谦〈骼裕” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵钻蔑,是天一觀的道長啥刻。 經(jīng)常有香客問我,道長咪笑,這世上最難降的妖魔是什么可帽? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮窗怒,結(jié)果婚禮上映跟,老公的妹妹穿的比我還像新娘蓄拣。我一直安慰自己,他們只是感情好努隙,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布球恤。 她就那樣靜靜地躺著,像睡著了一般剃法。 火紅的嫁衣襯著肌膚如雪碎捺。 梳的紋絲不亂的頭發(fā)上路鹰,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天贷洲,我揣著相機與錄音,去河邊找鬼晋柱。 笑死优构,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的雁竞。 我是一名探鬼主播钦椭,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼碑诉!你這毒婦竟也來了彪腔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤进栽,失蹤者是張志新(化名)和其女友劉穎德挣,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體快毛,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡格嗅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了唠帝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屯掖。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖襟衰,靈堂內(nèi)的尸體忽然破棺而出贴铜,到底是詐尸還是另有隱情,我是刑警寧澤瀑晒,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布绍坝,位于F島的核電站,受9級特大地震影響瑰妄,放射性物質(zhì)發(fā)生泄漏陷嘴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一间坐、第九天 我趴在偏房一處隱蔽的房頂上張望灾挨。 院中可真熱鬧邑退,春花似錦、人聲如沸劳澄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽秒拔。三九已至莫矗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間砂缩,已是汗流浹背作谚。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留庵芭,地道東北人妹懒。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像双吆,于是被迫代替她去往敵國和親眨唬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

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