Flutter混合开发和动态更新的探索历程Android版

[颤振]是谷歌推出的可以高效构建的Android,iOS的界面的移动UI框架,在国内中大公司像闲鱼/现在直播等应用陆续出现它的影子,当然闲鱼的最为成熟,闲鱼也非常的高效产出了很多优秀的文章。

可是

可是,网上能找到的混合开发方案或者动态更新扑的相关文章都没法符合我自己理想的效果。所以自己摸索了一套混合开发和动态更新的方案,这里记录一下摸索过程。

扑源码分析

如果说把自家的应用改造成纯扑方案那是不可能的,顶多是某个模块或者某些模块改成扑,所以自然想到扑如何跟原生混合开发,混合开发不是说java的去调用镖中的方法更多的是指如何从当前活动跳转到颤振实现的界面,要像知道这些东西那么必须得弄懂颤振源码,不求深入但求知之一二三四。

安卓的应用那么自然先找应用,所以很快找到了FlutterApplication:

<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">公共类FlutterApplication扩展Application { private Activity mCurrentActivity = null;
public FlutterApplication(){ }
@CallSuper public void onCreate(){ super.onCreate(); FlutterMain.startInitialization(本); }
public Activity getCurrentActivity(){ return this.mCurrentActivity; }
public void setCurrentActivity(Activity mCurrentActivity){ this.mCurrentActivity = mCurrentActivity; } }</pre>

还行初始化的东西不多,直接进入的onCreate的对应FlutterMain.startInitialization中去看看:

<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public static void startInitialization(Context applicationContext,FlutterMain.Settings settings){ long initStartTimestampMillis = SystemClock.uptimeMillis(); initConfig(的applicationContext); initAot(的applicationContext); initResources(的applicationContext); 的System.loadLibrary( “扑”); long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis; nativeRecordStartTimestamp(initTimeMillis); }</pre>

不具体一行一行的看代码,但是了看到很几个关键的词在initConfig方法中:

<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">private static void initConfig(Context applicationContext){ Bundle metadata = applicationContext.getPackageManager()。getApplicationInfo(applicationContext.getPackageName(),128).metaData; if(metadata!= null){ sAotSharedLibraryPath = metadata.getString(PUBLIC_AOT_AOT_SHARED_LIBRARY_PATH,“app.so”); sAotVmSnapshotData = metadata.getString(PUBLIC_AOT_VM_SNAPSHOT_DATA_KEY,“vm_snapshot_data”); sAotVmSnapshotInstr = metadata.getString(PUBLIC_AOT_VM_SNAPSHOT_INSTR_KEY,“vm_snapshot_instr”); sAotIsolateSnapshotData = metadata.getString(PUBLIC_AOT_ISOLATE_SNAPSHOT_DATA_KEY,“isolate_snapshot_data”); sAotIsolateSnapshotInstr = metadata.getString(PUBLIC_AOT_ISOLATE_SNAPSHOT_INSTR_KEY,“isolate_snapshot_instr”); sFlx = metadata.getString(PUBLIC_FLX_KEY,“app.flx”); sSnapshotBlob = metadata.getString(PUBLIC_SNAPSHOT_BLOB_KEY,“snapshot_blob.bin”); sFlutterAssetsDir = metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY,“flutter_assets”); } }</pre>

就是没错vm_snapshot_data、vm_snapshot_instr、isolate_snapshot_data、isolate_snapshot_instr为什么说这几个这么重要呢?

在这里插入图片描述

看下上面这几个编译的产物,我们就知道这就扑动的核心东西。或者换句话说只要弄懂了这个玩意很有可能我们就悟出混合开发的方案了,那么他们是怎么读取资产目录下的这些玩意呢?

<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">private static void initAot(Context applicationContext){ 设置<String> assets = listAssets(applicationContext,“”); sIsPrecompiledAsBlobs = assets.containsAll(Arrays.asList(sAotVmSnapshotData,sAotVmSnapshotInstr,sAotIsolateSnapshotData,sAotIsolateSnapshotInstr)); sIsPrecompiledAsSharedLibrary = assets.contains(sAotSharedLibraryPath); if(sIsPrecompiledAsBlobs && sIsPrecompiledAsSharedLibrary){ 抛出新的RuntimeException(“找到预编译的应用程序作为共享库和Dart VM快照。”); } }</pre>

看到方法跟资产挂钩确实很惊喜,因为看到肯定是从资产中把这些读出来的。可是读出来放哪里去?

求最后那那个的方法initResources该方法就是涉及存放的位置,跟着源码一路看下去,在ExtractTask.extractResources找到了一点一猫腻:

<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">File dataDir = new File(PathUtils.getDataDirectory(ResourceExtractor.this.mContext));</pre>

确实,在就是data/data/xxx/flutter_assets/路径下:

在这里插入图片描述

大体知道了这些个产物之后,界面是怎么加载?首先加载Flutter的界面是个活动叫
FlutterActivity主要是通过
FlutterActivityDelegate这个类,然后我们主要看
FlutterActivity.onCreate => FlutterActivityDelegate.onCreate

