Android Settings(设置)语言的切换和添加原理流程简析

Settings 部分

切换流程

首先是语言和输入的设置界面
src/com/android/settings/language/LanguageAndInputSettings.java

......
// 注释1_1:加载了language_and_input 这个布局文件
@Override
protected int getPreferenceScreenResId() {
   return R.xml.language_and_input;
}
......

然后看下language_and_input.xml这个布局文件

<PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:settings="http://schemas.android.com/apk/res-auto"
    android:title="@string/language_settings">
    <!-- 注释1_2: 这里的 phone_language 即对应着 界面上的语言选项 
                 下面的 keyboards_category 则对应着键盘的设置选项
                 注意:这里的 fragment 属性设置的是com.android.settings.localepicker.LocaleListEditor
                 即我们点开语言选项,即由这个页面来实现-->
    <Preference
        android:key="phone_language"
        android:title="@string/phone_language"
        android:icon="@drawable/ic_translate_24dp"
        android:fragment="com.android.settings.localepicker.LocaleListEditor" />

    <PreferenceCategory
        android:key="keyboards_category"
        android:title="@string/keyboard_and_input_methods_category">
        <Preference
            android:key="virtual_keyboard_pref"
            android:title="@string/virtual_keyboard_category"
            android:fragment="com.android.settings.inputmethod.VirtualKeyboardFragment"
            settings:keywords="@string/keywords_virtual_keyboard"/>
        <Preference
            android:key="physical_keyboard_pref"
            android:title="@string/physical_keyboard_title"
            android:summary="@string/summary_placeholder"
            android:fragment="com.android.settings.inputmethod.PhysicalKeyboardFragment"/>
    </PreferenceCategory>
    ......

另外,Settings里的界面基本都是Preference(界面xml) 和 xxxController(数据逻辑管理) ,语言的controller 是 PhoneLanguagePreferenceController ,具体这里不再详细展开。

多语言的切换和添加页面:
src/com/android/settings/localepicker/LocaleListEditor.java

......
      @Override
   public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setHasOptionsMenu(true);
   // 注释1_4: 这里是已经添加列表数据的初始化,获取和填充 这里的 LocaleStore getUserLocaleList()
   // 所涉及的 LocaleList LocalePiker 都是frameworks层的实现,后面会说到
      LocaleStore.fillCache(this.getContext());
      final List<LocaleStore.LocaleInfo> feedsList = getUserLocaleList();
      mAdapter = new LocaleDragAndDropAdapter(this.getContext(), feedsList);
   }
   
   @Override
   public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstState) {
      final View result = super.onCreateView(inflater, container, savedInstState);
      //注释1_4: 这里加载了布局 locale_order_list.xml 比较简单 一个列表 和 一个添加按钮
      final View myLayout = inflater.inflate(R.layout.locale_order_list, (ViewGroup) result);

      configureDragAndDrop(myLayout);
      return result;
   }

......
   // 注释1_5: 这里的方法,对应上面注释1_4。即已添加的语言列表数据
   private List<LocaleStore.LocaleInfo> getUserLocaleList() {
      final List<LocaleStore.LocaleInfo> result = new ArrayList<>();
      final LocaleList localeList = LocalePicker.getLocales();
      for (int i = 0; i < localeList.size(); i++) {
         Locale locale = localeList.get(i);
         result.add(LocaleStore.getLocaleInfo(locale));
      }
      return result;
   }

private void configureDragAndDrop(View view) {
      final RecyclerView list = view.findViewById(R.id.dragList);
      final LocaleLinearLayoutManager llm = new LocaleLinearLayoutManager(getContext(), mAdapter);
      llm.setAutoMeasureEnabled(true);
      list.setLayoutManager(llm);

      list.setHasFixedSize(true);
      mAdapter.setRecyclerView(list);
      list.setAdapter(mAdapter);

      mAddLanguage = view.findViewById(R.id.add_language);
      // 注释1_6: 这里是添加按钮的监听
      mAddLanguage.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {
               FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider()
                     .logSettingsTileClick(INDEX_KEY_ADD_LANGUAGE, getMetricsCategory());

               final Intent intent = new Intent(getActivity(),
                     LocalePickerWithRegionActivity.class);
               startActivityForResult(intent, REQUEST_LOCALE_PICKER);
         }
   });
   
   ......

已添加语言的由前面分析可以知道,布局locale_order_list.xml里面是由

自定义RecyclerView列表:src/com/android/settings/localepicker/LocaleRecyclerView.java

