一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

android架構(gòu)設(shè)計(jì)之插件化、組件化

 quasiceo 2018-08-15
置頂 2018年01月08日 10:21:50


如今移動(dòng)app市場(chǎng)已經(jīng)是百花齊放,其中有不乏有很多大型公司、巨型公司都是通過app創(chuàng)業(yè)發(fā)展起來的;app類型更加豐富,有電子商務(wù)、有視頻、有社交、有工具等等,基本上涵蓋了各行各業(yè)每個(gè)角落,為了更加具有競(jìng)爭(zhēng)力app不僅功能上有創(chuàng)性,內(nèi)容也更加多元化,更加飽滿,所以出現(xiàn)了巨大的工程。這些工程代碼不停添加如果沒有一個(gè)好的架構(gòu)所有代碼將會(huì)強(qiáng)耦合在一起,功能直接也會(huì)有很多依賴,那么就會(huì)出現(xiàn)很多問題;例如:

1、修改功能困難,牽一發(fā)動(dòng)全身。很多地方如果api寫的不好,封裝不優(yōu)雅,那么就會(huì)出現(xiàn)改一個(gè)地方需要改很多地方的調(diào)用。
2、更新迭代工作中冗余廢棄代碼資源過多造成刪除冗余變得很復(fù)雜,并且很可能出現(xiàn)很多bug。

大型app有哪些架構(gòu)解決方案?

在編碼架構(gòu)上有:

mvc
mvp
mvvm
  • 1
  • 2
  • 3
  • 4

從項(xiàng)目結(jié)構(gòu)上有:

插件化
組件化
  • 1
  • 2
  • 3

這里我們一個(gè)個(gè)來分析說明:

首先我們來看看編碼設(shè)計(jì)模式,上面模式的模式都是抽象模式,所以這個(gè)具象界定沒有官方一致的規(guī)定。所以要根據(jù)自己的理解很解釋都有自己的一套mvc模式,不一定是具象到什么細(xì)節(jié)這類的,下面討論的也是會(huì)舉出例子來說明自己的理解。

代碼設(shè)計(jì)模式

MVC全名是Model View Controller,是模型(model)-視圖(view)-控制器(controller)的縮寫,一種軟件設(shè)計(jì)典范,用一種業(yè)務(wù)邏輯、數(shù)據(jù)、界面顯示分離的方法組織代碼,將業(yè)務(wù)邏輯聚集到一個(gè)部件里面,在改進(jìn)和個(gè)性化定制界面及用戶交互的同時(shí),不需要重新編寫業(yè)務(wù)邏輯。MVC被獨(dú)特的發(fā)展起來用于映射傳統(tǒng)的輸入、處理和輸出功能在一個(gè)邏輯的圖形化用戶界面的結(jié)構(gòu)中。

舉個(gè)栗子:具有生命周期的activity相當(dāng)于Controller, 自己開發(fā)封裝用于獲取數(shù)據(jù)(網(wǎng)絡(luò)數(shù)據(jù)、本地?cái)?shù)據(jù)、數(shù)據(jù)處理邏輯等)的api相當(dāng)與Model,xml控件和自定義控制控件顯示數(shù)據(jù)的邏輯相當(dāng)與view。
mvc模式是非常常見的模式基本上有基本概念就能按照這個(gè)模式進(jìn)行開發(fā),這里就不過多討論了。

MVP 全稱:Model-View-Presenter ;MVP 是從經(jīng)典的模式MVC演變而來,它們的基本思想有相通的地方:Controller/Presenter負(fù)責(zé)邏輯的處理,Model提供數(shù)據(jù),View負(fù)責(zé)顯示。

舉個(gè)栗子:Adapter相當(dāng)與Presenter控制控制數(shù)據(jù)與顯示的分離,向Adapter喂食數(shù)據(jù)的api獲取處理數(shù)據(jù)相當(dāng)與Model,支持Adapter的顯示的控件相當(dāng)于View層。

mvp是從mvc基礎(chǔ)上衍生出來的,mvp看上去與mvc好像沒有什么差別,但是實(shí)際不然,mvc model數(shù)據(jù)與view層組合是直接組合,難免會(huì)產(chǎn)生耦合,這樣model復(fù)用性有一定缺失。mvp優(yōu)化這種結(jié)構(gòu),他抽象出來一個(gè)接口規(guī)則,那么view需要支持這個(gè)規(guī)則,而model按照這個(gè)規(guī)則向里面喂食數(shù)據(jù)。這樣解開耦合model view,這樣model與view鏈接邏輯都是用Presenter控制。

MVVM是Model-View-ViewModel的簡(jiǎn)寫。微軟的WPF帶來了新的技術(shù)體驗(yàn),如Silverlight、音頻、視頻、3D、動(dòng)畫……,這導(dǎo)致了軟件UI層更加細(xì)節(jié)化、可定制化。同時(shí),在技術(shù)層面,WPF也帶來了 諸如Binding、Dependency Property、Routed Events、Command、DataTemplate、ControlTemplate等新特性。MVVM(Model-View-ViewModel)框架的由來便是MVP(Model-View-Presenter)模式與WPF結(jié)合的應(yīng)用方式時(shí)發(fā)展演變過來的一種新型架構(gòu)框架。它立足于原有MVP框架并且把WPF的新特性糅合進(jìn)去,以應(yīng)對(duì)客戶日益復(fù)雜的需求變化。

