Android動(dòng)態(tài)加載

Dload

一個(gè)動(dòng)態(tài)加載jar包的實(shí)例

github:https://github.com/andoop/Dload


概念?說明

動(dòng)態(tài)加載:

此處的動(dòng)態(tài)加載是指從服務(wù)端或者其他地方獲取jar包摹闽,并在運(yùn)行時(shí)期刀疙,加載jar包您炉,并與 jar包互相調(diào)用妓美。
本例中凤跑,為了方便演示久锥,將要?jiǎng)討B(tài)加載的jar放到了assets目錄下姆钉,在程序運(yùn)行時(shí)期佳晶,將其加載到/data/data/pkgname/files下,來模擬從服務(wù)端獲取

為什么要?jiǎng)討B(tài)加載:
  1. 減少應(yīng)用安裝包體積截型, 程序包很大時(shí)趴荸,將部分模塊打成jar包,動(dòng)態(tài)的從服務(wù)端獲取宦焦,然后加載
  2. 方便升級(jí)維護(hù)发钝,將主要功能放入動(dòng)態(tài)jar包中,主應(yīng)用(不包含動(dòng)態(tài)jar包的部分)主要來維護(hù)動(dòng)態(tài)jar波闹,包括jar的加載酝豪,升級(jí)等。升級(jí)應(yīng)用精堕,可以更新動(dòng)態(tài)jar包孵淘,用戶在不重新安裝的情況下,就能做到部分(強(qiáng)調(diào)部分歹篓,是因?yàn)樗容^適用于部分功能升級(jí))升級(jí)
  3. 插件化開發(fā)瘫证,動(dòng)態(tài)jar包可以當(dāng)做插件來開發(fā),在應(yīng)用中滋捶,需要什么功能痛悯,就下載什么插件,如一些皮膚主題類的功能重窟,可以作為插件功能來開發(fā)载萌,用戶更換皮膚或者主題時(shí),只需要下載和更新對(duì)應(yīng)的插件就行,如:桌面系統(tǒng)(不同的桌面主題)扭仁,鎖屏(不同的鎖屏界面和風(fēng)格)
  4. 感覺還有好多好處垮衷,不一一列舉了........

demo比較簡(jiǎn)單,但是能演示和介紹整個(gè)流程以及思想就行啦乖坠,干脆利索搀突,效果圖如下:
圖片名稱

圖片名稱
圖片名稱
打開插件就會(huì)進(jìn)入插件中的頁面(ListFragment),點(diǎn)擊任何一個(gè)條目熊泵,進(jìn)入插件中另一個(gè)頁面(DetailFragment)

功能?實(shí)現(xiàn)細(xì)節(jié)

首先從一個(gè)類入手仰迁,這個(gè)類是功能核心,那就是ClassLoader,這個(gè)類可以讓我們實(shí)現(xiàn)動(dòng)態(tài)加載顽分,ClassLoader是一個(gè)抽象類徐许,實(shí)際開發(fā)過程中,我們一般是使用其具體的子類DexClassLoader卒蘸、PathClassLoader這些類加載器來加載類的雌隅,它們的不同之處是:

  1. DexClassLoader可以加載jar/apk/dex,可以從SD卡中加載未安裝的apk缸沃;
  2. PathClassLoader只能加載系統(tǒng)中已經(jīng)安裝過的apk恰起;

所以,因?yàn)槲覀円虞djar趾牧,所以我們選擇DexClassLoader检盼。


看一下整個(gè)工程目錄結(jié)構(gòu)

宿主工程和動(dòng)態(tài)工程都依賴dloadlib,dloadlib中定義了兩個(gè)工程都交互的接口武氓,并對(duì)動(dòng)態(tài)jar進(jìn)行加載和調(diào)用

看一下dloadlib工程結(jié)構(gòu)


看一下代碼細(xì)節(jié)

宿主工程中:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化插件
        Dload.getInstance(this).init(new DloadListener() {
            
            @Override
            public void onSuccess() {
                // TODO Auto-generated method stub
                Log.e(">>>","sccess");
            }
            
            @Override
            public void onStart() {
                // TODO Auto-generated method stub
                Log.e(">>>","start");
            }
            
            @Override
            public void onProgress(int persent) {
                // TODO Auto-generated method stub
                Log.e(">>>","progress>"+persent+"%");
            }
            
            @Override
            public void onFail(String err) {
                // TODO Auto-generated method stub
                Log.e(">>>","fail>"+err);
            }
        });
    }

  
    public void showList(View view){
        //打開插件中ListFragment
        Dload.getInstance(this).showList();
    }
}

