Android SDK的輕量級(jí)熱修更新

需求

最近要做sdk的熱更新
因?yàn)樾枨蠓降膕dk其實(shí)是jar包,只有class文件,沒有資源文件淆九,所以此文只針對(duì)class文件更新

首先羅列下一個(gè)輕量級(jí)更新框架的功能最小邊界:

  • 需要配置文件描述更新包
  • 更新包需要在線下載劲蜻,并檢驗(yàn)包的完整性
  • 只針對(duì)特定版本
  • 針對(duì)特定渠道
  • 補(bǔ)丁包的版本控制

調(diào)研

市面上比較流行的熱更新有Tinker、QZone尤辱、AndFix砂豌、Sophix、Robust光督、Dexposed這些大家都很熟悉阳距,但絕大部分是針對(duì)app的
先回顧下class文件是如何實(shí)現(xiàn)熱修復(fù)的:

概念:

DexClassLoader和PathClassLoader

回顧下Android中Classloader的知識(shí)
DexClassLoaderPathClassLoader都是繼承自BaseDexClassLoader
看下Android10的代碼

public class DexClassLoader extends BaseDexClassLoader {
    /**
     * ...
     */
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

可見這兩個(gè)類沒有本質(zhì)上區(qū)別,網(wǎng)上說的optimizedDirectory在Api26之后已廢棄
只不過PathClassLoader多了構(gòu)造函數(shù)參數(shù)sharedLibraryLoaders结借,可以加載系統(tǒng)類

ClassLoader

在Java的ClassLoader中:

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

findLoadedClass是保證加載的類不會(huì)被加載
findBootstrapClassOrNull返回的是空
所以調(diào)用的findClass方法

BaseDexClassLoader的findClass方法:

 @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // First, check whether the class is present in our shared libraries.
        if (sharedLibraryLoaders != null) {
            for (ClassLoader loader : sharedLibraryLoaders) {
                try {
                    return loader.loadClass(name);
                } catch (ClassNotFoundException ignored) {
                }
            }
        }
        // Check whether the class in question is present in the dexPath that
        // this classloader operates on.
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

可以由傳進(jìn)來的sharedLibraryLoaders來加載筐摘,但DexClassLoader的sharedLibraryLoaders為空
是交由DexPathList類來處理實(shí)現(xiàn)findClass

DexPathList

 DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
       ...
        this.definingContext = definingContext;

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);

        //native相關(guān)
        this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
        this.systemNativeLibraryDirectories =
                splitPaths(System.getProperty("java.library.path"), true);
        this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories());
        ...
    }

通過Element[]數(shù)組來存儲(chǔ)dex,makeDexElements這個(gè)方法:

 private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      for (File file : files) {
          if (file.isDirectory()) {
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();
              DexFile dex = null;
              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else {
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      suppressedExceptions.add(suppressed);
                  }
                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
              if (dex != null && isTrusted) {
                dex.setTrusted();
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

DexPathList在構(gòu)造函數(shù)通過makeDexElements方法船老,生成Element[]數(shù)組
在BaseDexClassLoader中調(diào)用的是DexPathList的findClass方法:

public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

很清楚的看到element是按順序取出
也就是說兩個(gè)element如果有相同的class咖熟,第二個(gè)就不會(huì)加載,如果將目標(biāo)dex包插隊(duì)到列隊(duì)最前面柳畔,則可以實(shí)現(xiàn)目標(biāo)dex包中的class替換掉其他dex中的class
熱修復(fù)的實(shí)現(xiàn):

  1. DexClassLoader加載目標(biāo)dex
  2. 將目標(biāo)的和系統(tǒng)的 dexElements 進(jìn)行合并
  3. 賦值給系統(tǒng)的 pathList
 public void loadCustomDex(Context mContext){
      //遍歷所有修復(fù)的dex
      File fileDir = yourfilepath;
      File[] listFiles = fileDir.listFiles();
      for (File f : listFiles){
          if(isValid(f)){
              dexList.add(f);
          }
      }
      //合并與應(yīng)用
      PathClassLoader pathClassLoader = (PathClassLoader) mContext.getClassLoader();
      for (File f : dexList){
          //加載指定的dex文件馍管。
          DexClassLoader dexClassLoader = new DexClassLoader(f.getAbsolutePath(),null,null,pathClassLoader);
          //合并
          Object dexObj = getPathList(dexClassLoader);
          Object pathObj = getPathList(pathClassLoader);
          Object dexElementsList = getDexElements(dexObj);
          Object pathElementsList = getDexElements(pathObj);
          Object newElements = combineArray(dexElementsList,pathElementsList);
          //給PathList里面的dexElements賦值
          Object pathList = getPathList(pathClassLoader);
          setField(pathList,pathList.getClass(),"dexElements",newElements);
      }
  }
 
    
private Object getPathList(Object obj) throws Exception {
        return reflectField(obj,Class.forName("dalvik.system.BaseDexClassLoader"),"pathList");
}

private Object getDexElements(Object obj) throws Exception {
        return reflectField(obj,obj.getClass(),"dexElements");
}

private static Object combineArray(Object array1, Object array2) {
        Class<?> localClass = array1.getClass().getComponentType();
        int i = Array.getLength(array1);
        int j = i + Array.getLength(array2);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(array1, k));
            } else {
                Array.set(result, k, Array.get(array2, k - i));
            }
        }
        return result;
    }