舉個(gè)栗子:使用databing可以搭建mvvm架構(gòu),獲取網(wǎng)絡(luò)數(shù)據(jù)封裝api相當(dāng)于Model,數(shù)據(jù)處理后分給databing設(shè)置界面綁定數(shù)據(jù)源和和界面上綁定的邏輯相當(dāng)于ViewModel層,用于最終現(xiàn)實(shí)的控件相當(dāng)于view層。
其實(shí)看到上面的似乎有點(diǎn)模糊不清楚,用mvp作為參照,只是p層替換成了vm層,增加xml功能屬性,能夠利于view層屬性方法來擴(kuò)張功能,將一些與界面相關(guān)邏輯處理加入這層,更加細(xì)分了抽象層次。

android組件化方案

組件化:

Android studio改變了項(xiàng)目構(gòu)建方式,eclipse環(huán)境下的工作空間和project變成現(xiàn)在的module和項(xiàng)目,這樣類別雖然不精確但是這個(gè)不是重點(diǎn),重點(diǎn)他加入項(xiàng)目構(gòu)建工具gradle使得我們項(xiàng)目構(gòu)建變得非常簡(jiǎn)單了。接下來用一個(gè)項(xiàng)目組件化方案來體會(huì)一下項(xiàng)目組件化的。

組件化好處:

1、架構(gòu)清晰業(yè)務(wù)組件間完成接耦合。
2、每個(gè)業(yè)務(wù)組件都可以根據(jù)BU需求完成獨(dú)立app發(fā)布。
3、開發(fā)中使開發(fā)者更加輕松,開發(fā)中加快功能開發(fā)調(diào)試的速度。
4、業(yè)務(wù)組件整體刪除添加替換變得非常輕松,減少工程中的代碼資源等冗余文件。
5、業(yè)務(wù)降級(jí),業(yè)務(wù)組件在促銷高峰期間可以業(yè)務(wù)為單元關(guān)閉,保證核心業(yè)務(wù)組件的順利執(zhí)行。

項(xiàng)目組件化方案

概述:
1、module library 切換。
2、組件間跳轉(zhuǎn)uri跳轉(zhuǎn)。
3、組件間通訊 binder機(jī)制。

首先看看項(xiàng)目中的角色:
這里寫圖片描述
從上圖可以發(fā)現(xiàn)有一根業(yè)務(wù)總線講所有組件個(gè)串聯(lián)起來,其中組件總線相當(dāng)于主工程(殼工程mudule),而業(yè)務(wù)組件相當(dāng)于工程中(mudule/library)??梢钥闯鼋M件化實(shí)現(xiàn)可以有自己認(rèn)定的維度,這里只是使用了最常用的維度按照業(yè)務(wù)區(qū)分組件。
上面是從抽象角來描述的一張圖,下面我們從具體角度來來看看工程結(jié)構(gòu):
這里寫圖片描述

從圖片可以看出,主要有三個(gè)角色:

1、主工程(殼工程mudele):主要負(fù)責(zé)事情不塞入任何具體業(yè)務(wù)邏輯,主要用于使用組合業(yè)務(wù)組件、初始化配置和發(fā)布應(yīng)用配置等操作。

2、組件(module/library):主要實(shí)現(xiàn)具體業(yè)務(wù)邏輯,盡可能保證業(yè)務(wù)獨(dú)立性,例如現(xiàn)在手淘這樣一個(gè)大型的app幾乎每個(gè)bu功能塊都能夠拿出來作為一個(gè)獨(dú)立業(yè)務(wù)app。但是沒有這么大型也可以按照小一些的業(yè)務(wù)邏輯來區(qū)分組件,例如:購(gòu)物車組件、收銀臺(tái)組件、用戶中心組件等,具體更具自己的項(xiàng)目需要來劃分。

3、公共庫(library):公共使用的工具類、sdk等庫,例如eventbus、xutils、rxandroid、自定義工具類等等,這些庫可以做成一個(gè)公共common sdk、也可以實(shí)現(xiàn)抽離很細(xì)按照需求依賴使用。
他們之間的關(guān)系則是 主工程依賴組件、組件依賴公共庫。

組件開發(fā)中分為兩種模式一種開發(fā)測(cè)試模式、一種是發(fā)布模式:
1、開發(fā)測(cè)試模式:這種模式下面組件應(yīng)該是獨(dú)立module模式,module是可以獨(dú)立運(yùn)行的,只要保證他對(duì)其他業(yè)務(wù)沒有依賴就可以獨(dú)立開發(fā)測(cè)試。
2、發(fā)布模式:這時(shí)候組件應(yīng)該library模式被主工程依賴組合,發(fā)布運(yùn)行,所有業(yè)務(wù)將組合成完整app發(fā)布運(yùn)行。

上面模式提出了個(gè)幾個(gè)問題我們可以一一來解決;

問題一:上面兩種模式要求組件一會(huì)是module,一會(huì)是library這樣切換是如何實(shí)現(xiàn)的?
問題二:業(yè)務(wù)之間跳轉(zhuǎn)如何進(jìn)行跳轉(zhuǎn)?
問題三:雖然業(yè)務(wù)組件相對(duì)獨(dú)立,但是如果有時(shí)候一定需要獲取其他組件運(yùn)行是某些狀態(tài)下數(shù)據(jù),也就是組件數(shù)據(jù)間的數(shù)據(jù)互通如何實(shí)現(xiàn)?

