InstantRun原理(2)——更新邏輯

上一篇博客我們介紹了InstantRun的初始化邏輯著蛙,接下來我們來看下在運(yùn)行時(shí)階段,InstantRun是如何加載修改的代碼的耳贬。

上一篇博客的末尾我們介紹了InstantRun在初始化完成后踏堡,會(huì)啟動(dòng)一個(gè)server。不難猜測(cè)咒劲,這個(gè)server就是在監(jiān)聽是否有代碼更新顷蟆。當(dāng)用戶更改代碼后,AndroidStudio會(huì)將相關(guān)更新發(fā)送給server缎患,server獲取到更新后執(zhí)行修復(fù)邏輯慕的。

1 SocketServerReplyThread

server的主要實(shí)現(xiàn)由其內(nèi)部類SocketServerReplyThread,首先來看下其實(shí)現(xiàn):

private class SocketServerReplyThread extends Thread { 
    private final LocalSocket mSocket; 
 
    SocketServerReplyThread(LocalSocket socket) { 
        this.mSocket = socket; 
    } 
 
    public void run() { 
        try { 
            DataInputStream input = new DataInputStream(this.mSocket.getInputStream()); 
            DataOutputStream output = new DataOutputStream(this.mSocket.getOutputStream()); 
            try { 
                handle(input, output); 
            } finally { 
                try { 
                    input.close(); 
                } catch (IOException ignore) { 
                } 
                try { 
                    output.close(); 
                } catch (IOException ignore) { 
                } 
            } 
            return; 
        } catch (IOException e) { 
            if (Log.isLoggable("InstantRun", 2)) { 
                Log.v("InstantRun", "Fatal error receiving messages", e); 
            } 
        } 
    } 
 
    private void handle(DataInputStream input, DataOutputStream output) throws IOException { 
        long magic = input.readLong(); 
        if (magic != 890269988L) { 
            Log.w("InstantRun", "Unrecognized header format " + Long.toHexString(magic)); 
            return; 
        } 
        int version = input.readInt(); 
        output.writeInt(4); 
        if (version != 4) { 
            Log.w("InstantRun", "Mismatched protocol versions; app is using version 4 and tool is using version " + version); 
        } else { 
            int message; 
            for (; ; ) { 
                message = input.readInt(); 
                switch (message) { 
                    case 7: 
                        if (Log.isLoggable("InstantRun", 2)) { 
                            Log.v("InstantRun", "Received EOF from the IDE"); 
                        } 
                        return; 
                    case 2: 
                        boolean active = Restarter.getForegroundActivity(Server.this.mApplication) != null; 
                        output.writeBoolean(active); 
                        if (Log.isLoggable("InstantRun", 2)) { 
                            Log.v("InstantRun", "Received Ping message from the IDE; returned active = " + active); 
                        } 
                        break; 
                    case 3: 
                        String path = input.readUTF(); 
                        long size = FileManager.getFileSize(path); 
                        output.writeLong(size); 
                        if (Log.isLoggable("InstantRun", 2)) { 
                            Log.v("InstantRun", "Received path-exists(" + path + ") from the " + "IDE; returned size=" + size); 
                        } 
                        break; 
                    case 4: 
                        long begin = System.currentTimeMillis(); 
                        path = input.readUTF(); 
                        byte[] checksum = FileManager.getCheckSum(path); 
                        if (checksum != null) { 
                            output.writeInt(checksum.length); 
                            output.write(checksum); 
                            if (Log.isLoggable("InstantRun", 2)) { 
                                long end = System.currentTimeMillis(); 
                                String hash = new BigInteger(1, checksum) 
                                        .toString(16); 
                                Log.v("InstantRun", "Received checksum(" + path 
                                        + ") from the " + "IDE: took " 
                                        + (end - begin) + "ms to compute " 
                                        + hash); 
                            } 
                        } else { 
                            output.writeInt(0); 
                            if (Log.isLoggable("InstantRun", 2)) { 
                                Log.v("InstantRun", "Received checksum(" + path 
                                        + ") from the " 
                                        + "IDE: returning "); 
                            } 
                        } 
                        break; 
                    case 5: 
                        if (!authenticate(input)) { 
                            return; 
                        } 
                        Activity activity = Restarter 
                                .getForegroundActivity(Server.this.mApplication); 
                        if (activity != null) { 
                            if (Log.isLoggable("InstantRun", 2)) { 
                                Log.v("InstantRun", 
                                        "Restarting activity per user request"); 
                            } 
                            Restarter.restartActivityOnUiThread(activity); 
                        } 
                        break; 
                    case 1: 
                        if (!authenticate(input)) { 
                            return; 
                        } 
                        List changes = ApplicationPatch 
                                .read(input); 
                        if (changes != null) { 
                            boolean hasResources = Server.hasResources(changes); 
                            int updateMode = input.readInt(); 
                            updateMode = Server.this.handlePatches(changes, 
                                    hasResources, updateMode); 
                            boolean showToast = input.readBoolean(); 
                            output.writeBoolean(true); 
                            Server.this.restart(updateMode, hasResources, 
                                    showToast); 
                        } 
                        break; 
                    case 6: 
                        String text = input.readUTF(); 
                        Activity foreground = Restarter 
                                .getForegroundActivity(Server.this.mApplication); 
                        if (foreground != null) { 
                            Restarter.showToast(foreground, text); 
                        } else if (Log.isLoggable("InstantRun", 2)) { 
                            Log.v("InstantRun", 
                                    "Couldn't show toast (no activity) : " 
                                            + text); 
                        } 
                        break; 
                } 
            } 
        } 
    } 
} 

