ZeusPlugin:掌閱Android App插件補丁實踐

原文鏈接

遇到問題

  • 65K方法數(shù)超限

    隨著應用不斷迭代,業(yè)務線的擴展,應用越來越大达箍,那么很不幸堪藐,總有一天细移,當你編譯的時候,會遇到一個類似下面的錯誤:

    Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536
    

    沒錯,這就是臭名昭著的65536方法數(shù)超限問題。具體原理可以參考由Android 65K方法數(shù)限制引發(fā)的思考這篇文章。當然充择,google也意識到這個問題,所以發(fā)布了MultiDex支持庫匪蟀。喜大普奔椎麦,趕緊使用,問題解決材彪?Too Young ! 使用過程中观挎,你會發(fā)現(xiàn)MultiDex有不少坑:啟動時間過長、ANR/Crash段化。當然也有解決方法嘁捷,可以參考美團自動拆包方案。但我只想說真的太....麻煩了显熏,還能不能愉快地回家玩游戲了....

  • 上線太慢雄嚣,更新率太低

    總所周知,Android APP發(fā)布流程較為漫長,一般需要經(jīng)歷開發(fā)完成—上傳市場—審核—上線幾個階段缓升,而且各個市場都各有各的政策和審核速度鼓鲁,每發(fā)一版都是一次煎熬呀。再者港谊,Android APP的升級率跟Android系統(tǒng)升級率一樣骇吭,怎一個慢字了得。新版本要覆蓋80%左右封锉,怎么也需要兩周左右绵跷。

  • 一上線就如臨大敵

    以為應用上線就完事了膘螟?NO 成福!相信大部分開發(fā)同學在應用上線的頭一周都是過得提心吊膽的,祈禱著不要出bug荆残,用戶不要反饋問題奴艾。但往往事與愿違,怎么辦内斯,趕緊出hotfix版本蕴潦?

解決方案

就不賣關子了,是的俘闯,我們的解決方案是構(gòu)建一套插件補丁的方案潭苞,期望可以無痛解決以上問題。插件化和補丁在目前看來是老生常談的東西了真朗,市面上已經(jīng)有一堆實現(xiàn)方案此疹,如DroidPluginSmall遮婶、Android-Plugin-Framework蝗碎。掌閱研究插件化是從2014年中開始,研究補丁是從2016年初開始旗扑,相對來說蹦骑,算是比較晚。直至目前臀防,插件化方案已經(jīng)達到相對成熟的階段眠菇,而補丁方案也已經(jīng)上線。秉著開源的精神袱衷,我們的插件補丁方案最近已經(jīng)在Github開源— ZeusPlugin捎废。相對其他插件化和熱修復方案,ZeusPlugin最大特點是:簡單易懂祟昭,核心類只有6個缕坎,類總數(shù)只有13個,我們期望開發(fā)同學在使用這套方案的同時能理解所有的實現(xiàn)細節(jié)篡悟,在我們看來谜叹,這確實不是什么晦澀難懂的東西匾寝。

原理

要實現(xiàn)插件補丁,其實無非就是要解決幾個問題:插件安裝荷腊、資源加載和類加載艳悔。這幾點,我們可以參考Android系統(tǒng)加載APK的實現(xiàn)原理女仰。