問題一:業(yè)務(wù)組件module/library切換解決方法

是用gradle輕松可以解決這個(gè)問題;每當(dāng)我們用AndroidStudio創(chuàng)建一個(gè)Android項(xiàng)目后,就會(huì)在項(xiàng)目的根目錄中生成一個(gè)文件 gradle.properties,我們將使用這個(gè)文件的一個(gè)重要屬性:在Android項(xiàng)目中的任何一個(gè)build.gradle文件中都可以把gradle.properties中的常量讀取出來;那么我們?cè)谏厦嫣岬浇鉀Q辦法就有了實(shí)際行動(dòng)的方法,首先我們?cè)趃radle.properties中定義一個(gè)常量值 isPlugin(是否是組件開發(fā)模式,true為是,false為否):

isPlugin=false
  • 1

然后我們?cè)跇I(yè)務(wù)組件的build.gradle中讀取 isPlugin,但是 gradle.properties 還有一個(gè)重要屬性: gradle.properties 中的數(shù)據(jù)類型都是String類型,使用其他數(shù)據(jù)類型需要自行轉(zhuǎn)換;也就是說我們讀到 isPlugin 是個(gè)String類型的值,而我們需要的是Boolean值,代碼如下:

if (isPlugin.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
  • 1
  • 2
  • 3
  • 4
  • 5

這樣可以輕松設(shè)置isModule就可以變成切換module、library。

接下來要解決的是就是包名,要知道library是不需要包名的,那么就可以這樣操作:

    defaultConfig {
        if (isPlugin.toBoolean()){
            applicationId 'com.example.rspluginmodule' 
        }

    ....

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

最后還要處理AndroidManifest.xml問題,因?yàn)閘ibrary、module的主配置文件是有區(qū)別的:

可以這樣處理首先在main文件家中創(chuàng)建release文件夾然后拷貝一份AndroidManifest.xml進(jìn)入release文件夾,那么發(fā)布模式下使用的就是release文件夾下面的AndroidManifest.xml,而開發(fā)模式下用的就是默認(rèn)的AndroidManifest.xml,這樣就要對(duì)release文件夾下面的AndroidManifest.xml進(jìn)行修改因?yàn)殚_發(fā)模式下release文件夾下面是用來給library使用的。

結(jié)構(gòu)如圖:
這里寫圖片描述

修改內(nèi)容release文件夾AndroidManifest.xml內(nèi)容為:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas./apk/res/android"
    xmlns:tools="http://schemas./tools"
    package="com.example.rspluginmodule">

    .........

    <application>

        <activity
            android:name="com.example.rspluginmodule.RSPluginTestActivity"
            android:exported="false"
            android:screenOrientation="portrait">
            <intent-filter>
                <data
                    android:host="sijienet"
                    android:path="/plugin_uri_path"
                    android:scheme="app_schem" />
                <action android:name="cn.com.bailian.plugin.VIEW_ACTION" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

    ........

    </application>

</manifest>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

可以發(fā)現(xiàn)上面去掉了application很多module使用的屬性。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas./apk/res/android"
    xmlns:tool="http://schemas./apk/res-auto"
    xmlns:tools="http://schemas./tools"
    package="com.example.rspluginmodule">


    ........

    <application
        android:allowBackup="true"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        tools:replace="android:allowBackup">

    <!--測(cè)試入口activity 只有在module環(huán)境下配置-->
        <activity android:name=".RSMainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>


        <activity
            android:name="com.example.rspluginmodule.RSPluginTestActivity"
            android:exported="false"
            android:screenOrientation="portrait">
            <intent-filter>
                <data
                    android:host="sijienet"
                    android:path="/plugin_uri_path"
                    android:scheme="app_schem" />
                <action android:name="cn.com.bailian.plugin.VIEW_ACTION" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

    ..........


    </application>

</manifest>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

