安卓换肤实现

最近实习的时候发现公司的换肤框架挺好的,所以本周打算学习一下安卓换肤的实现(2020.7.7 06:29)

换肤基础1——inflate布局流程,View的构造

布局是承载在activity上的,不管是view 还是fragment,他们都是以activity为基础,所以先从activity的创建开始看;

用户进程通过binder通知ams开启一个新的activity,ams经过自己的activitystack处理后最终通过binder像activityThread发送一个hanlder消息,在activity的handleMessage中去真实创建activity对象

最终在ActivityThread的performLaunchActivity()创建:

 /**  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. 创建ContextImpl,装饰器模式的装饰对象,activity是被装饰的对象
  2. 通过classloader获取class对象,再通过反射创建真实的Activity对象
  3. 创建Application对象
  4. 创建一个Window对象,当前window为null
  5. 通过attach和window进行绑定
  6. 调用onCreate()
setContentView():
    public void setContentView(int layoutResID) {
...

            installDecor();

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

在activity中调用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. 创建根view temp
  2. 通过inflate的第三个参数判断是否attachToRoot:
    false: 设置root的参数,不绑定到root;
    true:不设置root的参数,绑定给root,帮我们调用root.addView(), 如果parent为null,同false;

补充一下xml的解析方式


xml三种解析方式对比
createViewFromTag(): 创建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去创建view, factory是预留给开发者自定义的一个创建view的接口
  2. 如果factory创建失败则调用Layoutinflater.onCreateView(),通过反射调用view的构造方法
onCreateView()最终调用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. 先尝试从缓存获取构造器
  2. 通过mConstructorSignature获取view的二参构造器,并放入缓存
  3. 反射创建view对象
市面上的换肤框架基本基于这两种实现方式:
  • 通过Factory接口生成View
  • 自定义LayoutInflater,改写createView()自定义创建View

换肤基础2——资源的加载

资源是在handleBindApplication()中的Application对象创建的过程中加载,关于handleBindApplication()的调用时机在前面文章讲过,此处不在赘述;
handleBindApplication()中的具体创建时机是在创建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()创建ResourcesImpl对象
  3. 放入缓存
  4. 通过ResourcesImpl获取 Resources对象,先从缓存找,没有就新建
在创建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文件,在实现换肤框架中,我们新建一个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创建的时候记录所有的View及其Attr(background,drawable,color),可以通过改写Factory或者LayoutInflater实现

  2. 所有需要换肤的资源在插件里需要存放一个同名的文件,将这个插件传给AssertManager,交给native层去load,然后可以得到一个插件对应的Resource

  3. 执行换肤,遍历第一步记录的所有属性,通过id在插件的Resource对象中寻找同名的资源,加载给对应的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,Fragment基类实现IDynamicNewView 接口统计所有加载过的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对象设置给当前Application的LayoutInflater;

当LayoutInflater有了factory对象后,会优先使用factory对象去创建View,factory对象创建View的实现可以参考LayoutInflater的具体创建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. 创建View,具体参考LayoutInflater的实现
  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创建的时候,调用SkinManager的换肤方法设置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对象对应的资源),如果插件对应的Resources对象为null或者没有开启夜间皮肤则直接使用原本的资源
  2. 通过resId获取name,在通过name,属性,皮肤包路径,传给插件Resources对象,然后Resource对象会通过AssertManager的一个native方法获取到对应的夜间资源id(我们自己设置的名称相同,值不相同);
  3. 通过这个darkResId,我们就可以将其传给darkResource获取到对应的资源文件,返回给上层即可
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343