数据适配器:src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java

来组合实现的,先看下

LocaleDragAndDropAdapter.java

......
      public void doTheUpdate() {
      int count = mFeedItemList.size();
      final Locale[] newList = new Locale[count];

      for (int i = 0; i < count; i++) {
         final LocaleStore.LocaleInfo li = mFeedItemList.get(i);
         newList[i] = li.getLocale();
      }

      final LocaleList ll = new LocaleList(newList);
      // 注释1_7: 前面都是做准备工作,这里调用这个做更实质的处理
      updateLocalesWhenAnimationStops(ll);
   }
   
   private LocaleList mLocalesToSetNext = null;
   private LocaleList mLocalesSetLast = null;
   
   public void updateLocalesWhenAnimationStops(final LocaleList localeList) {
      if (localeList.equals(mLocalesToSetNext)) {
         return;
      }

      // This will only update the Settings application to make things feel more responsive,
      // the system will be updated later, when animation stopped.
      LocaleList.setDefault(localeList);

      mLocalesToSetNext = localeList;
      final RecyclerView.ItemAnimator itemAnimator = mParentView.getItemAnimator();
      itemAnimator.isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
         @Override
         public void onAnimationsFinished() {
               if (mLocalesToSetNext == null || mLocalesToSetNext.equals(mLocalesSetLast)) {
                  // All animations finished, but the locale list did not change
                  return;
               }
            // 注释1_8: 当已添加的语言数目发生了改变,则调用frameworks 层的 
            // LocalePicker的 updateLocales 方法处理,具体后面看
               LocalePicker.updateLocales(mLocalesToSetNext);
               mLocalesSetLast = mLocalesToSetNext;
               new ShortcutsUpdateTask(mContext).execute();

               mLocalesToSetNext = null;

               mNumberFormatter = NumberFormat.getNumberInstance(Locale.getDefault());
         }
      });
   }

初看可能有点疑惑,这个doTheUpdate方法 在哪里调用和触发的呢?

LocaleRecyclerView.java

@Override
public boolean onTouchEvent(MotionEvent e) {
   if (e.getAction() == MotionEvent.ACTION_UP || e.getAction() == MotionEvent.ACTION_CANCEL) {
      LocaleDragAndDropAdapter adapter = (LocaleDragAndDropAdapter) this.getAdapter();
      if (adapter != null) {
            // 注释1_9: 这里一目了然,当列表的触摸事件手指离开的时候,便会触发这个更新
            adapter.doTheUpdate();
      }
   }
   return super.onTouchEvent(e);
}

关于Settings 切换已选语言的处理流程基本就这些了,然后说下添加的流程。

添加流程

前面的介绍在 注释1_6 处,LocaleListEditor.java的添加语言的按钮,点击监听事件里是流程的入口,会跳转

src/com/android/settings/localepicker/LocalePickerWithRegionActivity.java

@Override
public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   getActionBar().setDisplayHomeAsUpEnabled(true);
// 注释1_10: 这里的页面以及逻辑实现都交给了 LocalePickerWithRegion ,
// 这个类的实现也是在frameworks 层
   final LocalePickerWithRegion selector = LocalePickerWithRegion.createLanguagePicker(
            this, LocalePickerWithRegionActivity.this, false /* translate only */);
   getFragmentManager()
            .beginTransaction()
            .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
            .replace(android.R.id.content, selector)
            .addToBackStack(PARENT_FRAGMENT_NAME)
            .commit();
}

所以,添加的流程,Settings 的添加流程到这里也就结束了

Frameworks 部分

切换流程

由前面分析可知:切换语言Settings 处理跳转到 framework 的入口是LocalePickerupdateLocales方法

base/core/java/com/android/internal/app/LocalePicker.java

......

   /**
   * Requests the system to update the list of system locales.
   * Note that the system looks halted for a while during the Locale migration,
   * so the caller need to take care of it.
   */
   @UnsupportedAppUsage
   public static void updateLocales(LocaleList locales) {
      if (locales != null) {
         locales = removeExcludedLocales(locales);
      }
      // Note: the empty list case is covered by Configuration.setLocales().

      try {
         final IActivityManager am = ActivityManager.getService();
         final Configuration config = am.getConfiguration();
      // 注释2_1:这里对切换语言后的数据封装到Configuration里,用于后面流程处理
         config.setLocales(locales);
         config.userSetLocale = true;
      // 注释2_2: 这里通过ActivityManager的一个Binder服务,调用
      // updatePersistentConfigurationWithAttribution 继续处理  
         am.updatePersistentConfigurationWithAttribution(config,
                  ActivityThread.currentOpPackageName(), null);
         // Trigger the dirty bit for the Settings Provider.
         BackupManager.dataChanged("com.android.providers.settings");
      } catch (RemoteException e) {
         // Intentionally left blank
      }
   }
   
