Android插件化(二)

廣播插件的兩種實現模式

接上一篇插件化(一),已經實現了通過插裝式實現activity插件和service插件,這兩種的實現是一樣的,但是廣播就不同了,廣播分為靜態廣播和動態廣播,那么是怎么實現廣播插件的運行呢。我們先從廣播的兩種注冊方式以及使用開始分析。

靜態廣播和動態廣播:

動態廣播不需要再Manifest中聲明

靜態廣播是需要的,聲明之后通過apk的安裝,系統解析manifest來實現廣播的注冊,從而可以接受到跨進程的消息

動態廣播

動態廣播其實和activity、service一樣,也是實現一個接口

public interface ProxyBroadCastInterface {

    void attch(Context context);

    void onReceive(Context context, Intent intent);
}

我們插件中對業務進行處理的廣播

public class MyReceive extends BroadcastReceiver implements ProxyBroadCastInterface {
    @Override
    public void attch(Context context) {
        // 廣播綁定成功
        Toast.makeText(context,"廣播綁定成功",Toast.LENGTH_LONG).show();
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        //接受到廣播
        Toast.makeText(context," 接受到廣播",Toast.LENGTH_LONG).show();
    }
}

然后在我們的插件的mainActivity中加兩個按鈕 一個是注冊廣播,一個發送廣播,

findViewById(R.id.mRegiestBroadCast).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                IntentFilter intent = new IntentFilter();
                intent.addAction("com.plugin.app.receive");
                //調用register方法 肯定要調用宿主的 所以重寫baseactivity
                registerReceiver(new MyReceive(), intent);
            }
        });

        findViewById(R.id.mSendBroadCast).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.setAction("com.plugin.app.receive");
                sendBroadcast(intent);
            }
        });

那么我們這里調用注冊廣播和發送廣播的方法,就是調用的baseActivity ,那么我就重寫BaseActivity的有關廣播的兩個方法

 @Override
    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        return that.registerReceiver(receiver, filter);
    }

    @Override
    public void sendBroadcast(Intent intent) {
        that.sendBroadcast(intent);
    }

所以最終都是調用我們宿主插件activity的兩個方法,在重寫插件的activity的兩個方法

@Override
    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        //收到調用注冊動態廣播的申請,那么我就幫你完成
        IntentFilter newIntentFilter = new IntentFilter();
        for (int i = 0; i < filter.countActions(); i++) {
            newIntentFilter.addAction(filter.getAction(i));
        }
        return super.registerReceiver(new ProxyReceive(receiver.getClass().getName(),this), newIntentFilter);
    }

下面是我們的代理的廣播

public class ProxyReceive extends BroadcastReceiver {
    String className;
    private  ProxyBroadCastInterface receiveObj;

    public ProxyReceive(String className,Context context) {
        this.className = className;
        //這里通過classname 得到class對象,然后
        try {
            Class<?> receiverClass = HookManager.getInstance().getClassLoader().loadClass(className);
            Constructor constructorReceiver = receiverClass.getConstructor(new Class[]{});
            receiveObj = (ProxyBroadCastInterface) constructorReceiver.newInstance(new Object[]{});
            receiveObj.attch(context);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        receiveObj.onReceive(context, intent);
    }
}

上運行效果


plugin_receive.gif

上面完成對動態注冊的廣播插件的運行,下面就到靜態注冊了

靜態注冊廣播

  1. 內存消耗高,因為常駐內存的。但是動態注冊的廣播是受activity的生命周期影響的

靜態廣播是沒有跟隨我們的app啟動,而是手機啟動的時候就已經被加載到內存中了,但是前提是你這個app已經安裝到手機上了,所以想要實現我們的靜態廣播插件的實現,的對apk的安裝進行分析了。

apk的安裝原理

Android有四種安裝方式
  1. 已安裝的系統應用安裝其他應用

    特點:沒有安裝界面,直接安裝

  2. 手機應用市場安裝apk

    特點:直接安裝,沒有安裝界面

  3. ADB工具安裝

    特點:無安裝界面

  4. 第三方應用安裝

