安卓換膚實現(xiàn)

最近實習的時候發(fā)現(xiàn)公司的換膚框架挺好的咒唆,所以本周打算學習一下安卓換膚的實現(xiàn)(2020.7.7 06:29)

換膚基礎(chǔ)1——inflate布局流程甚垦,View的構(gòu)造

布局是承載在activity上的茶鹃,不管是view 還是fragment,他們都是以activity為基礎(chǔ)艰亮,所以先從activity的創(chuàng)建開始看闭翩;

用戶進程通過binder通知ams開啟一個新的activity,ams經(jīng)過自己的activitystack處理后最終通過binder像activityThread發(fā)送一個hanlder消息迄埃,在activity的handleMessage中去真實創(chuàng)建activity對象

最終在ActivityThread的performLaunchActivity()創(chuàng)建:

 /**  Core implementation of activity launch. */
    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ActivityInfo aInfo = r.activityInfo;
         //  分析1
         ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            //  分析2
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
        }
        try {
            //  分析3
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);
                 Window window = null;
                if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
                    //  分析4
                    window = r.mPendingRemoveWindow;
                }
                appContext.setOuterContext(activity);
                //  分析5
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);
                activity.mStartedActivity = false;
                int theme = r.activityInfo.getThemeResource();
                if (theme != 0) {
                    activity.setTheme(theme);
                }

                activity.mCalled = false;
                if (r.isPersistable()) {
                    //  分析6
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }

                r.activity = activity;
            }
            r.setState(ON_CREATE);
        }
        return activity;
    }
  1. 創(chuàng)建ContextImpl疗韵,裝飾器模式的裝飾對象,activity是被裝飾的對象
  2. 通過classloader獲取class對象侄非,再通過反射創(chuàng)建真實的Activity對象
  3. 創(chuàng)建Application對象
  4. 創(chuàng)建一個Window對象蕉汪,當前window為null
  5. 通過attach和window進行綁定
  6. 調(diào)用onCreate()
setContentView():
    public void setContentView(int layoutResID) {
...

            installDecor();

            mLayoutInflater.inflate(layoutResID, mContentParent);
...
    }

在activity中調(diào)用setContentView()會做兩件事情:

  1. 初始化DecorView
  2. 通過inflater加載布局文件
inflate():
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
...
                    //  分析1
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        //  分析2
                        if (!attachToRoot) {
                            temp.setLayoutParams(params);
                        }
                    }
                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

                    //  分析2
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
            } 
            return result;
        }
    }

  1. 創(chuàng)建根view temp
  2. 通過inflate的第三個參數(shù)判斷是否attachToRoot:
    false: 設(shè)置root的參數(shù),不綁定到root逞怨;
    true:不設(shè)置root的參數(shù)者疤,綁定給root,幫我們調(diào)用root.addView(), 如果parent為null骇钦,同false;

補充一下xml的解析方式


xml三種解析方式對比
createViewFromTag(): 創(chuàng)建view
    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
...
        try {
            //  分析1
            View view;
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }

            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    //  分析2
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } 
            }
            return view;
        }  
    }
  1. 首先會通過mFactory2 , mFactory去創(chuàng)建view, factory是預(yù)留給開發(fā)者自定義的一個創(chuàng)建view的接口
  2. 如果factory創(chuàng)建失敗則調(diào)用Layoutinflater.onCreateView()宛渐,通過反射調(diào)用view的構(gòu)造方法
onCreateView()最終調(diào)用createView()
    //  成員變量
    static final Class<?>[] mConstructorSignature = new Class[] {
            Context.class, AttributeSet.class};


    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        
        //  分析1
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        Class<? extends View> clazz = null;

        try {
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                //  分析2
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } 

            Object lastContext = mConstructorArgs[0];
            if (mConstructorArgs[0] == null) {
                // Fill in the context if not already within inflation.
                mConstructorArgs[0] = mContext;
            }
            Object[] args = mConstructorArgs;
            args[1] = attrs;

            //  分析3
            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
                // Use the same context when inflating ViewStub later.
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            mConstructorArgs[0] = lastContext;
            return view;

        }  
    }
  1. 先嘗試從緩存獲取構(gòu)造器
  2. 通過mConstructorSignature獲取view的二參構(gòu)造器竞漾,并放入緩存
  3. 反射創(chuàng)建view對象
市面上的換膚框架基本基于這兩種實現(xiàn)方式:
  • 通過Factory接口生成View
  • 自定義LayoutInflater眯搭,改寫createView()自定義創(chuàng)建View

換膚基礎(chǔ)2——資源的加載