......

这里是通过 Binder 获取 ActivityManager 的一个服务代理对象,来处理 实现方法是 updatePersistentConfigurationWithAttribution
这里ActivityManager的Binder 实际处理对象是:
base/services/core/java/com/android/server/am/ActivityManagerService.java

@Override
public void updatePersistentConfiguration(Configuration values) {
   updatePersistentConfigurationWithAttribution(values,
            Settings.getPackageNameForUid(mContext, Binder.getCallingUid()), null);
}

@Override
public void updatePersistentConfigurationWithAttribution(Configuration values,
      String callingPackage, String callingAttributionTag) {
   enforceCallingPermission(CHANGE_CONFIGURATION, "updatePersistentConfiguration()");
   enforceWriteSettingsPermission("updatePersistentConfiguration()", callingPackage,
            callingAttributionTag);
   if (values == null) {
      throw new NullPointerException("Configuration must not be null");
   }

   int userId = UserHandle.getCallingUserId();
// 注释2_3:可以看到,这里流程将处理方法又传递给了 mActivityTaskManager 实例的updatePersistentConfiguration 方法
   mActivityTaskManager.updatePersistentConfiguration(values, userId);
}

上面的mActivityTaskManager的实例即 ActivityTaskManagerService.java,来看看内部实现

base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
通过内部层层调用,以及一些判断条件的筛选,最后会执行到(方法内部处理逻辑很多,挑重点看下):

/** Update default (global) configuration and notify listeners about changes. */
int updateGlobalConfigurationLocked(@NonNull Configuration values, boolean initLocale,
      boolean persistent, int userId) {
      
   ......
   // 注释2_4:这里对切换语言后封装的Configuration做后续处理的预检查和判断
      if (!initLocale && !values.getLocales().isEmpty() && values.userSetLocale) {
      final LocaleList locales = values.getLocales();
      int bestLocaleIndex = 0;
      if (locales.size() > 1) {
            if (mSupportedSystemLocales == null) {
            // 注释2_5:这里是获取系统资源配置支持的语言,也就是说实际能真的支持的语言
               mSupportedSystemLocales = Resources.getSystem().getAssets().getLocales();
            }
            // 注释2_6:这里是通过方法计算得到在所有支持语言列表里 最匹配的语言的所在列表的 索引
            // 具体算法可getFirstMatchIndex 一路点进去看
            bestLocaleIndex = Math.max(0, locales.getFirstMatchIndex(mSupportedSystemLocales));
      }
      // 注释2_7:这里是修改系统属性值,即 当前系统的默认语言
      SystemProperties.set("persist.sys.locale",
               locales.get(bestLocaleIndex).toLanguageTag());
      LocaleList.setDefault(locales, bestLocaleIndex);
   // 注释2_8:这里是将切换语言的动作通过 handler - message 的形式分发出去,以通知系统各个地方刷新
      final Message m = PooledLambda.obtainMessage(
               ActivityTaskManagerService::sendLocaleToMountDaemonMsg, this,
               locales.get(bestLocaleIndex));
      mH.sendMessage(m);
   }
   ......      
}

切换语言的流程,简单的流程就到这里,其余就不在详细展开了

添加流程

由前面分析可知,添加流程由 Settings 的 LocalePickerWithRegionActivity.java到 frameworks 的 LocalePickerWithRegion.java
base/core/java/com/android/internal/app/LocalePickerWithRegion.java

......

   @Override
   public void onListItemClick(ListView l, View v, int position, long id) {
      final LocaleStore.LocaleInfo locale =
               (LocaleStore.LocaleInfo) getListAdapter().getItem(position);

      if (locale.getParent() != null) {
         if (mListener != null) {
               mListener.onLocaleSelected(locale);
         }
         returnToParentFrame();
      } else {
      // 注释2_9:这里是语言列表的点击事件,即代表这开启了被点击语言的添加流程
      // 这里是 用到本类的 createCountryPicker 方法 
         LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker(
                  getContext(), mListener, locale, mTranslatedOnly /* translate only */);
         if (selector != null) {
               getFragmentManager().beginTransaction()
                     .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
                     .replace(getId(), selector).addToBackStack(null)
                     .commit();
         } else {
               returnToParentFrame();
         }
      }
   }
