ContentProvider启动流程分析(三)

0x01 扯东扯西的前言&概述


本片博客对应时序图上的step15—26上接第二篇博客:ContentProvider启动流程分析二!

同时此系列博客同步在博客园发布:ContentProvider启动流程分析系列!详情戳这里即可访问~

0x02 ContentProvider启动流程分析


step15: ApplicationThread#bindApplication()

上一步,在ApplicationThreadProxy类的bindApplication()函数中,通过Binder对象mRemote发出了一个进程间通信请求!

反查源码很容易知道,bindApplication()函数,其实是IApplicationThread接口类的接口函数,而ActivityThread类的内部类ApplicationThread实现了这一接口,所以自然也就实现了接口函数bindApplication(),我们来看ApplicationThread类的函数bindApplication()源码删减如下:

public final void bindApplication(String processName, ApplicationInfo appInfo,
            List<ProviderInfo> providers, ComponentName instrumentationName,
            ProfilerInfo profilerInfo, Bundle instrumentationArgs,
            IInstrumentationWatcher instrumentationWatcher,
            IUiAutomationConnection instrumentationUiConnection, int debugMode,
            boolean enableOpenGlTrace, boolean isRestrictedBackupMode, boolean persistent,
            Configuration config, CompatibilityInfo compatInfo, Map<String, IBinder> services,
            Bundle coreSettings) {
    AppBindData data = new AppBindData();
    data.processName = processName;
    data.appInfo = appInfo;
    data.providers = providers;
    data.instrumentationName = instrumentationName;
    data.instrumentationArgs = instrumentationArgs;
    data.instrumentationWatcher = instrumentationWatcher;
    data.instrumentationUiAutomationConnection = instrumentationUiConnection;
    data.debugMode = debugMode;
    data.enableOpenGlTrace = enableOpenGlTrace;
    data.restrictedBackupMode = isRestrictedBackupMode;
    data.persistent = persistent;
    data.config = config;
    data.compatInfo = compatInfo;
    data.initProfilerInfo = profilerInfo;
    sendMessage(H.BIND_APPLICATION, data);
}
  • 没错!ApplicationThread类的成员函数bindApplication(),就是类型为BIND_APPLICATION_TRANSACTION的进程间通信请求的真正处理者!处理的逻辑是:先把将要启动的ContentProvider组件列表,封装成一个AppBindData对象;
  • 然后再调用外部类ActivityThread的成员函数sendMessage(),再次将这个AppBindData对象封装成一个类型为BIND_APPLICATION的消息,以便可以发送到新建应用程序进程的主线程的消息队列中!

step16: ActivityThread#sendMessage()

ActivityThread类持有一个mH成员变量,mH实际上是一个Handler对象,通过mH.sendMessage()发送消息到新建应用程序进程的主线程的消息队列中!所以,这个消息最后是最终是在mH.handleMessage()函数中处理的。

step17: ActivityThread#handleMessage()

ActivityThread内部类H的程艳方法handleMessage()源码删减如下:

public void handleMessage(Message msg) {
    if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
    switch (msg.what) {
        ......
    case BIND_APPLICATION:
        AppBindData data = (AppBindData)msg.obj;
        handleBindApplication(data);
        break;
        ......
    }
    if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
}
  • 在handleMessage()函数中,我们只考虑BIND_APPLICATION的情况,首先将Message对象msg的成员变量obj强转成个AppBindData对象,它包含了要启动的ContentProvider列表。
  • 然后,调用handleBindApplication()函数把这些ContentProvider组件启动起来!

接下来,关注handleBindApplication()函数,是怎样把这些ContentProvider组件启动起来的?

step18: ActivityThread#handleBindApplication()

ActivityThread类的成员函数handleBindApplication()源码如下:

private void handleBindApplication(AppBindData data) {
    ......
    // don't bring up providers in restricted mode; they may depend on the
    // app's custom Application class
    if (!data.restrictedBackupMode) {
        List<ProviderInfo> providers = data.providers;
        if (providers != null) {
            installContentProviders(app, providers);
            ......
        }
    }
}

参数data是一个AppBindData对象,data的成员变量providers保存了,需要在当前进程中启动的ContentProvider组件,接下来则会调用ActivityThread类的成员函数installContentProviders()来启动这些组件!

step19: ActivityThread#installContentProviders()

ActivityThread类的成员函数installContentProviders()源码如下:

private void installContentProviders(
    Context context, List<ProviderInfo> providers) {
    final ArrayList<IActivityManager.ContentProviderHolder> results =
        new ArrayList<IActivityManager.ContentProviderHolder>();

    for (ProviderInfo cpi : providers) {
        IActivityManager.ContentProviderHolder cph = installProvider(context, null, cpi, false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
        if (cph != null) {
            cph.noReleaseNeeded = true;
            results.add(cph);
        }
    }

    try {
        ActivityManagerNative.getDefault().publishContentProviders(
            getApplicationThread(), results);
    } catch (RemoteException ex) {
    }
}
  • for-each循环得到保存在providers中的每一个组件ProviderInfo对象cpi,然后调用installProvider()函数启动每个cpi对应的ContnetProvider组件;
  • 这些组件启动之后,就可以获得其对应的一个IContentProvider接口;然后,把这个接口封装成一个ContentProviderHolder对象;
  • 最后调用AMS代理对象(也即,ActivityManagerProxy)的成员函数publishContentProviders(),将这些ContentProviderHolder对象传递给AMS;AMS正是通过这些ContentProviderHolder对象获取它所描述的ContentProvider组件的一个访问接口;

接下来,先分析ActivityThread类成员函数installProvider()在当前应用程序进程中启动一个ContentProvider组件的过程;然后,分析AMS代理对象ActivityManagerProxy成员函数publishContentProviders()将启动完成的组件发布到AMS的过程!

step20: ActivityThread#installProvider()

ActivityThread类成员函数installProvider()源码如下:

/**
 * Installs the provider.
 *
 * Providers that are local to the process or that come from the system server
 * may be installed permanently which is indicated by setting noReleaseNeeded to true.
 * Other remote providers are reference counted.  The initial reference count
 * for all reference counted providers is one.  Providers that are not reference
 * counted do not have a reference count (at all).
 *
 * This method detects when a provider has already been installed.  When this happens,
 * it increments the reference count of the existing provider (if appropriate)
 * and returns the existing provider.  This can happen due to concurrent
 * attempts to acquire the same provider.
 */
private IActivityManager.ContentProviderHolder installProvider(Context context,
        IActivityManager.ContentProviderHolder holder, ProviderInfo info,
        boolean noisy, boolean noReleaseNeeded, boolean stable) {
    ContentProvider localProvider = null;
    IContentProvider provider;
    // holder为空,表示参数info所描述的ContentProvider组件需要在当前进程中启动
    if (holder == null || holder.provider == null) {
        Context c = null;
        ApplicationInfo ai = info.applicationInfo;
        if (context.getPackageName().equals(ai.packageName)) {
            c = context;
        }
        ......
        final java.lang.ClassLoader cl = c.getClassLoader();
        localProvider = (ContentProvider)cl.
                        loadClass(info.name).newInstance();
        provider = localProvider.getIContentProvider();
        if (provider == null) {
            return null;
        }
        // XXX Need to create the correct context for this provider.
        localProvider.attachInfo(c, info);
    } else {
        provider = holder.provider;
    }

    IActivityManager.ContentProviderHolder retHolder;
    synchronized (mProviderMap) {
        IBinder jBinder = provider.asBinder();
        if (localProvider != null) {
            ComponentName cname = new ComponentName(info.packageName, info.name);
            ProviderClientRecord pr = mLocalProvidersByName.get(cname);
            if (pr != null) {
                provider = pr.mProvider;
            } else {
                holder = new IActivityManager.ContentProviderHolder(info);
                holder.provider = provider;
                holder.noReleaseNeeded = true;
                pr = installProviderAuthoritiesLocked(provider, localProvider, holder);
                mLocalProviders.put(jBinder, pr);
                mLocalProvidersByName.put(cname, pr);
            }
            retHolder = pr.mHolder;
        } else {
            ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
            if (prc != null) {
                ......
            } else {
                ProviderClientRecord client = installProviderAuthoritiesLocked(
                                                  provider, localProvider, holder);
                if (noReleaseNeeded) {
                    prc = new ProviderRefCount(holder, client, 1000, 1000);
                } else {
                    prc = stable ? new ProviderRefCount(holder, client, 1, 0)
                          : new ProviderRefCount(holder, client, 0, 1);
                }
                mProviderRefCountMap.put(jBinder, prc);
            }
            retHolder = prc.holder;
        }
    }
    return retHolder;
}
  • 对传进来的参数holder判空,如果为null,表示要在当前应用程序进程中,将参数info所描述的ContentProvider组件启动起来;另一个参数context描述了将要启动的ContentProvider组件运行的上下文环境;
  • localProvider表示一个ContentProvider组件对象,通过localProvider.getIContentProvider(),也即调用localProvider的成员函数getIContentProvider()来获得该组件对应的一个IContentProvider接口provider;这个IContentProvider最终需要发布到AMS,AMS会将它返回给那些需要访问它所描述的ContentProvider组件的调用方应用程序进程;
  • 接下来通过localProvider.attachInfo()来进一步初始化前面所创建的ContentProvider组件;
  • 接下来对全局变量mProviderMap加同步锁,然后对localProvider判空,正常情况下此时的localProvider应该是非空的,所以,自然要把它保存到mLocalProviders和mLocalProvidersByName中;
  • 但是如果localProvider仍然为空,接下来会出现一个ProviderRefCount对象prc,用来表示和记录ContentProvider组件的引用计数,如果prc为空,则把这个localProvider封装成一个ProviderClientRecord对象,并保存在prc变量中;
  • 最后,返回这个ContentProviderHolder对象!

接下来,继续分析ContentProvider组件的成员函数getIContentProvider()和成员函数attachInfo()的实现细节!

step21: ContentProvider#getIContnetProvider()

ContentProvider类成员函数getIContentProvider()源码如下:

public abstract class ContentProvider implements ComponentCallbacks2 {
    ......
    private Transport mTransport = new Transport();
    ......
    class Transport extends ContentProviderNative {......}
    ......
    public IContentProvider getIContentProvider() {
        return mTransport;
    }
    ......
}
  • 每一个ContentProvider组件都有一个内部类Transport,其本质是一个Binder本地对象;并且持有一个类型为Transport的全局变量mTransPort来表示一个Binder本地对象。
  • 通过将这个Binder本地对象传递给AMS,然后AMS会将引用了这个Binder本地对象的一个Binder代理对象返回给需要访问该ContentProvider组件的其他应用程序进程;这样,其他应用程序进程就可以通过这个Binder代理对象来间接的访问一个ContentProvider组件中的数据了!
  • ContentProvider类成员函数getIContentProvider()的实现很简单,只是简单地将全局变量mTransPort描述的一个IContentProvider接口返回给调用者。

step22: ContentProvider#attachInfo()

ContentProvider类的成员函数attachInfo(),作用是初始化前面所创建的一个ContentProvider组件!attachInfo()源码如下:

public abstract class ContentProvider implements ComponentCallbacks2 {
    ......
    /**
     * After being instantiated, this is called to tell the content provider
     * about itself.
     *
     * @param context The context this provider is running in
     * @param info Registered information about this content provider
     */
    public void attachInfo(Context context, ProviderInfo info) {
        attachInfo(context, info, false);
    }

    private void attachInfo(Context context, ProviderInfo info, boolean testing) {
        ......
        /*
         * Only allow it to be set once, so after the content service gives
         * this to us clients can't change it.
         */
        if (mContext == null) {
            mContext = context;
            if (context != null) {
                mTransport.mAppOpsManager = (AppOpsManager) context.getSystemService(
                        Context.APP_OPS_SERVICE);
            }
            mMyUid = Process.myUid();
            if (info != null) {
                setReadPermission(info.readPermission);
                setWritePermission(info.writePermission);
                setPathPermissions(info.pathPermissions);
                ......
            }
            ContentProvider.this.onCreate();
        }
    }
    ......
}
  • 依然是两个重载函数,接收两个参数的attachInfo()函数内部调用了接收三个参数的attachInfo()函数,我们直接关注接收三个参数的attachInfo()函数;
  • setReadPermission(info.readPermission),setWritePermission(info.writePermission)和setPathPermissions(info.pathPermissions)这三个函数的作用是,设置ContentProvider组件的读写权限和访问权限;
  • 最后ContentProvider.this.onCreate()函数,实际调用的是COntentProvider子类的onCreate()函数,以便在子类的onCreate()函数中,执行一些业务相关的初始化操作!
  • 在我们最开始的场景中,ContentProvider组件指的就是BxxApp应用程序进程中SubContentProvider组件,所以上面调用的时加上就是SubContentProvider类的onCreate()函数,而我们的业务逻辑相关的一些初始化工作,也正是放在SubContentProvider类的onCreate()函数中执行的!

step23: SubContentProvider#onCreate()

public class SubContentProvider extends ContentProvider {
    ......
    @Override
    public boolean onCreate() {
        ContentResolver resolver = getContext().getContentResolver();
        DatabaseHelper helper = new DtatabaseHelper(......);
        ......
        return true;
    }
    ......
}
  • 需要注意,由于一个ContentProvider组件再启动过程中需要执行onCreate()函数,因此,我们应该避免在onCeate()方法中执行耗时操作,例如和IO相关的操作,否则可能造成这个ContentProvider组件启动超时!

这一步执行完成后,就会返回到前面的step20,然后再次返回到前面的step19,即ActivityThread类成员函数installContentProviders()中,接下来就会调用AMS代理对象的成员函数publishContentProviders(),也即ActivityManagerProxy的成员函数publishContentProviders(),将前面所有启动的ContentProvider组件的一个IContentProvider访问接口发布到ActivityManagerService中~

step24: ActivityManagerProxy#publishContentProviders()

ActivityManagerProxy类成员函数publishContentProviders(),会将所有启动的ContentProvider组件的一个IContentProvider访问接口发布到ActivityManagerService中,源码如下:

public void publishContentProviders(IApplicationThread caller,
        List<ContentProviderHolder> providers) throws RemoteException {
    Parcel data = Parcel.obtain();
    Parcel reply = Parcel.obtain();
    data.writeInterfaceToken(IActivityManager.descriptor);
    data.writeStrongBinder(caller != null ? caller.asBinder() : null);
    data.writeTypedList(providers);
    mRemote.transact(PUBLISH_CONTENT_PROVIDERS_TRANSACTION, data, reply, 0);
    reply.readException();
    data.recycle();
    reply.recycle();
}
  • 先把接收的参数保存到Parcel对象data中;
  • 接着再通过ActivityManagerProxy类内部的一个Binder代理对象mRemote向ActivityMnagerService发送一个类型为PUBLISH_CONTENT_PROVIDERS_TRANSACTION进程间通信请求;

step25: ActivityManagerService#publishContentProviders()

以上10步都是在新建的应用程序进程中执行的!step25是在ActivityManagerService中执行的,AMS类成员函数publishContentProviders(),用来处理类型为PUBLISH_CONTENT_PROVIDERS_TRANSACTION进程间通信请求,其源码如下:

AMS#publishContentProviders()
  • 参数caller是一个类型为ApplicationThread的Binder代理对象,它引用了运行在新建应用程序进程中的一个ApplicationThread对象,getRecordForAppLocked(caller)方法通过caller来获取一个用来描述新建应用程序进程的ProcessRecord对象r;
  • 新建应用程序进程在启动时,会将需要在它里面运行的ContentProvider组件启动起来!从前面的step13可知,在AMS中,这些ContentProvider组件使用一个ContentProviderRecord对象来描述,它们保存在用来描述新建应用程序进程的一个ProcessRecord对象r的一个成员变量pubProviders中;
  • 第10到14行,第一个for循环,取出providers中的每一个ContentProvider组件,并且拿到ContentProvider组件对应的ContentProviderRecord对象dst;
  • 第15到22行,第二个for循环,通过两种方式,把ContentProviderRecord对象dst保存到全局变量mProviderMap;
  • 参数providers包含了要发布到AMS中的ContentProvider组件,每一个ContentProvider组件都使用一个ContentProviderHolder对象来描述,它里面包含了要发布的ContentProvider组件的一个IContentProvider接口,如图第34行到40行所示!

从前面的step8可知,一个应用程序进程请求ActivityManagerService返回一个ContentProvider组件的代理对象时,如果这个ContentProvider组件还未启动起来,那么AMS就会先创建一个新的应用程序进程来启动该ContentProvider组件,然后再在一个while循环中等待该ContentProvider组件启动完成,并且将他的一个代理对象发布到AMS中。

现在既然这个ContentProvider已经启动完成,并且将它的一个代理对象,即一个类型为Transport的Binder代理对象发布到AMS,因此,前面正在等待的一个AMS线程就可以停止等待,并且将这个类型为Transport的Binder代理对象封装成一个ContentProvider对象返回给请求它的应用程序进程。

这一步执行完毕,就会使得AMS从前面的step8返回step5,即返回到MainActivity组件所运行的应用程序进程中,即AxxApp应用程序进程!然后,继续执行ActivityThread类成员函数installProvider(),用来保存前面从ActivityManagerService获得的一个ContentProvider组件的一个IContentProvider访问接口。

step26: ActivityThread#installProvider()

ActivityThread类成员函数installProvider()源码如下:

/**
 * Installs the provider.
 *
 * Providers that are local to the process or that come from the system server
 * may be installed permanently which is indicated by setting noReleaseNeeded to true.
 * Other remote providers are reference counted.  The initial reference count
 * for all reference counted providers is one.  Providers that are not reference
 * counted do not have a reference count (at all).
 *
 * This method detects when a provider has already been installed.  When this happens,
 * it increments the reference count of the existing provider (if appropriate)
 * and returns the existing provider.  This can happen due to concurrent
 * attempts to acquire the same provider.
 */
private IActivityManager.ContentProviderHolder installProvider(Context context,
        IActivityManager.ContentProviderHolder holder, ProviderInfo info,
        boolean noisy, boolean noReleaseNeeded, boolean stable) {
    ContentProvider localProvider = null;
    IContentProvider provider;
    if (holder == null || holder.provider == null) {
        Context c = null;
        ApplicationInfo ai = info.applicationInfo;
        if (context.getPackageName().equals(ai.packageName)) {
            c = context;
        } else if (mInitialApplication != null &&
                   mInitialApplication.getPackageName().equals(ai.packageName)) {
            c = mInitialApplication;
        } else {
            try {
                c = context.createPackageContext(ai.packageName,
                                                 Context.CONTEXT_INCLUDE_CODE);
            } catch (PackageManager.NameNotFoundException e) {
                // Ignore
            }
        }
        if (c == null) {
            return null;
        }
        try {
            final java.lang.ClassLoader cl = c.getClassLoader();
            localProvider = (ContentProvider)cl.
                            loadClass(info.name).newInstance();
            provider = localProvider.getIContentProvider();
            if (provider == null) {
                return null;
            }
            // XXX Need to create the correct context for this provider.
            localProvider.attachInfo(c, info);
        } catch (java.lang.Exception e) {......}
    } else {
        provider = holder.provider;
    }

    IActivityManager.ContentProviderHolder retHolder;

    synchronized (mProviderMap) {
        IBinder jBinder = provider.asBinder();
        if (localProvider != null) {
            ComponentName cname = new ComponentName(info.packageName, info.name);
            ProviderClientRecord pr = mLocalProvidersByName.get(cname);
            if (pr != null) {
                provider = pr.mProvider;
            } else {
                holder = new IActivityManager.ContentProviderHolder(info);
                holder.provider = provider;
                holder.noReleaseNeeded = true;
                pr = installProviderAuthoritiesLocked(provider, localProvider, holder);
                mLocalProviders.put(jBinder, pr);
                mLocalProvidersByName.put(cname, pr);
            }
            retHolder = pr.mHolder;
        } else {
            ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
            if (prc != null) {
                // We need to transfer our new reference to the existing
                // ref count, releasing the old one...  but only if
                // release is needed (that is, it is not running in the
                // system process).
                if (!noReleaseNeeded) {
                    incProviderRefLocked(prc, stable);
                    try {
                        ActivityManagerNative.getDefault().removeContentProvider(
                            holder.connection, stable);
                    } catch (RemoteException e) {
                        //do nothing content provider object is dead any way
                    }
                }
            } else {
                ProviderClientRecord client = installProviderAuthoritiesLocked(
                                                provider, localProvider, holder);
                if (noReleaseNeeded) {
                    prc = new ProviderRefCount(holder, client, 1000, 1000);
                } else {
                    prc = stable
                          ? new ProviderRefCount(holder, client, 1, 0)
                          : new ProviderRefCount(holder, client, 0, 1);
                }
                mProviderRefCountMap.put(jBinder, prc);
            }
            retHolder = prc.holder;
        }
    }
    return retHolder;
}
  • 这步与前面的step20,都是在ActivityThread类成员函数installProvider()。不过,前面step20是在启动ContentProvider组件的应用程序进程中执行的;而这步是在AxxApp应用程序的MainActivity中执行的!另一个区别是:这步得到参数provider不等于null,它用来描述一个在其他应用程序进程中启动的ContentProvider组件的一个IContentProvider访问接口。

  • 这步执行完成后,就返回到前面的step1,这时在AxxApp应用的MainActivity中,就获得了与URI值对应的SubContentProvider组件的一个IContentProvider访问接口,最后就可以通过这个接口访问另一个应用程序的数据内容了!

0x03 参考文献与简单的结语


至此,ContentProvider启动流程分析到此结束!

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

推荐阅读更多精彩内容