Dload類中邏輯

public class Dload {

    private Context context;
    private DloadProxy dloadProxy;
    
    public static Dload INSTANCE;
    private Dload(Context context){
        this.context = context;
    }
    //單例
    public static Dload getInstance(Context context){
        
      if(INSTANCE==null){
          synchronized (Dload.class) {
              if(INSTANCE==null){
                  INSTANCE=new Dload(context);
              }
            
        }
      }
      return INSTANCE;
    }
    /**
     * 初始化狀態(tài)梯皿,例如:加載插件
     */
    public void init(DloadListener dloadListener){
        //真正去加載插件
        DexExcutor.getInstance(context).init(dloadListener);
        //初始化代理類
        dloadProxy=new DloadProxy();
    }
    /**
     * 打開列表,最終會(huì)調(diào)用插件功能
     */
    public void showList(){
        //調(diào)用代理類的方法县恕,最終會(huì)調(diào)用插件功能
        dloadProxy.showList(context);
    }
}

看一下怎么初始化插件,DexExcutor類走起..

public class DexExcutor {

    public static DexExcutor INSTANCE;
    private Context context;
    private DloadListener dloadListener;
    private String filepath;
    private DexClassLoader dexClassLoader;
    private DexExcutor(Context context){
        this.context = context;
        filepath=context.getFilesDir().getAbsolutePath();
    }
    //單例
    public static DexExcutor getInstance(Context context){
        
      if(INSTANCE==null){
          synchronized (Dload.class) {
              if(INSTANCE==null){
                  INSTANCE=new DexExcutor(context);
              }
            
        }
      }
      return INSTANCE;
    }
    //初始化插件
    public void init(DloadListener dloadListener){
        this.dloadListener = dloadListener;
        if(dloadListener!=null)
            dloadListener.onStart();
        
        //檢查插件是否存在
        File file = new File(filepath+File.separator+MConstans.jarname);
        if(file.exists()){
            //存在剂桥,加載插件
            loadPlugin();
        }else{
            //不存在忠烛,則將插件從asset或者服務(wù)端加載到本地
            downloadPlugin();
        }
    }
    
    private void downloadPlugin() {
        // TODO Auto-generated method stub
        DloadUtils.downloadPlugin(context,"plugin.jar",filepath+File.separator+MConstans.jarname,new DloadUtils.Downloadlistener() {
            
            @Override
            public void onSuccess() {
                // TODO Auto-generated method stub
                loadPlugin();
            }
            
            @Override
            public void onStart() {
                // TODO Auto-generated method stub
                
            }
            
            @Override
            public void onProgress(int progress) {
                // TODO Auto-generated method stub
                if(dloadListener!=null){
                    dloadListener.onProgress(progress);
                }
            }
            
            @Override
            public void onFail(String err) {
                // TODO Auto-generated method stub
                if(dloadListener!=null){
                    dloadListener.onFail(err);
                }
            }
        });
        
    }
    /**
     * 加載插件
     */
    private void loadPlugin() {
        
        try {
            //根據(jù)插件的路徑,實(shí)例化對(duì)應(yīng)的dexclassloader
            dexClassLoader = new DexClassLoader(filepath+File.separator+MConstans.jarname, filepath,null, context.getClassLoader());
            if(dloadListener!=null){
                dloadListener.onProgress(100);
                dloadListener.onSuccess();
            }
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            if(dloadListener!=null)
                dloadListener.onFail(e.getMessage());
        }
    }
    
    /**
     * 根據(jù)類名獲取一個(gè)實(shí)例
     * 
     * @param className 動(dòng)態(tài)工程中類的全類名
     *           
     * @return Object
     */
    public Object newInstance(String className) {
        try {
            if (null != dexClassLoader) {
                Class<?> dynamic_class = dexClassLoader.loadClass(className);
                return dynamic_class.newInstance();
            }
        } catch (ClassNotFoundException e) {
            Log.e(">>>", "newInstance:" + e.getMessage());
        } catch (InstantiationException e) {
            Log.e(">>>", "newInstance:" + e.getMessage());
        } catch (IllegalAccessException e) {
            Log.e(">>>", "newInstance:" + e.getMessage());
        }
        return null;
    }

    
}