資源是在handleBindApplication()中的Application對象創(chuàng)建的過程中加載,關(guān)于handleBindApplication()的調(diào)用時機在前面文章講過,此處不在贅述业岁;
handleBindApplication()中的具體創(chuàng)建時機是在創(chuàng)建ApplicationContext的時候:

    static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
                null);
        context.setResources(packageInfo.getResources());
        return context;
    }
LoadedApk.getResources()
    public Resources getResources() {
        if (mResources == null) {
            final String[] splitPaths;
            try {
                splitPaths = getSplitPaths(null);
            } catch (NameNotFoundException e) {
                // This should never fail.
                throw new AssertionError("null split not found");
            }

            mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                    splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                    Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
                    getClassLoader());
        }
        return mResources;
    }
ResourcesManager.getInstance().getResources():
    /**
     * A list of Resource references that can be reused.
     */
    private final ArrayList<WeakReference<Resources>> mResourceReferences = new ArrayList<>();


    public @Nullable Resources getResources() {
        try {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
            final ResourcesKey key = new ResourcesKey(
                    resDir,
                    splitResDirs,
                    overlayDirs,
                    libDirs,
                    displayId,
                    overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                    compatInfo);
            classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
            return getOrCreateResources(activityToken, key, classLoader);
        } 
    }


    private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        synchronized (this) {
                //  分析1
                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                            resourcesImpl, key.mCompatInfo);
                }
            //  分析2
            // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
            ResourcesImpl resourcesImpl = createResourcesImpl(key);
            if (resourcesImpl == null) {
                return null;
            }
            //  分析3
            // Add this ResourcesImpl to the cache.
            mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
          
            //  分析4
            resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);

            return resources;
        }
    }

    //  分析4詳細代碼
    private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
            @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) {
        // Find an existing Resources that has this ResourcesImpl set.
        final int refCount = mResourceReferences.size();
        for (int i = 0; i < refCount; i++) {
            WeakReference<Resources> weakResourceRef = mResourceReferences.get(i);
            Resources resources = weakResourceRef.get();
            //  如果緩存中的classloader和resourceImpl一樣就使用緩存
            if (resources != null &&
                    Objects.equals(resources.getClassLoader(), classLoader) &&
                    resources.getImpl() == impl) {
                return resources;
            }
        }

        // Create a new Resources reference and use the existing ResourcesImpl object.
        Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                : new Resources(classLoader);
        resources.setImpl(impl);   
        mResourceReferences.add(new WeakReference<>(resources));
        return resources;
    }
  1. 首先嘗試從緩存中找ResourcesImpl
  2. 如果緩存沒有鳞仙,createResourcesImpl()創(chuàng)建ResourcesImpl對象
  3. 放入緩存
  4. 通過ResourcesImpl獲取 Resources對象,先從緩存找笔时,沒有就新建
在創(chuàng)建ResourcesImpl的時候會借助一個 AssetManager
    private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
...
        final AssetManager assets = createAssetManager(key);
        if (assets == null) {
            return null;
        }
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
        return impl;
    }


    protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        final AssetManager.Builder builder = new AssetManager.Builder();
...
         builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,false /*overlay*/));
...
        return builder.build();
    }


    private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay)
            throws IOException {
        final ApkKey newKey = new ApkKey(path, sharedLib, overlay);
        //  分析1
        ApkAssets apkAssets = mLoadedApkAssets.get(newKey);
        if (apkAssets != null) {
            return apkAssets;
        }

        //  分析2
        // We must load this from disk.
        if (overlay) {
            apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path),
                    false /*system*/);
        } else {
            apkAssets = ApkAssets.loadFromPath(path, false /*system*/, sharedLib);
        }
        mLoadedApkAssets.put(newKey, apkAssets);
        mCachedApkAssets.put(newKey, new WeakReference<>(apkAssets));
        return apkAssets;
    }
  1. 緩存中找mLoadedApkAssets
  2. 從磁盤讀取apkAssets
ApkAssets.loadOverlayFromPath():
    public static @NonNull ApkAssets loadOverlayFromPath(@NonNull String idmapPath, boolean system)
            throws IOException {
        return new ApkAssets(idmapPath, system, false /*forceSharedLibrary*/, true /*overlay*/);
    }

    private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
            throws IOException {
        Preconditions.checkNotNull(path, "path");
        mNativePtr = nativeLoad(path, system, forceSharedLib, overlay); 
        mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
    }

看到native就全部都走通了棍好,將path傳給native,由C/C++去加載磁盤,加載完以后會將C的指針傳給java借笙;native加載的東西就是apk中的resources.arsc文件扒怖,在實現(xiàn)換膚框架中,我們新建一個module业稼,在里面放入夜間的資源盗痒,在通過某些手段將傳給native的path替換成新建的這個夜間module;