这个流程:

<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public void onCreate(Bundle savedInstanceState){ //沉浸式模式 if(VERSION.SDK_INT> = 21){ Window window = this.activity.getWindow(); window.addFlags(-2147483648); window.setStatusBarColor(1073741824); 。window.getDecorView()setSystemUiVisibility(1280); }
String [] args = getArgsFromIntent(this.activity.getIntent()); FlutterMain.ensureInitializationComplete(this.activity.getApplicationContext(),args); this.flutterView = this.viewFactory.createFlutterView(this.activity); if(this.flutterView == null){ FlutterNativeView nativeView = this.viewFactory.createFlutterNativeView(); this.flutterView = new FlutterView(this.activity,(AttributeSet)null,nativeView); this.flutterView.setLayoutParams(matchParent); this.activity.setContentView(this.flutterView); this.launchView = this.createLaunchView(); if(this.launchView!= null){ this.addLaunchView(); } } }</pre>

所以最界面的重要方法就是ensureInitializationComplete也。就是把扑的相关初始化进来然后使用FlutterView进行加载显示:

<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">ensureInitializationComplete://进行初始化 String appBundlePath = findAppBundlePath(applicationContext); String appStoragePath = PathUtils.getFilesDir(applicationContext); nativeInit(applicationContext,(String [])shellArgs.toArray(new String [0]),appBundlePath,appStoragePath);
//找到data / data / xxx / flutter_assets下的flutter产物 public static String findAppBundlePath(Context applicationContext){ String dataDirectory = PathUtils.getDataDirectory(applicationContext); 文件appBundle =新文件(dataDirectory,sFlutterAssetsDir); 返回appBundle.exists()?appBundle.getPath():null; }</pre>

每然后一个FlutterView中包了一个FlutterNativeView然后名单最终就是FlutterView->runFromBundle调用FlutterNativeView->runFromBundle求最后渲染到界面上。

到此我们大概了解了颤振的需要产物vm_snapshot_data、vm_snapshot_instr、isolate_snapshot_data、isolate_snapshot_instr然后简单的了解了加载流程,最后附上大闲鱼的一张编译大图:

在这里插入图片描述

混合开发

所以我觉得扑应该跟ReactNative类似只要把相关的捆绑文件放入我们的应用程序的资产即可,所以拿这个方向开始编译扑代码,开心开心的输入侧flutter run之后在AS中怎么就是找不到相关产物,作为Android的开发者知道肯定会有个建目录怎么就是不显示所以去电脑对应的盘中看了下是有这么个建立目录但是AS不显示,这样子办事很慢所以这里需要先加一个。gradle task