流程很簡(jiǎn)單权逗,再看一下代理是怎樣個(gè)代理美尸,進(jìn)入DloadProxy,

public class DloadProxy implements Idload {
  
    @Override
    public void showList(Context context) {
        //實(shí)例化動(dòng)態(tài)工程中的類斟薇,這個(gè)類實(shí)現(xiàn)了Idload接口
        Idload newInstance = (Idload) DexExcutor.getInstance(context).newInstance("cn.andoop.android.dloadplugin.DloadImp");
        if(newInstance==null){

            Toast.makeText(context, "插件還沒有加載好", Toast.LENGTH_SHORT).show();
            return;
        }
        //真正調(diào)用動(dòng)態(tài)工程中的功能
        newInstance.showList(context);
    }
}

有沒有很簡(jiǎn)單J病(因?yàn)槭莇emo,所以邏輯越簡(jiǎn)單越好堪滨,沒有做很多容錯(cuò)處理)


看一下動(dòng)態(tài)工程中怎樣實(shí)現(xiàn)的吧

public class DloadImp implements Idload {

    @Override
    public void showList(Context context) {
        // TODO Auto-generated method stub
        DLoadActivity.start(context, "cn.andoop.android.dloadplugin.ui.ListFragment", null);
    }

    
}

DloadImp 是動(dòng)態(tài)工程中的類胯陋,這個(gè)類就是對(duì)dloadlib中Idload的實(shí)現(xiàn)
DloadActivity是dloadlib中的acitvity,這個(gè)activity就是預(yù)先在宿主中注冊(cè)過了activity,用它來承載動(dòng)態(tài)工程中所有的頁面(Fragment)遏乔,這樣看來义矛,是不是動(dòng)態(tài)工程中展現(xiàn)頁面只能是Fragment?,其實(shí)不然,動(dòng)態(tài)工程中其實(shí)可以有fragment盟萨,activity凉翻,service,等組件捻激,只不過因?yàn)槭莿?dòng)態(tài)加載進(jìn)來的制轰,說白了,最后這些類的實(shí)例最后都是反射得到的胞谭,他們的生命周期艇挨,系統(tǒng)可不管,所以直接使用動(dòng)態(tài)工程中的fragment韭赘、activity缩滨、service等有生命周期的組件肯定是不行的,但是方法總是有地泉瞻,那就是“占坑”脉漏,那就是預(yù)先在宿主中注冊(cè)一些組件,如activity袖牙、service侧巨,然后讓這些組件來代理動(dòng)態(tài)工程中組件的生命周期,這樣問題就解決了鞭达,但是也有其他問題呀司忱,那就是動(dòng)態(tài)工程中的資源怎樣使用呢,能不能通過R去訪問呢畴蹭,這些問題留在后面解答坦仍,在我的dltest工程里尋找答案吧


看一下DLoadActivity吧,看看怎樣代理fragment生命周期

public class DLoadActivity extends FragmentActivity {

    private final static String CLASS_NAME = "classname";
    private String mClassName;
    private Bundle mBundle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (null != getIntent()) {
            mClassName = getIntent().getStringExtra(CLASS_NAME);
            mBundle = getIntent().getExtras();
        }

        setContentView(getContentView());

    }

    private View getContentView() {
        // TODO Auto-generated method stub

        LinearLayout ll_content = new LinearLayout(this);
        ll_content.setOrientation(LinearLayout.VERTICAL);
        ll_content.setId(10000012);
        FragmentTransaction ft = this.getSupportFragmentManager()
                .beginTransaction();
        // 通過類名叨襟,反射獲取到對(duì)應(yīng)的類繁扎。既Fragment
        Fragment fragment = (Fragment) DexExcutor.getInstance(this)
                .newInstance(mClassName);
        if (null != fragment) {
            if (null != mBundle) {
                fragment.setArguments(mBundle);
            }
            ft.add(ll_content.getId(), fragment, mClassName);
            ft.commit();
        }

        return ll_content;
    }
    //調(diào)用DLoadActivity加載fragment
    public static void start(Context context, String className, Bundle bundle) {
        Intent intent = new Intent(context, DLoadActivity.class);
        if (null != bundle) {
            intent.putExtras(bundle);
        }
        intent.putExtra(CLASS_NAME, className);
        context.startActivity(intent);
    }

}