......

// 注释2_10_1:看上一步,调用的是这个4个参数的方法
private static LocalePickerWithRegion createCountryPicker(Context context,
         LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
         boolean translatedOnly) {   
      LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
      // 注释2_10_2:重要是这一步,调用了localePicker.setListener,localePicker是LocalePickerWithRegion
      // 实例化的对象,于是将流程传递给了本类的setListener方法
      boolean shouldShowTheList = localePicker.setListener(context, listener, parent,
               translatedOnly);
      return shouldShowTheList ? localePicker : null;
   }

   public static LocalePickerWithRegion createLanguagePicker(Context context,
         LocaleSelectedListener listener, boolean translatedOnly) {
      LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
      localePicker.setListener(context, listener, /* parent */ null, translatedOnly);
      return localePicker;
   }

......

private boolean setListener(Context context, LocaleSelectedListener listener,
         LocaleStore.LocaleInfo parent, boolean translatedOnly) {
      this.mParentLocale = parent;
      this.mListener = listener;
      this.mTranslatedOnly = translatedOnly;
      setRetainInstance(true);
   // 注释2_11_1:这部分的逻辑是 获取已经添加的语言列表,作为需要忽略的部分,毕竟已经添加的,还能再添加就不合理了
      final HashSet<String> langTagsToIgnore = new HashSet<>();
      if (!translatedOnly) {
         final LocaleList userLocales = LocalePicker.getLocales();
         final String[] langTags = userLocales.toLanguageTags().split(",");
         Collections.addAll(langTagsToIgnore, langTags);
      }
   // 注释2_11_2:这里做了选择的语言不为空的判断后,就将逻辑流程 传递给了 LocaleStore.java 
   // 的getLevelLocales方法,从而获得一个新的已添加语言列表
      if (parent != null) {
         mLocaleList = LocaleStore.getLevelLocales(context,
                  langTagsToIgnore, parent, translatedOnly);
         if (mLocaleList.size() <= 1) {
               if (listener != null && (mLocaleList.size() == 1)) {
                  listener.onLocaleSelected(mLocaleList.iterator().next());
               }
               return false;
         }
      } else {
         mLocaleList = LocaleStore.getLevelLocales(context, langTagsToIgnore,
                  null /* no parent */, translatedOnly);
      }

      return true;

由于上面的添加流程,已经走完,转向了LocaleStore.java 这里,那就看看

base/core/java/com/android/internal/app/LocaleStore.java

......

   @UnsupportedAppUsage
   public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
         LocaleInfo parent, boolean translatedOnly) {
      // 注释2_12_1:这个方法很关键,后面逻辑处理保存的数据,需要这里先做预处理的
      fillCache(context);
      String parentId = parent == null ? null : parent.getId();

      HashSet<LocaleInfo> result = new HashSet<>();
      for (LocaleStore.LocaleInfo li : sLocaleCache.values()) {
         int level = getLevel(ignorables, li, translatedOnly);
         if (level == 2) {
               if (parent != null) { // region selection
                  if (parentId.equals(li.getParent().toLanguageTag())) {
                     result.add(li);
                  }
               } else { // language selection
                  if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
                     result.add(li);
                  } else {
                     result.add(getLocaleInfo(li.getParent()));
                  }
               }
         }
      }
      return result;
   }
   
   ......// fillCache方法内部逻辑不少,只看下核心部分
   
   @UnsupportedAppUsage
   public static void fillCache(Context context) {
      if (sFullyInitialized) {
         return;
      }
      ......
      // 注释2_12_2:LocalePicker.getSupportedLocales(context) 这个方法很关键 ,他是整个数据处理的来源
   for (String localeId : LocalePicker.getSupportedLocales(context)) {
         if (localeId.isEmpty()) {
               throw new IllformedLocaleException("Bad locale entry in locale_config.xml");
         }
         LocaleInfo li = new LocaleInfo(localeId);

         if (LocaleList.isPseudoLocale(li.getLocale())) {
               if (isInDeveloperMode) {
                  li.setTranslated(true);
                  li.mIsPseudo = true;
                  li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
               } else {
                  // Do not display pseudolocales unless in development mode.
                  continue;
               }
         }

         if (simCountries.contains(li.getLocale().getCountry())) {
               li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
         }
            // 注释2_12_3: 这也是2_12_1处后续逻辑处理 sLocaleCache的加载的地方
         sLocaleCache.put(li.getId(), li);
         final Locale parent = li.getParent();
         if (parent != null) {
               String parentId = parent.toLanguageTag();
               if (!sLocaleCache.containsKey(parentId)) {
                  sLocaleCache.put(parentId, new LocaleInfo(parent));
               }
         }
      }
      ......
   }