Android系統(tǒng)加載APK

  • APK安裝過程

    1. 復制APK安裝包到data/app臨時目錄下猜年,如vmdl648417937.tmp/base.apk

    2. 解析應用程序的配置文件AndroidManifest.xml疾忍;

    3. 進行Dexopt并生成ODEX,如vmdl648417937.tmp/oat/arm/base.odex乔外;

    4. 將臨時目錄(vmdl648417937.tmp)重命名為packageName + "-" + suffix,如com.test_1

    5. 在PackageManagerService中將上述步驟生成的apk信息通過mPackages成員變量緩存起來一罩;

      mPackages是個ArrayMap杨幼,key為包名,value為PackageParser.Package(apk包信息)

    6. 在data/data目錄下創(chuàng)建對應的應用數(shù)據(jù)目錄聂渊。

  • 啟動APK過程

    1. 點擊桌面App圖標差购,Launcher接收到點擊事件,獲取應用信息汉嗽,通過Binder IPC向SystemService進程(即system_process)發(fā)起startActivity請求(?ActivityManagerService(AMS)#startActivity)欲逃;
    2. SystemServer(AMS) 向zygote進程請求啟動一個新進程(ActivityManagerService#startProcessLocked);
    3. Zygote進程fork出新的子進程(APP進程)饼暑,在新進程中執(zhí)行 ActivityThread 類的 main 方法稳析;
    4. App進程創(chuàng)建ActivityThread實例,并通過Binder IPC向 SystemServer(AMS) 請求 attach 到 AMS;
    5. SystemServer(AMS) 進程在收到請求后撵孤,進行一系列準備工作后迈着,再通過binder IPC向App進程發(fā)送bindApplicationscheduleLaunchActivity請求;
    6. App進程(ActivityThread)在收到bindApplication請求后邪码,通過handler向主線程發(fā)送BIND_APPLICATION消息裕菠;
    7. 主線程在收到BIND_APPLICATION消息后,根據(jù)傳遞過來的ApplicationInfo創(chuàng)建一個對應的LoadApk對象(標志當前APK信息),然后創(chuàng)建ContextImpl對象(標志當前進程的環(huán)境),緊接著通過反射創(chuàng)建目標Application闭专,并調(diào)用其attach方法奴潘,將ContextImpl對象設置為目標Application的上下文環(huán)境,最后調(diào)用Application的onCreate函數(shù)影钉,做一些初始工作画髓;
    8. App進程(ApplicationThread)在收到scheduleLaunchActivity請求后,通過handler向主線程發(fā)送LAUNCH_ACTIVITY消息平委;
    9. 主線程在收到LAUNCH_ACTIVITY消息后奈虾,通過反射機制創(chuàng)建目標Activity,并調(diào)用Activity的onCreate()方法。

以上分析都是基于Android 6.0的源碼肉微,其他版本可能有少許差異匾鸥,但不影響主流程,限于篇幅問題碉纳,在此不一一展開分析勿负,只重點分析相關的關鍵幾個步驟。

為什么提到Android系統(tǒng)加載APK的流程劳曹,因為分析完Android系統(tǒng)加載APK的流程奴愉,插件補丁方案也就基本能實現(xiàn)出來了,下面我展開說一下铁孵。

插件安裝

從APK安裝過程分析得知

  1. 配置文件AndroidManifest.xml是在應用安裝時就已經(jīng)解析并記錄锭硼,所以插件的AndroidManifest.xml配置無法生效
  2. 每個APK安裝都是獨享空間的,不同APK库菲、同一個APK的不同時間安裝都是完全獨立的账忘。這樣做志膀,個人覺得大大降低了系統(tǒng)的復雜度熙宇,而且清晰明了。在這點上溉浙, ZeusPlugin插件安裝策略幾乎就是仿照系統(tǒng)設計的烫止。具體可以參考 ZeusPlugin源碼,在此不展開描述戳稽。

類加載

從上述啟動APK過程分析7馆蠕、9可以得知,Application和Activity都是通過反射機制創(chuàng)建的惊奇,我們可以看看Application創(chuàng)建具體源碼實現(xiàn):

ActivityThread#handleBindApplication

   private void handleBindApplication(AppBindData data) {
            ......
             //省略代碼
        .......
             //生成APK信息LoadedApk互躬,即packageInfo
           data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
             //創(chuàng)建上下文環(huán)境
           final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
            ......
             //省略代碼
        .......
           try {
               // If the app is being launched for full backup or restore, bring it up in
               // a restricted environment with the base application class.
                //通過反射機制創(chuàng)建Application實例
               Application app = data.info.makeApplication(data.restrictedBackupMode, null);
               mInitialApplication = app;

            ......
                //省略代碼
             .......

               try {
                    //調(diào)用Application onCreate方法·
                   mInstrumentation.callApplicationOnCreate(app);
               } catch (Exception e) {
                   if (!mInstrumentation.onException(app, e)) {
                       throw new RuntimeException(
                           "Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
                         }
          }
      } finally {
          StrictMode.setThreadPolicy(savedPolicy);
      }
  }

我們再看看LoadedApk#makeApplication的實現(xiàn)

   public Application makeApplication(boolean forceDefaultAppClass,
              Instrumentation instrumentation) {
          if (mApplication != null) {
              return mApplication;
          }

          Application app = null;

          String appClass = mApplicationInfo.className;
          if (forceDefaultAppClass || (appClass == null)) {
              appClass = "android.app.Application";
          }

          try {
                //獲取ClassLoader
              java.lang.ClassLoader cl = getClassLoader();
              if (!mPackageName.equals("android")) {
                  initializeJavaContextClassLoader();
              }
              ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
              //使用獲取到的ClassLoader通過反射機制創(chuàng)建Application實例,其內(nèi)部實現(xiàn)是通過     ClassLoader.loadClass(className)得到Application Class
              app = mActivityThread.mInstrumentation.newApplication(
                      cl, appClass, appContext);
              appContext.setOuterContext(app);
          } catch (Exception e) {
              if (!mActivityThread.mInstrumentation.onException(app, e)) {
                  throw new RuntimeException(
                      "Unable to instantiate application " + appClass
                      + ": " + e.toString(), e);
              }
          }
          mActivityThread.mAllApplications.add(app);
          mApplication = app;

         ......
          //省略代碼
       .......

          return app;
      }

從上述代碼可以得知颂郎,系統(tǒng)加載Application時候是先獲取一個特定ClassLoader吼渡,然后該ClassLoader通過反射機制創(chuàng)建Application實例。我們繼續(xù)看看getClassLoader()的實現(xiàn)

  public ClassLoader getClassLoader() {
          synchronized (this) {
              if (mClassLoader != null) {
                  return mClassLoader;
              }

              if (mIncludeCode && !mPackageName.equals("android")) {
                  ......
                 //省略代碼
              .......
               //創(chuàng)建ClassLoader
                  mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
                          mBaseClassLoader);

                  StrictMode.setThreadPolicy(oldPolicy);
              } else {
                  if (mBaseClassLoader == null) {
                      mClassLoader = ClassLoader.getSystemClassLoader();
                  } else {
                      mClassLoader = mBaseClassLoader;
                  }
              }
              return mClassLoader;
          }
      }

繼續(xù)跟蹤ApplicationLoaders類


      public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)
      {
        
          ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();

          synchronized (mLoaders) {
              if (parent == null) {
                  parent = baseParent;
              }

              /*
               * If we're one step up from the base class loader, find
               * something in our cache.  Otherwise, we create a whole
               * new ClassLoader for the zip archive.
               */
              if (parent == baseParent) {
                  ClassLoader loader = mLoaders.get(zip);
                  if (loader != null) {
                      return loader;
                  }
      
                  Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
                
                  PathClassLoader pathClassloader =
                      new PathClassLoader(zip, libPath, parent);
                  Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        
                  mLoaders.put(zip, pathClassloader);
                  return pathClassloader;
              }

              Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
              PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
              Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
              return pathClassloader;
          }
      }

ApplicationLoaders是一個靜態(tài)緩存工具類乓序,其內(nèi)部維護了一個key為dexPath寺酪,value為PathClassLoader的ArrayMap,可以看到替劈,應用程序使用的ClassLoader都是同一個PathClassLoader類的實例

我們繼續(xù)扒一扒PathClassLoader的源碼寄雀,發(fā)現(xiàn)其實現(xiàn)都在父類BaseDexClassLoader中,我們直接找到其findClass方法

  protected Class<?> findClass(String name) throws ClassNotFoundException {
          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;
      }

可以看到陨献,查找Class的任務通其內(nèi)部一個DexPathList類對象實現(xiàn)的盒犹,它的findClass方法如下:

   public Class findClass(String name, List<Throwable> suppressed) {
          for (Element element : dexElements) {
              DexFile dex = element.dexFile;

              if (dex != null) {
                  Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                  if (clazz != null) {
                      return clazz;
                  }
              }
          }
          if (dexElementsSuppressedExceptions != null) {
              suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
          }
          return null;
      }

至此,真相大白,原來急膀,APK類加載是通過遍歷dexElements這個數(shù)組來查找Class膜蛔,而dexElements就是APK dexPath里面的文件。

從上述分析可以得知要實現(xiàn)插件的類加載有兩種方式:

  1. 把插件的信息通過反射放進這個數(shù)組里面
  2. 替換系統(tǒng)的ClassLoader

考慮到類的隔離性以及框架拓展性脖阵,ZeusPlugin目前使用的方案是第二種皂股,根據(jù)類加載器的雙親委派模型,我們可以實現(xiàn)一套插件補丁類加載方案命黔,如下圖:

類加載.jpg
  1. 我們通過反射修改系統(tǒng)的ClassLoader為ZeusClassLoader呜呐,其內(nèi)包含多個ZeusPluginClassLoader
  2. 每一個插件對應一個ZeusPluginClassLoader,當移除插件時則刪除一個ZeusPluginClassLoader悍募,加載一個插件則添加一個ZeusPluginClassLoader蘑辑,
  3. ZeusClassLoader的parent為原始APK的ClassLoader(PathClassLoader),而原始APK的ClassLoader的parent(PathClassLoader)為ZeusHotfixClassLoader, ZeusHotfixClassLoader的parent為系統(tǒng)的ClassLoader(BootClassLoader)坠宴。

資源加載

關于資源加載洋魂,我們回到handleBindApplication方法

  private void handleBindApplication(AppBindData data) {
            ......
            //省略代碼
        .......
            //生成APK信息LoadedApk,即packageInfo
          data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
             //創(chuàng)建上下文環(huán)境
          final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
        ......
            //省略代碼
        .......
    }

這里創(chuàng)建了上下文環(huán)境喜鼓,即ContextImpl副砍,再看看createAppContext方法真正實現(xiàn):

  private ContextImpl(ContextImpl container, ActivityThread mainThread,
              LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
              Display display, Configuration overrideConfiguration, int createDisplayWithId) {
          ......
            //省略代碼
        .......
        //真正創(chuàng)建Resources的地方
          Resources resources = packageInfo.getResources(mainThread);
          if (resources != null) {
              if (displayId != Display.DEFAULT_DISPLAY
                      || overrideConfiguration != null
                      || (compatInfo != null && compatInfo.applicationScale
                              != resources.getCompatibilityInfo().applicationScale)) {
                  resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
                          packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
                          packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
                          overrideConfiguration, compatInfo);
              }
          }
          mResources = resources;
        ......
            //省略代碼
        .......
       
      }