    特點:有安裝界面,是由PackageInstaller.apk應用 來處理安裝及卸載過程的界面。

那么安裝時,系統幫我們做了什么事,BroadCastReceive又是怎么注冊的
  1. 安裝時吧apk文件復制到data/app這個目錄 (用戶程序安裝的目錄) .
  2. 然后開辟存放應用數據的目錄: /data/data/ 包名
  3. 將apk中的dex文件安裝到data/dalvik-cache目錄下(dex文件是dalvik虛擬機的可執行文件,其大小約為apk文件大小的四分之一)。

所以安裝一個apk 系統幫我一共做了三件事,復制apk、開辟存放數據的目錄、在吧dex文件進行拷貝。

真正加載廣播,是在系統發生啟動的時候,這個時候回將手機上所有的app都安裝一遍。而我們的靜態廣播是注冊在manifest文件中的,當我們的apk通過PMS安裝的時候回去解析Manifest文件,將其中的四大組件拿出來進行注冊。那我們的插件app是沒有安裝的,那么如何將它的清單文件中的廣播拿出來呢,所以我們有必要來梳理一些apk的安裝,也就是PMS。

PMS(PackageManagerService)服務

PMS服務是在開機的時候由SystemServer (系統服務去調用的)

SystemServer.java
//這是它的main方法
public static void main(String[] args) {
        new SystemServer().run();
}
private void run(){
  ...
  mPackageManagerService = PackageManagerService.main(mSystemContext, installer,
  mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore);
                ...
 }

那么我們分析一下系統是怎么去掃描data/app下面的程序呢
先看這個PackageManagerService.java文件

//這個是main方法,當系統服務運行起來就會調用PMS的main方法
public static PackageManagerService main(Context context, Installer installer,
            boolean factoryTest, boolean onlyCore) {
        // Self-check for initial settings.
        PackageManagerServiceCompilerMapping.checkProperties();
    
        //調用了構造方法
        PackageManagerService m = new PackageManagerService(context, installer,
                factoryTest, onlyCore);
        m.enableSystemUserPackages();
        ServiceManager.addService("package", m);
        final PackageManagerNative pmn = m.new PackageManagerNative();
        ServiceManager.addService("package_native", pmn);
        return m;
    }

可以看到main方法里面調用了構造方法 ,看看構造方法里面做了什么事

public PackageManagerService(Context context, Installer installer,
            boolean factoryTest, boolean onlyCore){
    ...
    synchronized (mPackages){
        ...
            //這個dataDir目錄就是/data 
            File dataDir = Environment.getDataDirectory();
            mAppInstallDir = new File(dataDir, "app");//這個目錄就是我們第三方app程序的目錄
            mAppLib32InstallDir = new File(dataDir, "app-lib");
            mAsecInternalPath = new File(dataDir, "app-asec").getPath();
            mDrmAppPrivateInstallDir = new File(dataDir, "app-private");
        ...
            //下面會調用這個方法 通過方法名字,可以看出來 是掃描/data/app這個目錄下的文件 將路徑傳進去
            scanDirTracedLI(mAppInstallDir, 0, scanFlags | SCAN_REQUIRE_KNOWN, 0);
    }
}

那我們在分析掃描data/app 這個里面做了什么事呢

private void scanDirLI(File dir, int parseFlags, int scanFlags, long currentTime) {
    final File[] files = dir.listFiles();//顯示吧所有的文件全部拿到 盤算是不是為空 如果為空,直接返回
        if (ArrayUtils.isEmpty(files)) {
            Log.d(TAG, "No files in app dir " + dir);
            return;
        }
    //接著遍歷data/app下面的目錄
    for (File file : files) {
        //如果是一個apk文件  那這個isPackage就為true
        final boolean isPackage = (isApkFile(file) || file.isDirectory())
                    && !PackageInstallerService.isStageName(file.getName());
        ...
           
       parallelPackageParser.submit(file, parseFlags);
    }
}

從上面這個submit方法可以發現,是將當前一個apk文件穿進去了

接下來到這個ParallelPackageParser.java文件了,那么我們看它的submit方法

/**
@params scanFile  當前要解析的apk文件
*/
public void submit(File scanFile, int parseFlags) {
    ...
        //看到在解析這個apk文件時 new了一個PackageParse這個javabean
        PackageParser pp = new PackageParser();
        pp.setSeparateProcesses(mSeparateProcesses);
        pp.setOnlyCoreApps(mOnlyCore);
        pp.setDisplayMetrics(mMetrics);
        pp.setCacheDir(mCacheDir);
        pp.setCallback(mPackageParserCallback);
        pr.scanFile = scanFile;
        //調用了PackageParse里面的parsePackage方法,
        pr.pkg = parsePackage(pp, scanFile, parseFlags);
    ...
}

從上面看到 調用了這個PackageParse這個類,很重要 ,那么我們去看看parsePackage這個方法到是怎么去解析一個apk文件的。

當前跳到了 PackageParse.java 文件中

public Package parsePackage(File packageFile, int flags, boolean useCaches)
            throws PackageParserException {
    ...
        //packageFile 就是從PMS那邊一步一步傳過來要解析的當前的apk文件
        //最后調用了這個方法
         parsed = parseMonolithicPackage(packageFile, flags);
    ...
}