socket開啟后,開始讀取數(shù)據(jù)挤渔,先進(jìn)行一些簡(jiǎn)單的校驗(yàn)肮街,判斷讀取的數(shù)據(jù)是否正確。然后依次讀取文件數(shù)據(jù)判导。

  • 如果讀到7嫉父,則表示已經(jīng)讀到文件的末尾,退出讀取操作
  • 如果讀到2眼刃,則表示獲取當(dāng)前Activity活躍狀態(tài)绕辖,并且進(jìn)行記錄
  • 如果讀到3,讀取UTF-8字符串路徑擂红,讀取該路徑下文件長(zhǎng)度仪际,并且進(jìn)行記錄
  • 如果讀到4,讀取UTF-8字符串路徑昵骤,獲取該路徑下文件MD5值树碱,如果沒有,則記錄0变秦,否則記錄MD5值和長(zhǎng)度成榜。
  • 如果讀到5,先校驗(yàn)輸入的值是否正確(根據(jù)token來判斷)蹦玫,如果正確赎婚,則在UI線程重啟Activity
  • 如果讀到1刘绣,先校驗(yàn)輸入的值是否正確(根據(jù)token來判斷),如果正確挣输,獲取代碼變化的List纬凤,處理代碼的改變(handlePatches,這個(gè)之后具體分析)歧焦,然后重啟
  • 如果讀到6移斩,讀取UTF-8字符串,showToast

當(dāng)讀到1時(shí)绢馍,獲取代碼變化的ApplicationPatch列表向瓷,然后調(diào)用handlePatches來處理代碼的變化。

handlePatches:

private int handlePatches(List changes, boolean hasResources, int updateMode) { 
    if (hasResources) { 
        FileManager.startUpdate(); 
    } 
    for (ApplicationPatch change : changes) { 
        String path = change.getPath(); 
        if (path.endsWith(".dex")) { 
            handleColdSwapPatch(change); 
            boolean canHotSwap = false; 
            for (ApplicationPatch c : changes) { 
                if (c.getPath().equals("classes.dex.3")) { 
                    canHotSwap = true; 
                    break; 
                } 
            } 
            if (!canHotSwap) { 
                updateMode = 3; 
            } 
        } else if (path.equals("classes.dex.3")) { 
            updateMode = handleHotSwapPatch(updateMode, change); 
        } else if (isResourcePath(path)) { 
            updateMode = handleResourcePatch(updateMode, change, path); 
        } 
    } 
    if (hasResources) { 
        FileManager.finishUpdate(true); 
    } 
    return updateMode; 
}  