AssertManager的核心native方法():
    // Resource name/ID native methods.
    private static native @AnyRes int nativeGetResourceIdentifier(long ptr, @NonNull String name,
            @Nullable String defType, @Nullable String defPackage);
    private static native @Nullable String nativeGetResourceName(long ptr, @AnyRes int resid);
    private static native @Nullable String nativeGetResourcePackageName(long ptr,
            @AnyRes int resid);
    private static native @Nullable String nativeGetResourceTypeName(long ptr, @AnyRes int resid);
    private static native @Nullable String nativeGetResourceEntryName(long ptr, @AnyRes int resid);

arsc文件

換膚整體思路

  1. 在View創(chuàng)建的時候記錄所有的View及其Attr(background低散,drawable俯邓,color),可以通過改寫Factory或者LayoutInflater實現(xiàn)

  2. 所有需要換膚的資源在插件里需要存放一個同名的文件,將這個插件傳給AssertManager熔号,交給native層去load稽鞭,然后可以得到一個插件對應(yīng)的Resource

  3. 執(zhí)行換膚,遍歷第一步記錄的所有屬性引镊,通過id在插件的Resource對象中尋找同名的資源朦蕴,加載給對應(yīng)的View

下面分析一下GitHub上的一個換膚框架源碼的核心類:Android-Skin-Loader

1. SkinManager的初始化
public class SkinManager implements ISkinLoader{
    private static SkinManager instance; // 單例
    private String skinPackageName;  // 插件的包名
    private Resources mResources;  //  插件的Resources對象
    private String skinPath;  //  插件的目錄
    private boolean isDefaultSkin = false;   //  是否使用皮膚
}

    public void load(){
        String skin = SkinConfig.getCustomSkinPath(context);
        load(skin, null);
    }

    /**
     * Load resources from apk in asyc task
     * @param skinPackagePath path of skin apk
     * @param callback callback to notify user
     */
    public void load(String skinPackagePath, final ILoaderListener callback) {
                        
                        PackageManager mPm = context.getPackageManager();
                        PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
                        skinPackageName = mInfo.packageName;

                        AssetManager assetManager = AssetManager.class.newInstance();
                        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
                        addAssetPath.invoke(assetManager, skinPkgPath);

                        Resources superRes = context.getResources();
                        Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
                        
                        SkinConfig.saveSkinPath(context, skinPkgPath);
                        
                        skinPath = skinPkgPath;
                        isDefaultSkin = false;
                        mResources = skinResource;
    }

2. Activity,F(xiàn)ragment基類實現(xiàn)IDynamicNewView 接口統(tǒng)計所有加載過的View
public class BaseActivity extends Activity implements ISkinUpdate, IDynamicNewView{
         //  分析1
    private SkinInflaterFactory mSkinInflaterFactory;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mSkinInflaterFactory = new SkinInflaterFactory();
                //  分析2
        getLayoutInflater().setFactory(mSkinInflaterFactory);
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        SkinManager.getInstance().attach(this);
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        SkinManager.getInstance().detach(this);
        mSkinInflaterFactory.clean();
    }
    
    protected void dynamicAddSkinEnableView(View view, String attrName, int attrValueResId){    
        mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, attrName, attrValueResId);
    }
}

  1. 自定義一個Factory對象 Factory的作用上文有詳細說到
  2. 將自定義的Factory對象設(shè)置給當前Application的LayoutInflater;

當LayoutInflater有了factory對象后弟头,會優(yōu)先使用factory對象去創(chuàng)建View梦重,factory對象創(chuàng)建View的實現(xiàn)可以參考LayoutInflater的具體創(chuàng)建View的過程;