看到上一步,其实数据源来自LocalePicker.getSupportedLocales(context),那就看看
base/core/java/com/android/internal/app/LocalePicker.java

......
// 注释2_13_1:这个方法也很关键,这是获取系统资源配置支持的语言列表
public static String[] getSystemAssetLocales() {
      return Resources.getSystem().getAssets().getLocales();
   }

   public static String[] getSupportedLocales(Context context) {
   // 注释2_13:好了,找到这里,似乎真相大白了,所有的语言列表,都是从这个supported_locales 资源数组获取的
   // 资源位置:base/core/res/res/values/locale_config.xml
      String[] allLocales = context.getResources().getStringArray(R.array.supported_locales);
      Predicate<String> localeFilter = getLocaleFilter();
      if (localeFilter == null) {
         return allLocales;
      }

      List<String> result = new ArrayList<>(allLocales.length);
      for (String locale : allLocales) {
         if (localeFilter.test(locale)) {
               result.add(locale);
         }
      }
      int localeCount = result.size();
      return (localeCount == allLocales.length) ? allLocales
               : result.toArray(new String[localeCount]);
   }
   
......

好了分析到这里,加载的流程也基本结束了。

最后总结

  • R.array.supported_locales 获取到的语言列表 是config 里的xml 配置,如果要对系统做语言支持上的变动,可以改这里
  • Resources.getSystem().getAssets().getLocales()才是系统真正可以支持的语言列表,ActivityTaskManagerService.java的updateGlobalConfigurationLocked 方法里判断选择的语言是不是可用,也是用的这个资源作为判断依据
  • R.array.supported_locales里的语言配置,可能Resources.getSystem().getAssets().getLocales()不能全部支持,这就会导致我们添加了某个语言,切换到它,但是却没效果,所以有个更好的优化方案这边分享一个:

修改 base/core/java/com/android/internal/app/LocalePicker.java 的 getSupportedLocales 方法

public static String[] getSupportedLocales(Context context) {
   String[] allLocales = context.getResources().getStringArray(R.array.supported_locales);
   // 设置切换语言不支持的问题__配置可支持的语言筛掉没有系统资源配置的
   /* 
   Predicate<String> localeFilter = getLocaleFilter();
   if (localeFilter == null) {
      return allLocales;
   }

   List<String> result = new ArrayList<>(allLocales.length);
   for (String locale : allLocales) {
      if (localeFilter.test(locale)) {
            result.add(locale);
      }
   }
   */
   Predicate<String> localeFilter = getLocaleFilter();
   List<String> result = new ArrayList<>(allLocales.length);
   String[] sysAssetLocales = getSystemAssetLocales();
   for(String locale : allLocales){
      if(!isCongenericLocales(sysAssetLocales,locale)){
            // 本地资源配没有有配置直接跳过继续检查下一个
            continue;
      }
      if(localeFilter == null){
            result.add(locale);
      }else{
            if (localeFilter.test(locale)) {
               result.add(locale);
            }
      }
   }

   int localeCount = result.size();
   return (localeCount == allLocales.length) ? allLocales
            : result.toArray(new String[localeCount]);
}

// 设置切换语言不支持的问题__筛查系统没有配置资源的语言
private static boolean isCongenericLocales(String[] sysAssetLocales, String xmlLocales){
   boolean result = false;
   try{
      if(xmlLocales !=null){
            String[] xmlPartHead = xmlLocales.split("-",2);
            for(String assetLocales : sysAssetLocales){
               String[] sysPartHead = assetLocales.split("-",2);
               if(xmlPartHead[0].equals(sysPartHead[0])){
                  result = true;
                  break;
               }
            }
      }
   }catch(Exception e){
      Log.e(TAG, "Failed to deal sysAssetLocales and xmlLocales compare!", e);
   }
   return result;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,639评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,093评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,079评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,329评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,343评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,047评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,645评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,565评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,201评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,338评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,014评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,701评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,194评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,685评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,345评论 2 358

推荐阅读更多精彩内容