本方法主要通過判斷Change的內(nèi)容舰涌,來判斷采用什么模式(熱部署猖任、溫部署或冷部署)

  • 如果后綴為“.dex”,冷部署處理handleColdSwapPatch
  • 如果后綴為“classes.dex.3”,熱部署處理handleHotSwapPatch
  • 其他情況,溫部署,處理資源handleResourcePatch

2 熱部署

我們知道如果僅僅修改某個(gè)方法的內(nèi)部實(shí)現(xiàn)瓷耙,InstantRun可以通過熱部署的方式更新朱躺。還是以上一篇博客的例子,我們對(duì)代碼進(jìn)行一點(diǎn)修改,將Toast彈出的文字從'click'變?yōu)?click!!!':

    @Override
    public void onClick(View view) {
        Toast.makeText(this, "click!!!", Toast.LENGTH_SHORT).show();
    }

此時(shí)如果點(diǎn)擊運(yùn)行搁痛,可以看到應(yīng)用在沒有重啟的情況更新了邏輯长搀。當(dāng)點(diǎn)擊run按鈕后,在build/intermediates/transforms/instantRun/debug/folders/4000/5目錄下會(huì)出現(xiàn)我們輸出即將發(fā)送給終端的patch:

這里寫圖片描述

可以看到patch總共分為兩部分:

  • 修改后的代碼,對(duì)應(yīng)圖中的com.alibaba.sdk.instantdemo.MainActivity$override

  • com.android.tools.fd.runtime.AppPatchesLoaderImpl.class用于記錄哪些類被修改了,如本例中的MainActivity

    public class AppPatchesLoaderImpl extends AbstractPatchesLoaderImpl {
        public static final long BUILD_ID = 76160209775610L;
    
        public AppPatchesLoaderImpl() {
        }
    
        public String[] getPatchedClasses() {
            return new String[]{"com.alibaba.sdk.instandemo.MainActivity"};
        }
    }
    

    ?

2.1 修改后的代碼

修改后的代碼會(huì)重新生成一個(gè)新的類名:舊類名+$override鸡典。如本例中的MainActivity$override源请,接下來看下MainActivity$override的源碼:

public class MainActivity$override implements IncrementalChange {
    public MainActivity$override() {
    }

    public static Object init$args(MainActivity[] var0, Object[] var1) {
        Object[] var2 = new Object[]{new Object[]{var0, new Object[0]}, "android/support/v7/app/AppCompatActivity.()V"};
        return var2;
    }

    public static void init$body(MainActivity $this, Object[] var1) {
    }

    public static void onCreate(MainActivity $this, Bundle savedInstanceState) {
        Object[] var2 = new Object[]{savedInstanceState};
        MainActivity.access$super($this, "onCreate.(Landroid/os/Bundle;)V", var2);
        $this.setContentView(2130968603);
        AndroidInstantRuntime.setPrivateField($this, (Button)$this.findViewById(2131427416), MainActivity.class, "btn");
        ((Button)AndroidInstantRuntime.getPrivateField($this, MainActivity.class, "btn")).setOnClickListener($this);
    }

    public static void onClick(MainActivity $this, View view) {
        Toast.makeText($this, "click!!!", 0).show();
    }

    public Object access$dispatch(String var1, Object... var2) {
        switch(var1.hashCode()) {
        case -1912803358:
            onClick((MainActivity)var2[0], (View)var2[1]);
            return null;
        case -641568046:
            onCreate((MainActivity)var2[0], (Bundle)var2[1]);
            return null;
        case 1345615064:
            init$body((MainActivity)var2[0], (Object[])var2[1]);
            return null;
        case 1495908858:
            return init$args((MainActivity[])var2[0], (Object[])var2[1]);
        default:
            throw new InstantReloadException(String.format("String switch could not find \'%s\' with hashcode %s in %s", new Object[]{var1, Integer.valueOf(var1.hashCode()), "com/alibaba/sdk/instandemo/MainActivity"}));
        }
    }
}