 public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException {
      final AssetManager assets = newConfiguredAssetManager();
     ...
         //調用這個方法 并且里面new了一個AssetManager 我們可以猜到里面有調用addAssetPath這個方法
         final Package pkg = parseBaseApk(apkFile, assets, flags);
         pkg.setCodePath(apkFile.getAbsolutePath());
         pkg.setUse32bitAbi(lite.use32bitAbi);
         return pkg;
     ...
 }

private Package parseBaseApk(File apkFile, AssetManager assets, int flags)
            throws PackageParserException {
    ...
     res = new Resources(assets, mMetrics, null);
    //  private static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";
     parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);
    final Package pkg = parseBaseApk(apkPath, res, parser, flags, outError);
    
}

從上面openXmlResourceParser 這個方法看出來 現在是解析Manifest.xml文件并且最終得到一個了一個XmlResourceParser對象,然后將這個對象傳到parseBaseApk這個方法得到當前apk的封裝對象PackageParse.package 它是PackageParse的內部類

可以看下類結構圖:

PackageParse$Package.png

這個xml解析我跟到最后發現是一個native方法,所以也不必看了,直接看parseBaseApk這個方法

還是在PackageParser.java類里面

/**
@params apkPath 當前要解析的apk文件
@params res 是在上一步new出來的 
@params parser 是manifest的解析器
*/
private Package parseBaseApk(String apkPath, Resources res, XmlResourceParser parser, int flags,
            String[] outError) throws XmlPullParserException, IOException {
    ...
    //可以看到它將我們的pkgName和apkPath轉成一個數組,然后添加到我們的res里面了
    String[] overlayPaths = mCallback.getOverlayPaths(pkgName, apkPath);
            if (overlayPaths != null && overlayPaths.length > 0) {
                for (String overlayPath : overlayPaths) {
                    res.getAssets().addOverlayPath(overlayPath);
                }
            }
    ...
    final Package pkg = new Package(pkgName);
    ...
    //又調用這個方法
    return parseBaseApkCommon(pkg, null, res, parser, flags, outError);
 }

一入源碼深似海,反正都是各種調用,其實發現真正做事情就那么幾個方法,只能時候封裝的太好了,題外話。。

再看看parseBaseApkCommon 這個方法

private Package parseBaseApkCommon(Package pkg, Set<String> acceptedTags, Resources res,
XmlResourceParser parser, int flags, String[] outError) throws XmlPullParserException,
            IOException {
                ...
       //private static final String TAG_APPLICATION = "application";
       if (tagName.equals(TAG_APPLICATION)) {
           //從這里我們看出來 才TM是真正解析application標簽了,前面分析了那么多,現在才是目的
           ...
              //又來一個調用的方法 只能進去看看了
            if (!parseBaseApplication(pkg, res, parser, flags, outError)) {
                    return null;
               }
       }
 }

// 從名字就能看出來 解析application標簽的