方案:

上述的熱更新實(shí)現(xiàn)方式有兩個(gè)問題需要解決:

  • app中可以在Application中的attachBaseContext中去做插入dex,SDK的類是不知道何時(shí)被調(diào)用的
  • ISPREVERIFIED問題

如果一個(gè)類的static方法薪韩,private方法确沸,override方法以及構(gòu)造函數(shù)中引用了其他類,而且這些類都屬于同一個(gè)dex文件俘陷,此時(shí)該類就會(huì)被打上CLASS_ISPREVERIFIED
如果在運(yùn)行時(shí)被打上CLASS_ISPREVERIFIED的類引用了其他dex的類罗捎,就會(huì)報(bào)錯(cuò)

解決ISPREVERIFIED問題,常用的就是“插裝”(往字節(jié)碼里插入自定義的字節(jié)碼)岭洲,我們只要讓所有類都引用其他dex中的某個(gè)類就可以了:

  1. 當(dāng)安裝apk的時(shí)候宛逗,classes.dex內(nèi)的類都會(huì)引用一個(gè)在不相同dex中的XX類,這樣就防止了類被打上CLASS_ISPREVERIFIED的標(biāo)志了盾剩,只要沒被打上這個(gè)標(biāo)志的類都可以進(jìn)行打補(bǔ)丁操作
  2. 我們需要在源碼編譯成字節(jié)碼之后雷激,在字節(jié)碼中進(jìn)行插入操作替蔬。對(duì)字節(jié)碼進(jìn)行操作的框架有很多,但是比較常用的則是ASM和javaassist

那么有沒有簡(jiǎn)單點(diǎn)的方法:

  1. 將工程拆分屎暇,保證要進(jìn)行更換的工程與其他工程沒有直接引用(都用反射)
  2. 對(duì)要進(jìn)行更換的工程采用全量替換方法承桥,考慮到sdk體積比較小,此方法最簡(jiǎn)單

配置文件

對(duì)應(yīng)需求中的最小邊界根悼,定義了如下的配置文件:

{
    "checksum": {
        "TYPE": "md5",
        "value": "20d81614ab8ac44b3185af0f5e8a96b2"
    },
    "channel": "abcdefgh",
    "version": "1.0.0",
    "subVersion": 1,
    "package": "http://robinfjb.github.io/dex.jar",
    "className": "robin.sdk.sdk_impl2.ServiceImpl"
}
  1. checksum是對(duì)包完整性的驗(yàn)證
  2. channel是渠道凶异,在sdk一般用appkey作為渠道標(biāo)識(shí)
  3. version目標(biāo)版本,由于sdk的定制化比較多挤巡,不同版本的代碼功能都不一樣剩彬,所以只針對(duì)某個(gè)版本更新
  4. subVersion補(bǔ)丁版本,只有新的補(bǔ)丁才會(huì)應(yīng)用
  5. package包的下載地址
  6. className新包的入口類名矿卑,后面會(huì)講到