<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">task flutterPlugin << { println“工程目录= {project.rootDir} /” println“编译成功的位置= {this.buildDir} /”
def projectName = this.buildDir.getPath() projectName = projectName.substring(0,projectName.length() - “app /”。length())
def rDir = new File(“{this.rootDir} / FlutterPlugin /”) def bDir =新文件(projectName) if(!rDir.exists()){ rDir.mkdirs() } else { rDir.deleteDir() } bDir.eachDir {File目录 - > def subDir = dir.getPath() def flutterJarDirName = subDir.replace(“ {projectName} /”,“”) def flutterJarDir = null if(subDir.contains(“app”)){//如果是app目录的话拷贝编译后生成的flutter目录 flutterJarDir =新文件(“{subDir} / intermediates / assets /”) } else { flutterJarDir =新文件(“ {subDir} / intermediates / intermediate-jars /”) } project.copy { 来自flutterJarDir 成为“{rDir} / {flutterJarDirName}” } } }</pre>

把看不到的建设中产物给拷贝出来,查询查询结果将工程放入的FlutterPlugin目录下:

在这里插入图片描述

红色框内的东西是扑的gradle这个插件产生的依赖包,我们也是需要的,所以顺便一起拷贝出来,那需要在哪?看下面的这个类就知道了。

<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public final class GeneratedPluginRegistrant { public static void registerWith(PluginRegistry registry){ PathProviderPlugin.registerWith(registry.registrarFor( “io.flutter.plugins.pathprovider.PathProviderPlugin”)); SharedPreferencesPlugin.registerWith(registry.registrarFor( “io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin”)); } }</pre>

到此为止我们把编译扑的产物都拷贝出来,所以我们直接将这些产物放入我们的远程工程对应的资产以及LIB路径中去。可是对应的FlutterActivity还是报红,所以说扑还有一些产物没有被我们发现。这时也不知道是什么玩意,所以就找大闲鱼的文章<贴在末尾>,找到名单最终了还有一个flutter.jar包没有引入。

在这里插入图片描述

就是这名单最终在原生的工程下新建了一个fluttermodule模块的名单最终层级关系了。然后把演示的中类相关拿进来通过startActivity成功的展示进入到FlutterActivity。

这里还是要把大闲鱼说的相关产物解释附上:

在这里插入图片描述

混合开发的巨坑:

很开心的运行然后用AS打开一看对应的flutter.so确是
armv8a

的框架,如果说直接拿到我们的应用程序中去就挂了因为我们的应用程序中:

<pre style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">ndk { abiFilters“armeabi-v7a” }</pre>

我们因为只用v7a的框架,这就很头痛了。

<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">申请自:“$ flutterRoot / packages / flutter_tools / gradle / flutter.gradle”</pre>

我们的新建扑项目有这么一个gradle这个文件,所以说这样兼容问题肯定是这货引起的。所以跟着进去看看哪里有猫腻....

还算比较顺利很快找到原因原来这个gradle插件会自动的帮你找到最适合当前环境的所以文件,所以我们只需要强制让它返回v7a的即可:

<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">Path baseEnginePath = Paths.get(flutterRoot.absolutePath,“bin”,“cache”,“artifacts”,“engine”) String targetArch ='arm' // if(project.hasProperty('target-platform')&& // project.property('target-platform')=='android-arm64'){ // targetArch ='arm64' //} // targetArch ='arm'</pre>

让也就是说targetArch为臂即可,所以说扑混合进来的时候最大的坑就是我觉得就是这样兼容问题,索性还是比较顺利。

颤振动态更新方案

当我完成混合成功之后,我就在想能不能像其他的混合开发库能实现动态更新这里再次感谢大闲鱼的思路:因为大闲鱼说直接把data/data/xxxxx下的vm_snapshot_data、vm_snapshot_instr、isolate_snapshot_data、isolate_snapshot_instr替换分类中翻译新compile-成功的那么界面加载出来的就是新的界面,所以说这不就是动态更新吗?

所以说跟着节奏试试,将编译出来的打包成ZIP放入SD卡中去......

第一步:

<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">/ ** *解压SD路径下的flutter包 * / public static void doUnzipFlutterAssets()throws Exception { String sdCardPath = Environment.getExternalStorageDirectory()。getPath()+ File.separator; String zipPath = sdCardPath +“flutter_assets.zip”; 文件zipFile =新文件(zipPath); if(zipFile.exists()){ ZipFile zFile = new ZipFile(zipFile); 枚举zList = zFile.entries(); ZipEntry zipEntry; byte [] buffer = new byte [1024];
while(zList.hasMoreElements()){ zipEntry =(ZipEntry)zList.nextElement(); Log.w(“Jacyuhou”,“==== zipEntry Name =”+ zipEntry.getName()); if(zipEntry.isDirectory()){ String destPath = sdCardPath + zipEntry.getName(); Log.w(“Jayuchou”,“==== destPath =”+ destPath); File dir = new File(destPath); dir.mkdirs(); 继续; }
OutputStream out = new BufferedOutputStream(new FileOutputStream(new File(sdCardPath + zipEntry.getName()))); InputStream = new BufferedInputStream(zFile.getInputStream(zipEntry));
int len; while((len = is.read(buffer))!= -1){ out.write(buffer,0,len); } 了out.flush(); out.close(); is.close(); } zFile.close(); } }</pre>

第二步:

<pre class="prettyprint" style="box-sizing: inherit; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 16px; overflow: auto; max-width: 100%; margin: 0px 0px 30px; padding: 20px 25px; white-space: pre-wrap; word-wrap: break-word; border: 0px; border-radius: 3px; background-color: rgb(244, 246, 249); line-height: 1.875; color: rgb(112, 112, 123); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">/ ** *拷贝到data / data路径下 * / public static void doCopyToDataFlutterAssets(Context mContext)throws Exception { String destPath = PathUtils.getDataDirectory(mContext.getApplicationContext())+ File.separator +“flutter_assets /”; String originalPath = Environment.getExternalStorageDirectory()。getPath()+ File.separator +“flutter_assets /”; Log.w(“Jayuchou”,“===== dataPath =”+ destPath); Log.w(“Jayuchou”,“===== originalPath =”+ originalPath); 文件destFile = new File(destPath); 文件originalFile = new File(originalPath);
File [] files = originalFile.listFiles(); for(文件文件:files){ Log.w(“Jayuchou”,“===== file =”+ file.getPath()); Log.w(“Jayuchou”,“===== file =”+ file.getName()); if(file.getPath()。contains(“isolate_snapshot_data”) || file.getPath()。包含( “isolate_snapshot_instr”) || file.getPath()。包含( “vm_snapshot_data”) || file.getPath()。contains(“vm_snapshot_instr”)){ doCopyToDestByFile(file.getName(),originalFile,destFile); } } }</pre>

将对应的文件拷贝到数据目录下去,跑起来看看总算是成功了...

在这里插入图片描述

看上面的gif图,一开的Flutter界面上显示null那么你完了线上的包显示null错误,所以这时就需要紧急发个补丁包,然后经过Http下载下来重新打开界面就修复了这个错误。

所以说这就是动态更新的方案...

结束...

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容