動(dòng)態(tài)工程中fragment又是怎樣寫的呢?

public class ListFragment extends Fragment implements OnItemClickListener {

private RelativeLayout rl_content;
private MBaseAdapter mBaseAdapter;
private List<ListItem> data;

@Override
@Nullable
public View onCreateView(LayoutInflater inflater,
        @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    // TODO Auto-generated method stub
    initData();
    return getContentView();
}


private View getContentView() {
    rl_content = new RelativeLayout(getActivity());
    ListView listView = new ListView(getActivity());
    rl_content.addView(listView);
    listView.setAdapter(mBaseAdapter);
    listView.setOnItemClickListener(this);
    return rl_content;
}
   ...后面代碼就不粘貼啦

在這個(gè)demo中糊闽,可不能給fragment寫布局文件了梳玫,必須進(jìn)行代碼布局了,其實(shí)也沒啥嘛右犹,布局文件能干的提澎,我都能在代碼中實(shí)現(xiàn),不就是多寫幾行代碼而已念链,但是圖片資源怎樣使用呢盼忌?
看看下面吧


在動(dòng)態(tài)工程中使用圖片資源

這是動(dòng)態(tài)工程中另一個(gè)fragment积糯,在它中,使用了圖片資源
public class DetailFragment extends Fragment {

    @Override
    @Nullable
    public View onCreateView(LayoutInflater inflater,
            @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        return getContentView();
    }

    //照樣通過代碼布局
    private View getContentView() {
        LinearLayout linearLayout = new LinearLayout(getActivity());
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        TextView title = new TextView(getActivity());
        ImageView imageView=new ImageView(getActivity());
        WebView webView = new WebView(getActivity());
        
        title.setPadding(20, 20, 20, 20);
        title.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        title.setGravity(Gravity.CENTER);
        
        webView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        webView.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                // TODO Auto-generated method stub
                return false;
            }
        });
        webView.getSettings().setJavaScriptEnabled(true);
        
        linearLayout.addView(title);
        linearLayout.addView(imageView);
        linearLayout.addView(webView);

        //獲取資源碴犬,就這樣絮宁,將圖片名稱傳入這個(gè)工具類,就可以獲取bp對(duì)象
        Bitmap imageResouce = new ResourceUtils(getActivity()).getImageResouce("assets/car.jpg");
        imageView.setImageBitmap(imageResouce);

        Bundle bundle = getArguments();
        if(bundle!=null&&( bundle.getSerializable("item")!=null)){
            if(bundle.getSerializable("item") instanceof ListItem) {
                ListItem listitem = (ListItem) bundle.getSerializable("item");
                title.setText(listitem.text);
                webView.loadUrl(listitem.url);
            }
        }
        
        return linearLayout;
    }
}

ResourceUtils是一個(gè)工具類服协,就是讀取文件中圖片而已绍昂,有興趣可以看看,也可以完善一下
代碼分析到此結(jié)束偿荷,如有不懂窘游,還是看看code吧,show you code跳纳!忍饰,但是本篇教程距離結(jié)束還早呢,還是往下看吧


怎樣將動(dòng)態(tài)工程打包呢寺庄?

task buildLib (type: Jar,dependsOn:'build') {
    from ('build/intermediates/classes/release')
    //包含資源目錄
    from ('src/main/assets/')
    //from fileTree(dir: 'src/main',includes: ['assets/**'])

}

自定義task艾蓝,打包的時(shí)候也需要用到的圖片放到j(luò)ar中,需要將圖片放入到assets文件夾下的assets下
執(zhí)行buildLib即可生成動(dòng)態(tài)工程對(duì)應(yīng)的jar斗塘,所在目錄為build/lib


對(duì)生成的jar再次處理

android dalvik 不能直接加載變異.class文件赢织,需要再次處理一下,編譯成dex文件馍盟,通過dx命令即可于置,

格式如下

dx --dex --output=out.jar in.jar