public class SkinInflaterFactory implements Factory {
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        //  分析1
        boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
        if (!isSkinEnable){
                return null;
        }
        //分析2
        View view = createView(context, name, attrs);
        if (view == null){
            return null;
        }
        //分析3
        parseSkinAttr(context, attrs, view);
        return view;
    }
    //分析2
    private View createView(Context context, String name, AttributeSet attrs) {
        View view = null;
        try {
            if (-1 == name.indexOf('.')){
                if ("View".equals(name)) {
                    view = LayoutInflater.from(context).createView(name, "android.view.", attrs);
                } 
                if (view == null) {
                    view = LayoutInflater.from(context).createView(name, "android.widget.", attrs);
                } 
                if (view == null) {
                    view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs);
                } 
            }else {
                view = LayoutInflater.from(context).createView(name, null, attrs);
            }

        } 
        return view;
    }
}


  1. 首先會從SF中獲取是否需要使用皮膚包
  2. 創(chuàng)建View亮瓷,具體參考LayoutInflater的實現(xiàn)
  3. 將加載出來的View使用插件的資源
    private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
        List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
        for (int i = 0; i < attrs.getAttributeCount(); i++){
            String attrName = attrs.getAttributeName(i);
            String attrValue = attrs.getAttributeValue(i);
            if(attrValue.startsWith("@")){
                try {
                    int id = Integer.parseInt(attrValue.substring(1));
                    String entryName = context.getResources().getResourceEntryName(id);
                    String typeName = context.getResources().getResourceTypeName(id);
                  //  分析1
                    SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
                    if (mSkinAttr != null) {
                        viewAttrs.add(mSkinAttr);
                    }
                } 
            }
        }
        
        if(!ListUtils.isEmpty(viewAttrs)){
            SkinItem skinItem = new SkinItem();
            skinItem.view = view;
            skinItem.attrs = viewAttrs;
            mSkinItems.add(skinItem);
            if(SkinManager.getInstance().isExternalSkin()){
               //  分析2
                skinItem.apply();
            }
        }
    }


    @Override
    public void apply(View view) {      
        if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){
            view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId));
        }else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){
            Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId);
            view.setBackgroundDrawable(bg);
        }
    }
  1. 找出所有的資源封裝成SkinAttr對象,包括屬性的名稱(background)琴拧、屬性的id值(int類型),屬性的id值(@+id嘱支,string類型)蚓胸,屬性的值類型(color),全部保存到集合中
  2. 在View創(chuàng)建的時候,調(diào)用SkinManager的換膚方法設(shè)置bg/color
SkinManager.geyDrawable()
    @SuppressLint("NewApi")
    public Drawable getDrawable(int resId){
                //  分析1
        Drawable originDrawable = context.getResources().getDrawable(resId);
        if(mResources == null || isDefaultSkin){
            return originDrawable;
        }
                //  分析2
        String resName = context.getResources().getResourceEntryName(resId);
        int trueResId = mResources.getIdentifier(resName, "drawable", skinPackageName);
        
        Drawable trueDrawable = null;
        try{
                        //  分析3
            if(android.os.Build.VERSION.SDK_INT < 22){
                trueDrawable = mResources.getDrawable(trueResId);
            }else{
                trueDrawable = mResources.getDrawable(trueResId, null);
            }
        }catch(NotFoundException e){
            e.printStackTrace();
            trueDrawable = originDrawable;
        }
        return trueDrawable;
    }
  1. 首先獲取主項目的資源(即原本的Resources對象對應(yīng)的資源)除师,如果插件對應(yīng)的Resources對象為null或者沒有開啟夜間皮膚則直接使用原本的資源
  2. 通過resId獲取name沛膳,在通過name,屬性汛聚,皮膚包路徑锹安,傳給插件Resources對象,然后Resource對象會通過AssertManager的一個native方法獲取到對應(yīng)的夜間資源id(我們自己設(shè)置的名稱相同倚舀,值不相同)叹哭;
  3. 通過這個darkResId,我們就可以將其傳給darkResource獲取到對應(yīng)的資源文件,返回給上層即可
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末痕貌,一起剝皮案震驚了整個濱河市风罩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌舵稠,老刑警劉巖超升,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件入宦,死亡現(xiàn)場離奇詭異,居然都是意外死亡室琢,警方通過查閱死者的電腦和手機乾闰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盈滴,“玉大人汹忠,你說我怎么就攤上這事”荆” “怎么了宽菜?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長竿报。 經(jīng)常有香客問我铅乡,道長,這世上最難降的妖魔是什么烈菌? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任阵幸,我火速辦了婚禮,結(jié)果婚禮上芽世,老公的妹妹穿的比我還像新娘挚赊。我一直安慰自己,他們只是感情好济瓢,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布荠割。 她就那樣靜靜地躺著,像睡著了一般旺矾。 火紅的嫁衣襯著肌膚如雪蔑鹦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天箕宙,我揣著相機與錄音嚎朽,去河邊找鬼。 笑死柬帕,一個胖子當著我的面吹牛哟忍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播陷寝,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼锅很,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了盼铁?” 一聲冷哼從身側(cè)響起粗蔚,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤尝偎,失蹤者是張志新(化名)和其女友劉穎饶火,沒想到半個月后鹏控,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡肤寝,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年当辐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鲤看。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡缘揪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出义桂,到底是詐尸還是另有隱情找筝,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布慷吊,位于F島的核電站袖裕,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏溉瓶。R本人自食惡果不足惜急鳄,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望堰酿。 院中可真熱鬧疾宏,春花似錦、人聲如沸触创。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽哼绑。三九已至顺饮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間凌那,已是汗流浹背兼雄。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留帽蝶,地道東北人赦肋。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像励稳,于是被迫代替她去往敵國和親佃乘。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354