工程

我們選擇類似類加載方案
將整個(gè)sdk拆分成 業(yè)務(wù)實(shí)現(xiàn)喉恋,更新模塊,對(duì)外api 三個(gè)模塊

  • 業(yè)務(wù)實(shí)現(xiàn):sdk-impl , 更新則只要更新業(yè)務(wù)實(shí)現(xiàn)模塊
  • 更新模塊:sdk-dynamic , 實(shí)現(xiàn)熱更新下載母廷,應(yīng)用
  • 對(duì)外api:sdk轻黑,sdk對(duì)外暴露的類
  • 代理: sdk-proxy 為了避免sdk-impl對(duì)sdk的依賴,用單獨(dú)工程維護(hù)兩個(gè)模塊之間的接口
  • 公共:sdk-common 放一些基礎(chǔ)類琴昆,比如log之類

代碼

命名

補(bǔ)丁類可以命名為packagename+version
比如業(yè)務(wù)包名:robin.sdk.sdk_impl
補(bǔ)丁包名則為:robin.sdk.sdk_impl1

關(guān)鍵代碼:

代理:

例子中使用了CS結(jié)構(gòu)氓鄙,我們將service代理:

public interface ServiceProxy {
    void onCreate(Context var1);

    int onStartCommand(Intent var1, int var2, int var3);

    IBinder onBind(Intent var1);

    boolean onUnBind(Intent var1);

    void onDestroy();
}

對(duì)外的service中實(shí)現(xiàn):

public final class RobinService extends Service {
    @Override
    public void onCreate() {
        try {
            context = getApplicationContext();
            proxy = serviceLoad();
            proxy.onCreate(this);
            checkUpdate();
        } catch (Throwable e) {
        }
    }
    
    @Override
    public int onStartCommand(Intent var1, int var2, int var3) {
        return proxy.onStartCommand(var1, var2, var3);
    }

    @Override
    public IBinder onBind(Intent var1) {
        return proxy.onBind(var1);
    }

    @Override
    public boolean onUnbind(Intent var1) {
        return proxy.onUnBind(var1);
    }

    @Override
    public void onDestroy() {
        proxy.onDestroy();
    }
}

然后在sdk-impl中實(shí)現(xiàn)代理接口:

public class ServiceImpl implements ServiceProxy {
}

這個(gè)類就是我們需要更換的類

下載補(bǔ)丁

在程序入口(Service onCreate)方法, 開啟下載
先下載配置文件,下載完成后:

DyInfo dyInfo = new DyInfo(new JSONObject(response));
if (checkDyInfo(context, dyInfo)) {//檢查配置文件有效性
    LogUtil.e(UPDATE_TAG, "downloadDyJar");
    downloadDyJar(dyInfo, context);
}

checkDyInfo檢查當(dāng)前包是否需要下載與應(yīng)用補(bǔ)丁
幾個(gè)關(guān)鍵檢測(cè):

  • 檢測(cè)key是否等于當(dāng)前key业舍,說明是針對(duì)某個(gè)key發(fā)的補(bǔ)丁抖拦,如果為空說明是全部渠道都應(yīng)用補(bǔ)丁
  • 檢測(cè)SDK版本是否和配置文件中一致,補(bǔ)丁只針對(duì)某個(gè)版本
  • 檢測(cè)補(bǔ)丁版本勤讽,在補(bǔ)丁應(yīng)用成功后會(huì)記錄應(yīng)用的補(bǔ)丁版本蟋座,只有新的補(bǔ)丁版本大于已應(yīng)用的,補(bǔ)丁才會(huì)下載與生效

下載完成后:檢查文件完整性與保存配置信息