次工程對(duì)應(yīng)的批處理文件(在builddex中)如下:

cd E:\android_dev\sdk\sdk\build-tools\23.0.2\
e:
dx --dex --output=F:\projects\mprojects\DLoad\builddex\dex\plugin.jar F:\projects\mprojects\DLoad\builddex\dloadplugin.jar

需要處理的jar為dloadplugin.jar,生成的jar為plugin.jar贞岭,執(zhí)行dx命令需要到對(duì)應(yīng)文件夾下執(zhí)行才行八毯,如我的:E:\android_dev\sdk\sdk\build-tools\23.0.2\,不多解釋了瞄桨,很簡(jiǎn)單

最后话速,將生成的plugin.jar放入宿主工程的assets目錄即可(本工程是這樣做的,真正開發(fā)中讲婚,jar往往會(huì)放在服務(wù)端尿孔,宿主去檢查時(shí)候需要更新插件),本工程只為演示而生筹麸,更多變化,以及完善雏婶,這都需要你結(jié)合實(shí)際開發(fā)而去應(yīng)對(duì)物赶。


總結(jié)一下吧

動(dòng)態(tài)加載用法流程如下:

  1. 定義宿主和動(dòng)態(tài)工程交互接口如:ILoad,并預(yù)先注冊(cè)需要的組件
  2. 宿主或著動(dòng)態(tài)工程中實(shí)現(xiàn)接口
  3. 生成動(dòng)態(tài)jar
  4. 通過dexclassloader加載動(dòng)態(tài)jar包留晚,動(dòng)態(tài)jar包可以來源于服務(wù)端或者其他地方
  5. 宿主調(diào)用動(dòng)態(tài)工程中ILoad的實(shí)現(xiàn)酵紫,動(dòng)態(tài)工程也可以調(diào)用宿主方法告嘲,宿主實(shí)現(xiàn)一下ILoad既可(工程中沒有體現(xiàn),原理比較簡(jiǎn)單奖地,通過dloadlib橄唬,直接調(diào)用就行)

最后說一下注意點(diǎn)

工程中值演示了加載一個(gè)動(dòng)態(tài)jar,當(dāng)然也可以加載多個(gè)参歹,但是每一個(gè)動(dòng)態(tài)jar對(duì)應(yīng)一個(gè)dexclassloader對(duì)象
對(duì)jar的升級(jí)維護(hù)校驗(yàn)仰楚,工程沒有體現(xiàn),真正開發(fā)犬庇,這些都要考慮僧界。

暫時(shí)就這些吧,后期如有需求和疑問臭挽,還會(huì)再補(bǔ)充完善的

周一捂襟、二會(huì)不斷更新內(nèi)容,歡迎持續(xù)關(guān)注andoop欢峰,每周干貨永不停葬荷!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市纽帖,隨后出現(xiàn)的幾起案子宠漩,更是在濱河造成了極大的恐慌,老刑警劉巖抛计,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哄孤,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡吹截,警方通過查閱死者的電腦和手機(jī)瘦陈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來波俄,“玉大人晨逝,你說我怎么就攤上這事∨称蹋” “怎么了捉貌?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)冬念。 經(jīng)常有香客問我趁窃,道長(zhǎng),這世上最難降的妖魔是什么急前? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任醒陆,我火速辦了婚禮,結(jié)果婚禮上裆针,老公的妹妹穿的比我還像新娘刨摩。我一直安慰自己寺晌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布澡刹。 她就那樣靜靜地躺著呻征,像睡著了一般。 火紅的嫁衣襯著肌膚如雪罢浇。 梳的紋絲不亂的頭發(fā)上陆赋,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音己莺,去河邊找鬼奏甫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛凌受,可吹牛的內(nèi)容都是我干的阵子。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼胜蛉,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼挠进!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起誊册,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤领突,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后案怯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體君旦,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年嘲碱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了金砍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡麦锯,死狀恐怖恕稠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情扶欣,我是刑警寧澤鹅巍,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站料祠,受9級(jí)特大地震影響骆捧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜髓绽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一凑懂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧梧宫,春花似錦接谨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至忌卤,卻和暖如春扫夜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驰徊。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工笤闯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人棍厂。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓颗味,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親牺弹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子浦马,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容