private boolean parseBaseApplication(Package owner, Resources res,
            XmlResourceParser parser, int flags, String[] outError)
   ...
    while(...){
         String tagName = parser.getName();
        //得到我們當前解析的標簽名
        if (tagName.equals("activity")) {
            //如果是activity 
             Activity a = parseActivity(owner, res, parser, flags, outError, cachedArgs, false,
                        owner.baseHardwareAccelerated);
                if (a == null) {
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return false;
                }

                owner.activities.add(a);

            } else if (tagName.equals("receiver")) {
            // 如果是receiver
                Activity a = parseActivity(owner, res, parser, flags, outError, cachedArgs,
                        true, false);
                if (a == null) {
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return false;
                }

                owner.receivers.add(a);

            } else if (tagName.equals("service")) {
                Service s = parseService(owner, res, parser, flags, outError, cachedArgs);
                if (s == null) {
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return false;
                }

                owner.services.add(s);

            } else if (tagName.equals("provider")) {
                Provider p = parseProvider(owner, res, parser, flags, outError, cachedArgs);
                if (p == null) {
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return false;
                }

                owner.providers.add(p);
            }
    }
    
}

從上面我們可以看出來,通過xml解析將四大組件解析出來,放到Package這個類的四個集合中去,如下

PackageParse$Package

public final ArrayList<Activity> activities = new ArrayList<Activity>(0);
public final ArrayList<Activity> receivers = new ArrayList<Activity>(0);
public final ArrayList<Provider> providers = new ArrayList<Provider>(0);
public final ArrayList<Service> services = new ArrayList<Service>(0);

我們看到存放activity的集合的泛型的是Activity,這里要說明的是不是我們四大組件的activity,而是我們的PackageParse的內部類 Activity(PackageParse$Package)。并且activity和receiver集合的泛型都是一樣的,那么這是為啥?因為我們在清單文件 里面注冊activity或者receiver都要聲明IntetFilter,所以在設計的時候,能復用就復用。對于Service和Provider就不一樣了。

那么我們就看看是如何是怎樣將標簽變成一個Activity的,看parseActivity方法

private Activity parseActivity(Package owner, Resources res,
            XmlResourceParser parser, int flags, String[] outError, CachedComponentArgs cachedArgs,
            boolean receiver, boolean hardwareAccelerated)
            throws XmlPullParserException, IOException {
    
    ...
        //如果是reveicer就是true 否則就是false
   cachedArgs.mActivityArgs.tag = receiver ? "<receiver>" : "<activity>";
    if (!receiver) {
        //如果是activity 
    }else{
        // 如果是receiver
       
    }
    ...
        //開始解析activity和receiver的intent-Filter
       while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
               && (type != XmlPullParser.END_TAG
                       || parser.getDepth() > outerDepth)) {
           if (parser.getName().equals("intent-filter")) {
               //可以看到這里就解析的intent-filter
               ActivityIntentInfo intent = new ActivityIntentInfo(a);
               ...
                //這個a 就是我們的Activity,intents就是用來裝Intent的一個集合,這里是ActivityIntenrInfo,那么ActivityIntentInfo是繼承IntentInfo,IntentInfo又是繼承自IntentFilter的
                a.intents.add(intent);
           }
       }
}

那么是在哪里解析activity和receiver的name的呢,那一個標簽肯定有名字的對吧,通過查找,發現名字是在一個

Activity類里面的ActivityInfo里面 而且ActivityInfo繼承ComponentInfo 繼承PackageItemInfo,最終發現在PackageItemInfo這個類里面,看源碼的注釋可以看到

public class PackageItemInfo {
 /**
     * Public name of this item. From the "android:name" attribute.
     */
    public String name;// android:name 就是我們要找的組件的名字
}

而且這個ActivityInfo 是通過調用這個方法生成的

public static final ActivityInfo generateActivityInfo(ActivityInfo ai, int flags,
            PackageUserState state, int userId) {
        if (ai == null) return null;
        if (!checkUseInstalledOrHidden(flags, state, ai.applicationInfo)) {
            return null;
        }
        // This is only used to return the ResolverActivity; we will just always
        // make a copy.
        ai = new ActivityInfo(ai);
        ai.applicationInfo = generateApplicationInfo(ai.applicationInfo, flags, state, userId);
        return ai;
    }