Resources resources = packageInfo.getResources(mainThread);這段代碼就是真正創(chuàng)建Resources的地方,我們繼續(xù)跟進去會發(fā)現(xiàn)它最終調(diào)用的是ResourcesManager的getTopLevelResources方法

   Resources getTopLevelResources(String resDir, String[] splitResDirs,
              String[] overlayDirs, String[] libDirs, int displayId,
              Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
          final float scale = compatInfo.applicationScale;
          Configuration overrideConfigCopy = (overrideConfiguration != null)
                  ? new Configuration(overrideConfiguration) : null;
          ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale);
          Resources r;
          synchronized (this) {
              // Resources is app scale dependent.
              if (DEBUG) Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
           //判斷是否已經(jīng)存在Resources
              WeakReference<Resources> wr = mActiveResources.get(key);
              r = wr != null ? wr.get() : null;
              //if (r != null) Log.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
              if (r != null && r.getAssets().isUpToDate()) {
                  if (DEBUG) Slog.w(TAG, "Returning cached resources " + r + " " + resDir
                          + ": appScale=" + r.getCompatibilityInfo().applicationScale
                          + " key=" + key + " overrideConfig=" + overrideConfiguration);
                  return r;
              }
          }

          //if (r != null) {
          //    Log.w(TAG, "Throwing away out-of-date resources!!!! "
          //            + r + " " + resDir);
          //}
        //創(chuàng)建資源管理器
          AssetManager assets = new AssetManager();
          // resDir can be null if the 'android' package is creating a new Resources object.
          // This is fine, since each AssetManager automatically loads the 'android' package
          // already.
          if (resDir != null) {
             //添加APK資源路徑
              if (assets.addAssetPath(resDir) == 0) {
                  return null;
              }
          }
        ......
            //省略代碼
        .......
          //創(chuàng)建Resources
          r = new Resources(assets, dm, config, compatInfo);
          if (DEBUG) Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
                  + r.getConfiguration() + " appScale=" + r.getCompatibilityInfo().applicationScale);

          synchronized (this) {
              WeakReference<Resources> wr = mActiveResources.get(key);
              Resources existing = wr != null ? wr.get() : null;
              if (existing != null && existing.getAssets().isUpToDate()) {
                  // Someone else already created the resources while we were
                  // unlocked; go ahead and use theirs.
                  r.getAssets().close();
                  return existing;
              }

              // XXX need to remove entries when weak references go away
              mActiveResources.put(key, new WeakReference<>(r));
              if (DEBUG) Slog.v(TAG, "mActiveResources.size()=" + mActiveResources.size());
              return r;
          }
      }