我們看到,MainActivity$override實(shí)現(xiàn)了IncrementalChange并覆寫了access$dispatch方法彻况。

該patch會(huì)通過server被寫到應(yīng)用的私有目錄下谁尸,然后通過handleHotSwapPatch進(jìn)行加載。

2.2 hot swap:handleHotSwapPatch

private int handleHotSwapPatch(int updateMode, ApplicationPatch patch) { 
    if (Log.isLoggable("InstantRun", 2)) { 
        Log.v("InstantRun", "Received incremental code patch"); 
    } 
    try { 
        String dexFile = FileManager.writeTempDexFile(patch.getBytes()); 
        if (dexFile == null) { 
            Log.e("InstantRun", "No file to write the code to"); 
            return updateMode; 
        } 
        if (Log.isLoggable("InstantRun", 2)) { 
            Log.v("InstantRun", "Reading live code from " + dexFile); 
        } 
        String nativeLibraryPath = FileManager.getNativeLibraryFolder() 
                .getPath(); 
        DexClassLoader dexClassLoader = new DexClassLoader(dexFile, 
                this.mApplication.getCacheDir().getPath(), 
                nativeLibraryPath, getClass().getClassLoader()); 
        Class aClass = Class.forName( 
                "com.android.tools.fd.runtime.AppPatchesLoaderImpl", true, 
                dexClassLoader); 
        try { 
            if (Log.isLoggable("InstantRun", 2)) { 
                Log.v("InstantRun", "Got the patcher class " + aClass); 
            } 
            PatchesLoader loader = (PatchesLoader) aClass.newInstance(); 
            if (Log.isLoggable("InstantRun", 2)) { 
                Log.v("InstantRun", "Got the patcher instance " + loader); 
            } 
            String[] getPatchedClasses = (String[]) aClass 
                    .getDeclaredMethod("getPatchedClasses", new Class[0]) 
                    .invoke(loader, new Object[0]); 
            if (Log.isLoggable("InstantRun", 2)) { 
                Log.v("InstantRun", "Got the list of classes "); 
                for (String getPatchedClass : getPatchedClasses) { 
                    Log.v("InstantRun", "class " + getPatchedClass); 
                } 
            } 
            if (!loader.load()) { 
                updateMode = 3; 
            } 
        } catch (Exception e) { 
            Log.e("InstantRun", "Couldn't apply code changes", e); 
            e.printStackTrace(); 
            updateMode = 3; 
        } 
    } catch (Throwable e) { 
        Log.e("InstantRun", "Couldn't apply code changes", e); 
        updateMode = 3; 
    } 
    return updateMode; 
}

該方法將patch的dex文件寫入到臨時(shí)目錄纽甘,然后使用DexClassLoader去加載dex良蛮。然后反射調(diào)用AppPatchesLoaderImpl類的load方法。

AppPatchesLoaderImpl繼承自抽象類AbstractPatchesLoaderImpl悍赢,并實(shí)現(xiàn)了抽象方法:getPatchedClasses决瞳。而AbstractPatchesLoaderImpl抽象類代碼如下:

public abstract class AbstractPatchesLoaderImpl implements PatchesLoader { 
      public abstract String[] getPatchedClasses(); 
      public boolean load() { 
           try { 
                 for (String className : getPatchedClasses()) { 
                       ClassLoader cl = getClass().getClassLoader(); 
                       Class aClass = cl.loadClass(className + "$override"); 
                       Object o = aClass.newInstance(); 
                       Class originalClass = cl.loadClass(className); 
                       Field changeField = originalClass.getDeclaredField("$change"); 
                       changeField.setAccessible(true); 
                       Object previous = changeField.get(null); 
                       if (previous != null) { 
                            Field isObsolete = previous.getClass().getDeclaredField("$obsolete"); 
                            if (isObsolete != null) { 
                                 isObsolete.set(null, Boolean.valueOf(true)); 
                            } 
                       } 
                       changeField.set(null, o); 
                       if ((Log.logging != null) && (Log.logging.isLoggable(Level.FINE))) { 
                            Log.logging.log(Level.FINE, String.format("patched %s", new Object[] { className })); 
                       } 
                  } 
            } catch (Exception e) { 
                  if (Log.logging != null) { 
                         Log.logging.log(Level.SEVERE, String.format("Exception while patching %s", new Object[] { "foo.bar" }), e); 
} 
                  return false; 
            } 
            return true; 
      } 
}  

現(xiàn)在我們?cè)倩剡^頭去看下MainActivity的代碼:

package com.alibaba.sdk.instandemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;
import com.android.tools.fd.runtime.IncrementalChange;
import com.android.tools.fd.runtime.InstantReloadException;

public class MainActivity extends AppCompatActivity
  implements View.OnClickListener
{
  public static final long serialVersionUID = 0L;
  private Button btn;

  public MainActivity()
  {
  }

  MainActivity(Object[] paramArrayOfObject, InstantReloadException paramInstantReloadException)
  {
    this();
  }

  public void onClick(View paramView)
  {
    IncrementalChange localIncrementalChange = $change;
    if (localIncrementalChange != null)
    {
      localIncrementalChange.access$dispatch("onClick.(Landroid/view/View;)V", new Object[] { this, paramView });
      return;
    }
    Toast.makeText(this, "click", 0).show();
  }

  public void onCreate(Bundle paramBundle)
  {
    IncrementalChange localIncrementalChange = $change;
    if (localIncrementalChange != null)
    {
      localIncrementalChange.access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[] { this, paramBundle });
      return;
    }
    super.onCreate(paramBundle);
    setContentView(2130968603);
    this.btn = ((Button)findViewById(2131427416));
    this.btn.setOnClickListener(this);
  }
}

結(jié)合兩段代碼,不難看出左权,loadClass方法的原理其實(shí)就是通過反射的方法將原有class中的$change設(shè)置為修復(fù)類皮胡,然后通過access$dispatch執(zhí)行更新后的邏輯。

這里有一個(gè)問題涮总。如果我多次修改MainActivityhandleHotSwapPatch就會(huì)加載多次MainActivity$override祷舀,難道不會(huì)沖突嗎瀑梗?一個(gè)類不是只能加載一次嗎烹笔?其實(shí)這個(gè)不用擔(dān)心,因?yàn)?code>handleHotSwapPatch每次都重新創(chuàng)建了一個(gè)DexClassLoader,不同的ClassLoader即使加載同一個(gè)class也會(huì)被認(rèn)為是不同class抛丽,所以不用擔(dān)心谤职。

2.3 warm swap:handleResourcePatch

private static int handleResourcePatch(int updateMode, ApplicationPatch patch, String path){
    if (Log.isLoggable("InstantRun", 2)) {
        Log.v("InstantRun", "Received resource changes (" + path + ")");
    }
    FileManager.writeAaptResources(path, patch.getBytes());
    updateMode = Math.max(updateMode, 2);
    return updateMode;
}

調(diào)用了FileManager.writeAaptResources方法寫入Aapt resource。

public static void writeAaptResources(String relativePath, byte[] bytes){
    File resourceFile = getResourceFile(getWriteFolder(false));
    File file = resourceFile;
    File folder = file.getParentFile();
    if (!folder.isDirectory()) {
        boolean created = folder.mkdirs();
        if (!created) {
            if (Log.isLoggable("InstantRun", 2)) {
                Log.v("InstantRun", "Cannot create local resource file directory " + folder);
            }
            return;
        }
    }
    if (relativePath.equals("resources.ap_"))
    {
        writeRawBytes(file, bytes);
    }
    else
        writeRawBytes(file, bytes);
}