至此分析的差不多了 ,也知道開啟啟動,Android為啥啟動的這么慢,就是因為要遍歷所有的apk,安裝一遍,解析MainFest文件這些操作。

插件中靜態廣播的解析

通過上面的分析,知道廣播是怎樣解析的,怎樣加載到內存中去的,通過什么樣的方式,那些方法,怎么調用的,最終又是封裝在那些類里面,知道了這些原理,我們才可以去加載插件apk中的一個靜態廣播,其實上面分析了這么多,就是為了干這個事情,也順帶著把我們的PMS稍微分析了一遍

那么我們就開始解析我們插件中的廣播,在HookManager 的loadPathToPlugin這個方法中解析我們的清單文件

/**
     * 通過解析清單文件來 拿到靜態廣播并且進行注冊
     *
     * @param activity
     * @param path
     */
    private void parseReceivers(Activity activity, String path) {
        try {
            //我們知道解析一個apk文件的入口就是PackageParse.parsePackage 這個方法
            //所以我們使用反射 來調用這個方法 最終得到了一個 PackageParse$Package 這個類
            Class<?> mPackageParseClass = Class.forName("android.content.pm.PackageParser");
            Method mParsePackageMethod = mPackageParseClass.getDeclaredMethod("parsePackage", File.class, int.class);
            Object mPackageParseObj = mPackageParseClass.newInstance();
            Object mPackageObj = mParsePackageMethod.invoke(mPackageParseObj, new File(path), PackageManager.GET_ACTIVITIES);

            //解析出來的receiver就存在PackageParse$Package 這個類里面的一個receivers集合里面
            Field mReceiversListField = mPackageObj.getClass().getDeclaredField("receivers");
            //然后得到反射得到這個屬性的值 最終得到一個集合
            List mReceiverList = (List) mReceiversListField.get(mPackageObj);

            //接下來我們要拿到 IntentFilter 和name屬性 這樣才能反射創建對象,動態在宿主里面注冊廣播
            Class<?> mComponetClass = Class.forName("android.content.pm.PackageParser$Component");
            Field mIntentFields = mComponetClass.getDeclaredField("intents");

            //這兩行是為了調用generateActivityInfo 而反射拿到的參數
            Class<?> mPackageParse$ActivityClass = Class.forName("android.content.pm.PackageParser$Activity");
            Class<?> mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");


            Object mPackzgeUserStateObj = mPackageUserStateClass.newInstance();


            // 拿到generateActivityInfo這個方法
            Method mGeneReceiverInfo = mPackageParseClass.getMethod("generateActivityInfo", mPackageParse$ActivityClass, int.class, mPackageUserStateClass, int.class);

            Class<?> mUserHandlerClass = Class.forName("android.os.UserHandle");
            Method getCallingUserIdMethod = mUserHandlerClass.getDeclaredMethod("getCallingUserId");

            int userId = (int) getCallingUserIdMethod.invoke(null);


            //然后for循環 去拿到name和 intentFilter
            for (Object activityObj : mReceiverList) {
                //調用generateActivityInfo
                // 這個是我們要調用的方法的形參 public static final ActivityInfo generateActivityInfo(Activity a, int flags,PackageUserState state, int userId);
                //得到一個ActivityInfo
                ActivityInfo info = (ActivityInfo) mGeneReceiverInfo.invoke(mPackageParseObj, activityObj, 0, mPackzgeUserStateObj, userId);
                //拿到這個name 相當于我們在清單文件中Android:name 這樣,是一個全類名,然后通過反射去創建對象
                BroadcastReceiver broadcastReceiver = (BroadcastReceiver) getClassLoader().loadClass(info.name).newInstance();

                //在拿到IntentFilter
                List<? extends IntentFilter> intents = (List<? extends IntentFilter>) mIntentFields.get(activityObj);
                //然后直接調用registerReceiver方法發
                for (IntentFilter intentFilter : intents) {
                    activity.registerReceiver(broadcastReceiver, intentFilter);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

ok 然后創建廣播和插件進行注冊,下面是效果圖

plugin_static_receive.gif

這是源碼地址:https://github.com/doujd/PluginDemo1

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容