至此庄岖,Resources就創(chuàng)建好了豁翎,這里有一個關鍵的類AssetManager,它是應用程序的資源管理器隅忿,在它的構(gòu)造函數(shù)里會把framework/framework-res.apk也會添加到資源路徑中心剥,這是C++調(diào)用,有興趣的話背桐,可以參考一下老羅這篇文章优烧。同時這也解釋了為什么我們開發(fā)的應用可以訪問到系統(tǒng)的資源。

通過上述分析链峭,我們可以得知畦娄,要實現(xiàn)插件資源加載,只需創(chuàng)建一個AssetManager,然后把把宿主資源路徑和插件apk路徑添加進去熏版,創(chuàng)建我們自己的Resources纷责,然后通過反射把PackageInfo的mResources替換成我們的Resources即可,具體代碼如下:

   AssetManager assetManager = AssetManager.class.newInstance();
              Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
              addAssetPath.invoke(assetManager, mBaseContext.getPackageResourcePath());
              if (mLoadedPluginList != null && mLoadedPluginList.size() != 0) {
                  //每個插件的packageID都不能一樣
                  for (String id : mLoadedPluginList.keySet()) {
                      addAssetPath.invoke(assetManager, PluginUtil.getAPKPath(id));
                  }
              }
              //這里提前創(chuàng)建一個resource是因為Resources的構(gòu)造函數(shù)會對AssetManager進行一些變量的初始化
              //還不能創(chuàng)建系統(tǒng)的Resources類撼短,否則中興系統(tǒng)會出現(xiàn)崩潰問題
              PluginResources newResources = new PluginResources(assetManager,
                      mBaseContext.getResources().getDisplayMetrics(),
                      mBaseContext.getResources().getConfiguration());


              PluginUtil.setField(mBaseContext, "mResources", newResources);
              //這是最主要的需要替換的再膳,如果不支持插件運行時更新,只留這一個就可以了
              PluginUtil.setField(mPackageInfo, "mResources", newResources);