可以看到它去獲取了對(duì)應(yīng)的資源文件亿鲜,就是我們?cè)谏厦嫣岬降?data/data/[applicationId]/files/instant-run/resources.ap_允蜈,InstantRun直接對(duì)它進(jìn)行了字節(jié)碼操作,把通過Socket傳過來的修改過的資源傳遞了進(jìn)去蒿柳。對(duì)Android上的資源打包不了解的同學(xué)可以去看老羅的Android應(yīng)用程序資源的編譯和打包過程分析這篇文章饶套。

2.4 cold swap:handleColdSwapPatch

private static void handleColdSwapPatch(ApplicationPatch patch) {
    if (patch.path.startsWith("slice-")) {
        File file = FileManager.writeDexShard(patch.getBytes(), patch.path);
        if (Log.isLoggable("InstantRun", 2))
            Log.v("InstantRun", "Received dex shard " + file);
    }
    }
public static File writeDexShard(byte[] bytes, String name){
    File dexFolder = getDexFileFolder(getDataFolder(), true);
    if (dexFolder == null) {
        return null;
    }
    File file = new File(dexFolder, name);
    writeRawBytes(file, bytes);
    return file;
}

對(duì)于cold swap,其實(shí)就是把數(shù)據(jù)寫進(jìn)對(duì)應(yīng)的dex中垒探,所以在art的情況下需要重啟app妓蛮,而對(duì)于API20以下的只能重新構(gòu)建和部署了。

3 總結(jié)

兩篇博客大致介紹了InstantRun的原理圾叼,從宏觀上講蛤克,InstantRun通過創(chuàng)建宿主Application的方式來代理所有來的加載,為熱更新提供了Runtime夷蚊。從微觀上來講构挤,三種情況的原理各有不同:

  • hot swap玩的方法替換,通過重新生成一個(gè)新類惕鼓,并將原有類的方法映射到新類中的方法筋现。思想上比較類似AndFix,不過AndFix的更新在native層完成呜笑,hot swap則是在java層通過插樁完成夫否。不熟悉AndFix的朋友可以看下這篇博客:AndFix Bug熱修復(fù)框架及源碼解析
  • warm swap的原理是加載resources.ap_并寫入到AssetManager的加載路徑中
  • cold swap的原理其實(shí)就是把數(shù)據(jù)寫進(jìn)對(duì)應(yīng)的dex中。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末叫胁,一起剝皮案震驚了整個(gè)濱河市凰慈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌驼鹅,老刑警劉巖微谓,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異输钩,居然都是意外死亡豺型,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門买乃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來姻氨,“玉大人,你說我怎么就攤上這事剪验‰群福” “怎么了前联?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)娶眷。 經(jīng)常有香客問我似嗤,道長(zhǎng),這世上最難降的妖魔是什么届宠? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任烁落,我火速辦了婚禮,結(jié)果婚禮上豌注,老公的妹妹穿的比我還像新娘伤塌。我一直安慰自己,他們只是感情好幌羞,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布寸谜。 她就那樣靜靜地躺著,像睡著了一般属桦。 火紅的嫁衣襯著肌膚如雪熊痴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天聂宾,我揣著相機(jī)與錄音果善,去河邊找鬼。 笑死系谐,一個(gè)胖子當(dāng)著我的面吹牛巾陕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播纪他,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼鄙煤,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了茶袒?” 一聲冷哼從身側(cè)響起梯刚,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎薪寓,沒想到半個(gè)月后亡资,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡向叉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年锥腻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片母谎。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瘦黑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情幸斥,我是刑警寧澤存崖,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站睡毒,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏冗栗。R本人自食惡果不足惜演顾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望隅居。 院中可真熱鬧钠至,春花似錦、人聲如沸胎源。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涕蚤。三九已至宪卿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間万栅,已是汗流浹背佑钾。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留烦粒,地道東北人休溶。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像扰她,于是被迫代替她去往敵國(guó)和親兽掰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

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