if (!checkJarMd5(context, dyInfo.checksumValue)) {
    LogUtil.e(UPDATE_TAG, "checkJarMd5 fail");
    deleteFailjar(context);
} else {
    //保存jar信息
    SpUtil.setDyInfo(context, dyInfo);
    LogUtil.e(UPDATE_TAG, "checkJarMd5 success");
}

應(yīng)用補(bǔ)丁

        try {
            DyInfo dyInfo = SpUtil.getDyInfo(context);
            if (UpdateManager.checkDyInfo(context, dyInfo) && checkJar(dyInfo, usingJar)) {
                DexClassLoader dexClassLoader = new DexClassLoader(usingJar.getAbsolutePath(),
                        context.getCacheDir().getAbsolutePath(), null, context.getClassLoader());
                Class libclass = dexClassLoader.loadClass(dyInfo.className);
                lib = (ServiceProxy) libclass.newInstance();
                SpUtil.setPatchVersion(context, dyInfo.subVersion);
                LogUtil.e(DYNAMIC_TAG, "動(dòng)態(tài)包已加載成功 ");
            }
        } catch (Throwable throwable) {
            LogUtil.e(DYNAMIC_TAG, "動(dòng)態(tài)包加載異常:" + throwable.getLocalizedMessage());
        }
        if(lib == null) {
            try {
                Class libclass = context.getClassLoader().loadClass("robin.sdk.sdk_impl.ServiceImpl");
                lib = (ServiceProxy) libclass.newInstance();
            } catch (Throwable throwable) {
                LogUtil.e(DYNAMIC_TAG, "正常包加載異常:" + throwable.getLocalizedMessage());
            }
        }
  • 讀出上一次下載的配置文件與補(bǔ)丁包脚牍,進(jìn)行校驗(yàn)
  • DexClassLoader加載目標(biāo)usingJar(data/user/0/packagename/file/robin/dex.jar)的包
  • DexClassLoader加載包中的類ServiceImpl(類名比如:robin.sdk.sdk_impl1.ServiceImpl
  • 如果未加載到向臀,則加載默認(rèn)包中的類robin.sdk.sdk_impl.ServiceImpl

腳本

sdk

Android studio的assembleXXX往往打的都是aar包,我們需要一個(gè)jar包的gradle task:

task makeSdkJar(type: Jar) {
   //指定生成的jar名
    baseName 'sdk'
    //從哪里打包c(diǎn)lass文件
    from('build/intermediates/javac/release/classes/')
    from('../sdk-proxy/build/intermediates/javac/release/classes/')
    from('../sdk-common/build/intermediates/javac/release/classes/')
    from('../sdk-dynamic/build/intermediates/javac/release/classes/')
    from('../sdk-impl/build/intermediates/javac/release/classes/')
}
makeSdkJar.dependsOn(clean, 'compileReleaseJavaWithJavac')

注意:由于gradle版本不同,intermediates目錄的路徑可能會(huì)有變化

打出sdk.jar包诸狭,然后進(jìn)行混淆

task _proguardJar(dependsOn: makeSdkJar, type: proguard.gradle.ProGuardTask) {
    String inJar = makeSdkJar.archivePath.getAbsolutePath()
    println("正在混淆jar...path= " + inJar)

    injars inJar
    outjars "build/libs/proguard.jar"
    configuration "$rootDir/sdk/proguard-rules.pro"
}

proguard-rules.pro中券膀,需要keep住所有需要反射的類

-keep public class robin.sdk.*.ServiceImpl {*;}
-keep class robin.sdk.*.ServiceImpl$* {*;}

對(duì)于sdk對(duì)外的類,也需要keep:

-keep public class robin.sdk.hotfix.RobinClient {*;}
-keep class robin.sdk.hotfix.RobinClient$* {*;}

補(bǔ)丁

這里采用全量打包方式驯遇,將sdk的jar包解壓芹彬,去除hotfix,proxy叉庐,sdk_common舒帮,service_dynamic目錄:

//打patch任務(wù)
task renameJar(type: Copy) {
    from 'build/libs/'
    include 'proguard.jar'
    destinationDir file('build/libs/')
    rename 'proguard.jar', "classes.zip"
}

task upzip(dependsOn: renameJar, type: Copy) {
    def zipFile = file('build/libs/classes.zip')
    def outputDir = file("build/libs/unzip")
    from zipTree(zipFile)
    into outputDir
}

task _patchProguardJar(dependsOn: upzip, type: Jar) {
//指定生成的jar名
    baseName 'patch'
    from('build/libs/unzip/')
    exclude('robin/sdk/hotfix')
    exclude('robin/sdk/proxy')
    exclude('robin/sdk/sdk_common')
    exclude('robin/sdk/service_dynamic')
    doLast {
        delete('build/libs/unzip')
        delete('build/libs/classes.zip')
    }
}

將jar包轉(zhuǎn)dex方便DexClassLoader加載:

task _jarToDex(type: Exec) {
    commandLine 'cmd'
    doFirst {
        //jar文件對(duì)象
        def srcFile = file("/build/libs/hot.jar")
        //需要生成的dex文件對(duì)象
        def desFile = file(srcFile.parent + "/" + "dex.jar")
        workingDir srcFile.parent
        //拼接dx.bat執(zhí)行的參數(shù)
        def list = []
        list.add("/c")
        list.add("dx")
        list.add("--dex")
        list.add("--output")
        list.add(desFile)
        list.add(srcFile)
        args list
    }
}

測(cè)試驗(yàn)證

使用_proguardJar task打包sdk.jar文件,放到測(cè)試app中:
app的gradle依賴:

implementation files("libs/sdk.jar")

首次啟動(dòng):

日志如下:

image

此日志為CLient和Service直接的交互,可見Service為robin.sdk.sdk_impl.ServiceImpl,為原版的Service
image

此日志為ServiceImpl中類的引用玩郊,目前的類為robin.sdk.sdk_impl.a(已混淆)

Service啟動(dòng)后會(huì)滿足條件自動(dòng)下載補(bǔ)丁包肢执;


image

第二次啟動(dòng):

日志如下:
加載動(dòng)態(tài)包后的:

image

原來的robin.sdk.sdk_impl.a已替換為robin.sdk.sdk_impl2.a
image

Service也替換成功

工程代碼:

https://github.com/robinfjb/Android_SDK_Hotfix

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市译红,隨后出現(xiàn)的幾起案子预茄,更是在濱河造成了極大的恐慌,老刑警劉巖侦厚,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件耻陕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡刨沦,警方通過查閱死者的電腦和手機(jī)诗宣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來想诅,“玉大人梧田,你說我怎么就攤上這事〔嗾海” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵鹉梨,是天一觀的道長(zhǎng)讳癌。 經(jīng)常有香客問我,道長(zhǎng)存皂,這世上最難降的妖魔是什么晌坤? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮旦袋,結(jié)果婚禮上骤菠,老公的妹妹穿的比我還像新娘。我一直安慰自己疤孕,他們只是感情好商乎,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著祭阀,像睡著了一般鹉戚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上专控,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天抹凳,我揣著相機(jī)與錄音,去河邊找鬼伦腐。 笑死赢底,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播幸冻,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼粹庞,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了嘁扼?” 一聲冷哼從身側(cè)響起信粮,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎趁啸,沒想到半個(gè)月后强缘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡不傅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年旅掂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片访娶。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡商虐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出崖疤,到底是詐尸還是另有隱情秘车,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布劫哼,位于F島的核電站叮趴,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏权烧。R本人自食惡果不足惜眯亦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望般码。 院中可真熱鬧妻率,春花似錦、人聲如沸板祝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扔字。三九已至囊嘉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間革为,已是汗流浹背扭粱。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留震檩,地道東北人琢蛤。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓蜓堕,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親博其。 傳聞我的和親對(duì)象是個(gè)殘疾皇子套才,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349