現(xiàn)在曲横,參考以上思路喂柒,我們已經(jīng)基本可以實現(xiàn)一個插件補丁框架不瓶,其實站在巨人的肩膀(Android 系統(tǒng)源碼)上,是不是覺得實現(xiàn)一套插件補丁框架也沒那么復雜呢灾杰?當然蚊丐,真正項目中,還有很多細節(jié)需要處理艳吠,譬如說資源分區(qū)麦备、代碼混淆等問題。但核心邏輯基本還是以上這些思路昭娩。具體實現(xiàn)可以參考 ZeusPlugin源碼

TODO

由于公司業(yè)務線凛篙、時間精力等原因, ZeusPlugin有一些特性和功能還沒實現(xiàn)栏渺,但很多也提上日程了呛梆,比如:

  • demo完善

  • gradle插件maven遠程依賴

  • 支持補丁更換資源

    ……..

GitHub

https://github.com/iReaderAndroid/ZeusPlugin

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市磕诊,隨后出現(xiàn)的幾起案子填物,更是在濱河造成了極大的恐慌,老刑警劉巖霎终,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滞磺,死亡現(xiàn)場離奇詭異,居然都是意外死亡神僵,警方通過查閱死者的電腦和手機雁刷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來保礼,“玉大人,你說我怎么就攤上這事责语∨谡希” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵坤候,是天一觀的道長胁赢。 經(jīng)常有香客問我,道長白筹,這世上最難降的妖魔是什么智末? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮徒河,結(jié)果婚禮上系馆,老公的妹妹穿的比我還像新娘。我一直安慰自己顽照,他們只是感情好由蘑,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布闽寡。 她就那樣靜靜地躺著,像睡著了一般尼酿。 火紅的嫁衣襯著肌膚如雪爷狈。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天裳擎,我揣著相機與錄音涎永,去河邊找鬼。 笑死鹿响,一個胖子當著我的面吹牛土辩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播抢野,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼拷淘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了指孤?” 一聲冷哼從身側(cè)響起启涯,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎恃轩,沒想到半個月后结洼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡叉跛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年松忍,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片筷厘。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡鸣峭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出酥艳,到底是詐尸還是另有隱情摊溶,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布充石,位于F島的核電站莫换,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏骤铃。R本人自食惡果不足惜拉岁,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望惰爬。 院中可真熱鬧喊暖,春花似錦、人聲如沸补鼻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至咨跌,卻和暖如春沪么,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背锌半。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工禽车, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人刊殉。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓殉摔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親记焊。 傳聞我的和親對象是個殘疾皇子逸月,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,304評論 25 707
  • 是時候來一波Android插件化了 是時候來一波Android插件化了前言Android開發(fā)演進模塊化介紹插件化介...
    流水不腐小夏閱讀 4,794評論 3 51
  • 最近幾年移動開發(fā)業(yè)界興起了「 插件化技術(shù) 」的旋風,各個大廠都推出了自己的插件化框架遍膜,各種開源框架都評價自身功能優(yōu)...
    斜杠時光閱讀 3,951評論 1 36
  • //1.創(chuàng)建串行或變行隊列 //2.延遲執(zhí)行和特定時間執(zhí)行 3.//多個任務都結(jié)束后再執(zhí)行其它任務碗硬,用dispat...
    wangyu2488閱讀 791評論 0 0
  • 大學到工作其中的落差感其實我們大家都有所體會,每天三點一線的生活瓢颅,每天“單曲循環(huán)”的重復恩尾。昨天跟我?guī)讉€高中同學小聚...
    無法接觸閱讀 404評論 0 0