那么AndroidManifest.xml文件已經(jīng)建立好了,接下來就要修改gradle配置了;

    sourceSets {
        main {
            if (isPlugin.toBoolean()){
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }else {
                manifest.srcFile 'src/main/release/AndroidManifest.xml'
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

這樣問題一就全部解決了,我們可以輕松使用isPlugin來切換業(yè)務(wù)組件的module和library。接下來我們看看業(yè)務(wù)組件間如何進(jìn)行跳轉(zhuǎn)問題。

問題二:業(yè)務(wù)組件間跳轉(zhuǎn)解決方法

不管是開發(fā)模式或者是發(fā)布模式我們都需要處理界面間跳轉(zhuǎn)問題,在業(yè)務(wù)運(yùn)行階段經(jīng)常會(huì)有跳轉(zhuǎn)到不同業(yè)務(wù)組件的界面的需求,我們用什么方法是可以解決這個(gè)問題,其實(shí)android本身提供了這種機(jī)制,而且在很多地方都在使用。例如:調(diào)用系統(tǒng)拍照功能、電話功能等這些功能都是在不同app當(dāng)中,你也可以理解為不同的module當(dāng)中,他們之間的調(diào)用底層都是進(jìn)程通訊,實(shí)現(xiàn)手段非常簡(jiǎn)單,就是是使用意圖篩選器。
可以看到上面的AndroidManifest.xml中配置組件activity時(shí)候都有配置意圖篩選器;

            <intent-filter>
                <data
                    android:host="sijienet"
                    android:path="/plugin_uri_path"
                    android:scheme="app_schem" />
                <action android:name="cn.com.bailian.plugin.VIEW_ACTION" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

我們就可以通過隱式意圖方式打開新的其他組件activity;舉個(gè)例子我要打開RSPluginTestActivity類;就可以調(diào)用下面的方法。

    public RMRouter jump(Activity activity,String url, String parm, int animId){
        if (url==null)
            return this;

        Log.i(TAG,"jump page=="+url);
        Log.i(TAG,"jump page parm=="+parm);

        Intent intent=new Intent(RMConfig.ROUTER_URL_ACTION, Uri.parse(url));
        intent.addCategory(Intent.CATEGORY_DEFAULT);
        intent.putExtra(RMConfig.ROUTER_PARM, parm);

        PackageManager packageManager=activity.getPackageManager();
        List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(intent, 0);

        if (! resolveInfos.isEmpty())
        {
            activity.startActivity(intent);
            selectTranlateAnim(activity, animId);
        }else {
            Log.i(TAG,"no page");
        }
        return this;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

上面使用的是uri跳轉(zhuǎn),也可以簡(jiǎn)單點(diǎn)使用跳轉(zhuǎn)。

這里還有一個(gè)就是規(guī)范問題:

1、組件命名規(guī)范,java類名加大些前綴,例如RSPluginTestActivity RS就是前綴,類似ios要求的代碼約定,xml、image等資源文件使用對(duì)應(yīng)前綴例如 rs_ 。
2、組件內(nèi)的activity、service系統(tǒng)組件要遵守rest風(fēng)格(rest風(fēng)格把業(yè)務(wù)對(duì)象看作資源用唯一uri標(biāo)識(shí)調(diào)用),組件間盡量能夠通過uri唯一標(biāo)識(shí)調(diào)用,不用過多業(yè)務(wù)bean傳遞依賴。

這樣問題二組件跳轉(zhuǎn)問題就解決了。接下來就來解決最后一座大山問題了:

問題三:組件間通訊問題

組件間如果按照規(guī)范應(yīng)該業(yè)務(wù)邏輯獨(dú)立,對(duì)其他模塊沒有耦合的情況,但是有時(shí)候要發(fā)生那么數(shù)據(jù)交換的話要怎么解決?如果嚴(yán)格按照rest風(fēng)格業(yè)務(wù)組件每塊只需要通過界面間跳轉(zhuǎn)的方式就可以輕松通過intent將數(shù)據(jù)傳輸過去,基本上可以滿足組件間數(shù)據(jù)傳遞的問題。但是這個(gè)只是簡(jiǎn)單夸界面數(shù)據(jù)傳遞,那么如果要是沒有界面跨越也想組件間數(shù)據(jù)傳遞那么要怎么解決?類似web開發(fā)http協(xié)議可以通過get post傳遞數(shù)據(jù),那么不跳頁的時(shí)候數(shù)據(jù)應(yīng)該如何通訊,web提供了ajax機(jī)制。那么android提供什么機(jī)制滿足我們需求?

如果按照地耦合的方式開發(fā),我們業(yè)務(wù)組件間是可以獨(dú)立存在,并且不需要依賴其他業(yè)務(wù)組件,如果公共部分就可以提取成公共業(yè)務(wù)組件工具庫library,但是開發(fā)需求總是非常多變,如果有時(shí)候有著情況時(shí)候我們就要用到進(jìn)程通訊aidl。首先要知道組件開發(fā)模式下的組件都是獨(dú)立module,那么每個(gè)獨(dú)立的module都是獨(dú)立進(jìn)程;在發(fā)布模式下面每個(gè)業(yè)務(wù)組件又是library,那么進(jìn)程變成了一個(gè)。aidl解決進(jìn)程間通訊、系統(tǒng)組件activity、service通訊問題的方案。aidl實(shí)際上是android提供生成binder接口的方法而已,實(shí)際上底層使用的都是binder機(jī)制。

binder機(jī)制這里簡(jiǎn)單介紹一下,他基本上貫通了了怎么android系統(tǒng)和應(yīng)用,首先他是android首選進(jìn)程通訊機(jī)制(IPC),android是建立在linux kernel之上的所以他支持linux的IPC方式,例如:網(wǎng)絡(luò)鏈接進(jìn)程通訊(Internet Process Connection): 管道(Pipe)、信號(hào)(Signal)和跟蹤(Trace)、插口(Socket)、報(bào)文隊(duì)列(Message)、共享內(nèi)存(Share Memory)和信號(hào)量(Semaphore)。那么為什么還要出現(xiàn)binder機(jī)制那是因?yàn)樗轻槍?duì)移動(dòng)端這種時(shí)效性快、資源消耗低而設(shè)計(jì)出來了,是移動(dòng)端首選的進(jìn)程通訊方式。從binder應(yīng)用的范圍就知道他重要性,除了Zygote進(jìn)程和SystemServer進(jìn)程之間使用的socket通訊之外,基本上其他進(jìn)程通訊都是用的是binder方式。

首先我們來看一張圖:
這里寫圖片描述
可以發(fā)現(xiàn)每個(gè)module都provider屬于自己提供出去的action,這樣這些可以在提供其他業(yè)務(wù)組件調(diào)用。這時(shí)候provider端相當(dāng)于服務(wù)端,提供處理后數(shù)據(jù),調(diào)用相當(dāng)于客戶端。

下面看看binder機(jī)制實(shí)現(xiàn)方法:

首先第一步創(chuàng)建進(jìn)程通訊接口:
CommonProvider.java


/**
 * 作者: 李一航
 * 時(shí)間: 18-1-4.
 */

public interface CommonProvider extends IInterface {
    String getJsonData(String jsonParm) throws RemoteException;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

這里只是創(chuàng)建一個(gè)action function 例子,可以根據(jù)自己需要?jiǎng)?chuàng)建多個(gè)action function。

接下來創(chuàng)建service端實(shí)現(xiàn)基類:

CommonStub.java


/**
 * 作者: 李一航
 * 時(shí)間: 18-1-4.
 */

public abstract class CommonStub extends Binder implements CommonProvider {

    public static final String DESCRIPTOR="com.ffmpeg.bin.CommonProvider";
    public static final int ACTION_1 = IBinder.FIRST_CALL_TRANSACTION;

    public CommonStub() {
        this.attachInterface(this, DESCRIPTOR);
    }

    @Override
    public IBinder asBinder() {
        return this;
    }

    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        switch (code)
        {
            case INTERFACE_TRANSACTION:
            {
                reply.writeString(DESCRIPTOR);
                return true;
            }
            case ACTION_1:
            {
                data.enforceInterface(DESCRIPTOR);
                String parm = data.readString();
                String jsonData = getJsonData(parm);
                reply.writeNoException();
                reply.writeString(jsonData);
                return true;
            }
        }
        return super.onTransact(code, data, reply, flags);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

service端實(shí)現(xiàn)了binder機(jī)制回調(diào)動(dòng)作:onTransact 方法。這里將會(huì)調(diào)用繼承類的接口實(shí)現(xiàn)法方法:getJsonData

接下來創(chuàng)建遠(yuǎn)程代理類:

CommonProxy.java


/**
 * 作者: 李一航
 * 時(shí)間: 18-1-4.
 */

public class CommonProxy implements CommonProvider {

    private IBinder binder;

    public CommonProxy(IBinder binder) {
        this.binder = binder;
    }

    public static CommonProvider asInterface(IBinder iBinder){
        if (iBinder==null)
            return null;
        IInterface iInterface = iBinder.queryLocalInterface(CommonStub.DESCRIPTOR);
        if (iInterface!=null && iInterface instanceof CommonProvider)
            return (CommonProvider)iInterface;
        return new CommonProxy(iBinder);
    }

    @Override
    public String getJsonData(String jsonParm) throws RemoteException {
        Parcel data = Parcel.obtain();
        Parcel reply = Parcel.obtain();
        String result=null;
        try {
            data.writeInterfaceToken(CommonStub.DESCRIPTOR);
            data.writeString(jsonParm);
            binder.transact(CommonStub.ACTION_1, data, reply, 0);
            reply.readException();
            result=reply.readString();
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            data.recycle();
            reply.recycle();
        }
        return result;
    }

    @Override
    public IBinder asBinder() {
        return binder;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

使用代理可以拿到接口代理對(duì)象完成action操作。

三個(gè)binder使用的類、接口可以作為基礎(chǔ)類庫使用。接下來可以完成遠(yuǎn)程調(diào)用例子:

創(chuàng)建service業(yè)務(wù)實(shí)現(xiàn)類:
CommonProviderImp.java


/**
 * 作者: 李一航
 * 時(shí)間: 18-1-4.
 */

public class CommonProviderImp extends CommonStub {
    @Override
    public String getJsonData(String jsonParm) throws RemoteException {

        // TODO: 18-1-4 provider action logic

        return "result data string+parm:"+jsonParm;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

這里只完成了簡(jiǎn)單的輸入輸出,方便理解測(cè)試;

創(chuàng)建調(diào)用service類:
PluginProviderService.java



/**
 * 作者: 李一航
 * 時(shí)間: 18-1-4.
 */

public class PluginProviderService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new CommonProviderImp();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在AndroidManifest.xml配置service信息:

        <service android:name="com.ffmpeg.bin.PluginProviderService">
            <intent-filter>
                <action android:name="android.intent.action.PROVIDER_MAIN_ACTION"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </service>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

上面provider暴露的action function完成了,接下來就在其他業(yè)務(wù)組件中完成調(diào)用測(cè)試。
創(chuàng)建調(diào)用測(cè)試activity調(diào)用:
ClientActivity.java


/**
 * 作者: 李一航
 * 時(shí)間: 18-1-4.
 */

public class ClientActivity extends AppCompatActivity implements ServiceConnection {

    private CommonProvider provider;

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

        Intent intent=new Intent();
        intent.setComponent(new ComponentName(getPackageName(), PluginProviderService.class.getName()));

        bindService(intent,this, BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        unbindService(this);
    }

    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
        provider = CommonProxy.asInterface(iBinder);
        try {
            Log.i(ClientActivity.class.getSimpleName(), provider.getJsonData("input"));
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {
        provider=null;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

組件間通訊就這樣完成了,這里為了好理解代碼都是使用了最簡(jiǎn)單樣子,可以在項(xiàng)目進(jìn)行封裝優(yōu)化來適合項(xiàng)目復(fù)用。

組件化總結(jié)

組件化是大型app開發(fā)中非常重要架構(gòu)設(shè)計(jì),其實(shí)上面貢獻(xiàn)的只是組件化方案的核心部分,要是一套完整組件化還需要處理很多細(xì)節(jié)問題,在開發(fā)中還要不斷封裝、優(yōu)化、復(fù)用等才能夠使得架構(gòu)清晰健壯。上面組件化方案中主要涉及到的知識(shí)點(diǎn)并不復(fù)雜,概括一下有g(shù)radle項(xiàng)目配置、router uri、 binder進(jìn)程間通訊等。組件化重要的思想,現(xiàn)在很多文章有各種各樣的方案,理清思路選擇適合自己項(xiàng)目的架構(gòu)才是最重要的。

android插件化方案

有了上面組件化方案理解之后,插件化理解也不是難事,首先我們還是用業(yè)務(wù)為界限來劃分組件內(nèi)容;開發(fā)模式下面module本來就是一個(gè)獨(dú)立app,只是發(fā)布模式下變成library,那么插件化就是不存在發(fā)布模式開發(fā)模式,每個(gè)組件業(yè)務(wù)就是一個(gè)獨(dú)立apk開發(fā),然后通過主工程app動(dòng)態(tài)加載部署業(yè)務(wù)組件apk。

插件化帶來的好處:
1、業(yè)務(wù)組件解耦合,能夠?qū)崿F(xiàn)業(yè)務(wù)組件熱拔插。
2、更改了公司開發(fā)的迭代模式,從以前以整個(gè)app發(fā)版流程更改為app發(fā)版和業(yè)務(wù)插件發(fā)版流程。
3、實(shí)現(xiàn)了用戶不需要更新app版本完成bug修復(fù)和業(yè)務(wù)組件升級(jí)等熱修復(fù)功能。
4、組件化有的好處插件化都有,因?yàn)椴寮⒃诮M件化架構(gòu)之上。
  • 1
  • 2
  • 3
  • 4
  • 5

那么插件化帶來都是好處,有沒有缺點(diǎn)呢?現(xiàn)在很多各式各樣的開源插件框架都有各自優(yōu)點(diǎn)和缺點(diǎn);接下來我們還是一問題的形式來解決這樣問題。

首先我們來了解一下插件化實(shí)現(xiàn)的原理,由于插件化原理涵蓋內(nèi)容太多這里只是介紹一下核心內(nèi)容;我們了解一下app打包過程。請(qǐng)看下圖:
這里寫圖片描述
上面是android打包形成apk的一個(gè)過程,可以發(fā)現(xiàn)android開發(fā)主要的部分是整合編譯代碼、整合編譯資源,然后就是安全簽名保證apk完整性。我們?cè)倏匆粡垐D:
這里寫圖片描述

上面是一個(gè)apk解壓之后的文件,可以看出,里面幾個(gè)比較重要的部分:
1、dex文件java編譯之后的代碼文件。
2、app中使用資源文件。
3、so文件動(dòng)態(tài)鏈接庫。
  • 1
  • 2
  • 3
  • 4

而插件化動(dòng)態(tài)加載部署這些內(nèi)容。加載上面的內(nèi)容就產(chǎn)生幾個(gè)技術(shù)問題:dex文件加載、資源文件加載、so文件加載部署、activity、service等android組件的動(dòng)態(tài)注冊(cè)。

首先是dex文件加載:

    public static DexClassLoader readDexFile(Context context, String apkPath, String outDexPath){
        DexClassLoader dexClassLoader=null;
        try {
            dexClassLoader=new DexClassLoader(apkPath, getOutDexpaPath(context, outDexPath), context.getApplicationInfo().nativeLibraryDir, context.getClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
            Log.i(tag,""+e.getMessage());
        }
        return dexClassLoader;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

夾在apk中dex就是通過DexClassLoader api來加載的,通過DexClassLoader就可以調(diào)用apk中java代碼,接下來就是通過反射機(jī)制去替換app的ClassLoader,這一步步驟對(duì)android framework源碼依賴非常大,然后通過ClassLoader的雙親機(jī)制將主工程app的ClassLoader設(shè)置成父級(jí)ClassLoader,這樣加載dex步驟就完成了,具體實(shí)現(xiàn)技術(shù)篇幅太大以后有空會(huì)專門出一篇文章。

然后是加載apk中資源文件:

    public static Resources readApkRes(Context context, String apkPath){
        Resources resources1=null;
        try {
            AssetManager assetManager=AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            addAssetPath.invoke(assetManager, apkPath);
            Resources resources = context.getResources();
            resources1 = new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
            Log.i(tag,""+e.getMessage());
        }
        return resources1;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

通過上面的方法可以加載出資源內(nèi)容,接下來也是通過反射替換app默認(rèn)夾在的mResources對(duì)象,這樣資源加載完成。

上面比較核心的功能思路,由于篇幅還有很多細(xì)節(jié)這里不能夠全部列出,比如so文件加載部署、activity、service等android組件的動(dòng)態(tài)注冊(cè)都是非常依賴源碼的操作,都是要使用大量的反射來修改系統(tǒng)的成員變量等,所以其實(shí)插件化實(shí)施起來最大的困難就是適配機(jī)型的源碼,所有成本就在這里。那么我們?cè)摬辉撚貌寮??首先根?jù)公司項(xiàng)目實(shí)際情況認(rèn)定,需不需要插件化提供的熱更新功能,如果需要可以選擇插件化。接下來我們來看看市面上插件化框架對(duì)比!
這里寫圖片描述

每一款框架都是自己優(yōu)點(diǎn)和缺點(diǎn),但是似乎都不能夠滿足所有功能,這里我總結(jié)一下什么時(shí)候應(yīng)該用到插件化,首先不是所有地方都是插件化而是局部使用,符合一下條件可以考慮使用:
1、項(xiàng)目一些偏向ui界面具有更新要求快的模塊可以使用,例如一些出銷頁面更新較快的地方。
2、activity為主,大部分框架對(duì)activity支持較好。
3、對(duì)so等第三方庫依賴較少,so對(duì)插件化兼容性穩(wěn)定考驗(yàn)比較大。
4、沒有使用aop切面編程的代碼。
  • 1
  • 2
  • 3
  • 4
  • 5

speed-tools插件化框架使用

這里自己寫了一個(gè)對(duì)源碼侵入性小的插件化框架speed tools。

github: speed-tools
可以的話可以 star 一下 ^-^

首先看看項(xiàng)目結(jié)構(gòu):

這里寫圖片描述

lib_speed_tools: 插件化核心功能library
module_host_main:宿主工程主工程,負(fù)責(zé)加載部署apk
module_client_one:測(cè)試業(yè)務(wù)apk 1
module_client_two:測(cè)試業(yè)務(wù)apk 2
lib_img_utils:測(cè)試imageloader圖片框架

注意:需要使用speed tools 只需要依賴lib_speed_tools即可,然后開始配置插件化步驟:

首先在module_client_one中創(chuàng)建業(yè)務(wù)邏輯類:TestClass.java

/**
 *  by liyihang
 */
public class TestClass extends SpeedBaseInterfaceImp {

    private Activity activity;

    @Override
    public void onCreate(Bundle savedInstanceState, final Activity activity) {
        this.activity=activity;

        activity.setContentView(R.layout.activity_client_main);

        activity.findViewById(R.id.jump).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SpeedUtils.goActivity(activity,"first_apk", "two_class");
            }
        });

        ImageView imageView= (ImageView) activity.findViewById(R.id.img_view);
        imageView.setVisibility(View.VISIBLE);
        ImgUtils.getInstance(activity).showImg("http://img.my.csdn.net/uploads/201309/01/1378037235_3453.jpg", imageView);

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

SpeedBaseInterfaceImp業(yè)務(wù)組件中業(yè)務(wù)activity代理類,他是實(shí)現(xiàn)了主要的生命周期方法,相當(dāng)于組件的activity類。

然后創(chuàng)建hock類每個(gè)業(yè)務(wù)組件中只創(chuàng)建一個(gè):ClientMainActivity.java

public class ClientMainActivity extends SpeedClientBaseActivity {

    @Override
    public SpeedBaseInterface getProxyBase() {
        return new TestClass();
    }


}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

這個(gè)類在組件中是唯一的,作用就是hock在獨(dú)立測(cè)試時(shí)候使用。

接下來配置配置組件的AndroidManifest.xml

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/SpeedTheme">

        <!--必須設(shè)置root_class-->
        <meta-data
            android:name="root_class"
            android:value="com.example.clientdome.TestClass" />

        <meta-data
            android:name="two_class"
            android:value="com.example.clientdome.TwoClass" />

        <activity
            android:name=".ClientMainActivity"
            android:theme="@style/SpeedTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <!--組件意圖-->
            <intent-filter>
                <data android:scheme="speed_tools" android:host="sijienet.com" android:path="/find_class"/>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>
    </application>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

組件意圖寫死保持一直,root_class 是調(diào)用死后使用對(duì)于配置的com.example.clientdome.TestClass業(yè)務(wù)類。這樣業(yè)務(wù)組件配置完成。

接下來配置宿主工程module_host_main;

創(chuàng)建宿主工程唯一hock類:ApkActivity.java

/**
 *  by liyihang
 */
public class ApkActivity extends SpeedHostBaseActivity {


    @Override
    public String getApkKeyName() {
        return HostMainActivity.FIRST_APK_KEY;
    }

    @Override
    public String getClassTag() {
        return null;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

整個(gè)宿主工程創(chuàng)建一個(gè)類即可,用戶是hock activity;然后創(chuàng)建一個(gè)開屏頁apk第一次加載時(shí)候需要一些時(shí)間,放入開屏等待頁面是非常合適的。

HostMainActivity.java

/**
 *  by liyihang
 */
public class HostMainActivity extends AppCompatActivity implements Runnable,Handler.Callback, View.OnClickListener {


    public static final String FIRST_APK_KEY="first_apk";
    public static final String TWO_APK_KEY="other_apk";

    private Handler handler;

    private TextView showFont;
    private ProgressBar progressBar;
    private Button openOneApk;
    private Button openTwoApk;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_host_main);

        showFont= (TextView) findViewById(R.id.show_font);
        progressBar= (ProgressBar) findViewById(R.id.progressbar);
        openOneApk= (Button) findViewById(R.id.open_one_apk);
        openTwoApk= (Button) findViewById(R.id.open_two_apk);

        handler=new Handler(this);
        new Thread(this).start();
    }

    @Override
    public void run() {
        String s = "module_client_one-release.apk";
        String dexOutPath="dex_output2";
        File nativeApkPath = SpeedUtils.getNativeApkPath(getApplicationContext(), s);
        SpeedApkManager.getInstance().loadApk(FIRST_APK_KEY, nativeApkPath.getAbsolutePath(), dexOutPath, this);

        String s2 = "module_client_two-release.apk";
        String dexOutPath2="dex_output3";
        File nativeApkPath1 = SpeedUtils.getNativeApkPath(getApplicationContext(), s2);
        SpeedApkManager.getInstance().loadApk(TWO_APK_KEY, nativeApkPath1.getAbsolutePath(), dexOutPath2, this);

        handler.sendEmptyMessage(0x78);
    }

    @Override
    public boolean handleMessage(Message message) {
        showFont.setText("當(dāng)前是主宿主apk\n插件apk完畢");
        progressBar.setVisibility(View.GONE);
        openOneApk.setVisibility(View.VISIBLE);
        openTwoApk.setVisibility(View.VISIBLE);
        openOneApk.setOnClickListener(this);
        openTwoApk.setOnClickListener(this);
        return false;
    }

    @Override
    public void onClick(View v) {
        if (v.getId()==R.id.open_one_apk)
        {
            SpeedUtils.goActivity(this, FIRST_APK_KEY, null);
        }
        if (v.getId()==R.id.open_two_apk)
        {
            SpeedUtils.goActivity(this, TWO_APK_KEY, null);
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

加載apk核心代碼是:

        String s = "module_client_one-release.apk";
        String dexOutPath="dex_output2";
        File nativeApkPath = SpeedUtils.getNativeApkPath(getApplicationContext(), s);
        SpeedApkManager.getInstance().loadApk(FIRST_APK_KEY, nativeApkPath.getAbsolutePath(), dexOutPath, this);
  • 1
  • 2
  • 3
  • 4

業(yè)務(wù)apk都是放在assets目錄中。最后配置AndroidManifest.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas./apk/res/android"
    package="com.example.hostproject">


    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>


    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/SpeedTheme">

        <!--啟動(dòng)activity 加載apk-->
        <activity android:name=".HostMainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>


        <!--組件hack-->
        <activity
            android:name=".ApkActivity"
            android:label="@string/app_name"
            android:theme="@style/SpeedTheme" >
            <intent-filter>
                <data android:scheme="speed_tools" android:host="jason.com" android:path="/find_class"/>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>
    </application>

</manifest>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

這樣所有配置結(jié)束,插件化實(shí)現(xiàn)。

總結(jié):

1、插件化在項(xiàng)目中還是加入很多機(jī)制,如果主工程下載更新機(jī)制,配合后臺(tái)區(qū)分用戶版本發(fā)布能夠支持的業(yè)務(wù)插件等,這些東西加起來是個(gè)龐大的工程;

2、現(xiàn)在市面上很多成熟的插件框架都android framework依賴還是很重,但也有侵入性小的框架,例如speed tools。

3、插件架構(gòu)不是全局使用就好,而是根據(jù)自己的需要結(jié)合實(shí)際需求來使用,常更新有不滿足html5提供用戶體驗(yàn)的情況比較合適。

上面是對(duì)項(xiàng)目中插件化開發(fā)積累的經(jīng)驗(yàn),如果有建議可以給我留言。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多

    国产视频在线一区二区| 扒开腿狂躁女人爽出白浆av| 黑鬼糟蹋少妇资源在线观看| 欧美日韩有码一二三区| 亚洲欧美日产综合在线网 | 久久午夜福利精品日韩| 国产欧美日韩综合精品二区| 欧美日韩国产成人高潮| 青青操日老女人的穴穴| 亚洲午夜福利不卡片在线| 日韩一区二区三区观看| 少妇人妻一级片一区二区三区| 亚洲精品黄色片中文字幕| 日本99精品在线观看| 精品人妻av区波多野结依| 亚洲熟女一区二区三四区| 亚洲深夜精品福利一区| 日本黄色美女日本黄色| 亚洲一区二区三区一区| 国产欧美日韩一级小黄片| 久久大香蕉精品在线观看| 国产亚洲成av人在线观看| 免费高清欧美一区二区视频| 久久热中文字幕在线视频| 黄片三级免费在线观看| 精品日韩中文字幕视频在线| 东北女人的逼操的舒服吗| 福利在线午夜绝顶三级| 日韩av亚洲一区二区三区| 夫妻性生活真人动作视频| 91亚洲人人在字幕国产| 日韩人妻av中文字幕| 中文文精品字幕一区二区| 午夜视频免费观看成人| 国产中文另类天堂二区| 在线免费国产一区二区| 亚洲国产性感美女视频| 激情亚洲内射一区二区三区| 欧美日本道一区二区三区| 午夜视频成人在线观看| 欧美成人黄色一区二区三区|