終極組件化框架項目方案詳解

<pre>

目錄

1.什么是組件化霍转?
2.為什么需要組件化和組件化帶來的好處极阅?
3.組件化的基本框架
4.組件化框架的具體實現(xiàn)
4.1.基類庫的封裝
4.2組件模式和集成模式切換的實現(xiàn)
4.3第三方開源庫和組件版本號的管理
4.4.組件間通信實現(xiàn)
5.組件合并時res資源和AndroidManifest配置的問題
6.組件全局application的實現(xiàn)和數(shù)據(jù)的初始化
7.組件內(nèi)網(wǎng)絡請求和攔截器的實現(xiàn)
8.組件化實現(xiàn)的技術(shù)難點
8.1.greendao數(shù)據(jù)庫在組件內(nèi)的實現(xiàn)
8.2.資源命名沖突
8.3.butterKnife不能使用的原因
9.組件化與熱修復的無縫連接
10.結(jié)束語

前言

本文所講的組件化案例是基于自己開源的組件化框架項目

github上地址https://github.com/HelloChenJinJun/NewFastFrame

其中即時通訊(Chat)模塊是單獨的項目

github上地址https://github.com/HelloChenJinJun/TestChat

1.什么是組件化答憔?

項目發(fā)展到一定階段時攘已,隨著需求的增加以及頻繁地變更女蜈,項目會越來越大验烧,代碼變得越來越臃腫板驳,耦合會越來越多,開發(fā)效率也會降低碍拆,這個時候我們就需要對舊項目進行重構(gòu)即模塊的拆分若治,官方的說法就是組件化慨蓝。

2.為什么需要組件化和組件化帶來的好處?

1端幼、 現(xiàn)在Android項目中代碼量達到一定程度礼烈,編譯將是一件非常痛苦的事情,一般都需要變異5到6分鐘婆跑。Android studio推出instant run由于各種缺陷和限制條件(比如采用熱修復tinker)一般情況下是被關(guān)閉的此熬。而組件化框架可以使模塊單獨編譯調(diào)試,可以有效地減少編譯的時間滑进。

2犀忱、通過組件化可以更好的進行并行開發(fā),因為我們可以為每一個模塊進行單獨的版本控制郊供,甚至每一個模塊的負責人可以選擇自己的設(shè)計架構(gòu)而不影響其他模塊的開發(fā)峡碉,與此同時組件化還可以避免模塊之間的交叉依賴,每一個模塊的開發(fā)人員可以對自己的模塊進行獨立測試驮审,獨立編譯和運行鲫寄,甚至可以實現(xiàn)單獨的部署。從而極大的提高了并行開發(fā)效率疯淫。

3.組件化的基本框架

[圖片上傳失敗...(image-bf1f7e-1514268060021)]

[圖片上傳失敗...(image-424472-1514268060021)]

4.組件化框架的具體實現(xiàn)

4.1地来、基類庫的封裝

[圖片上傳失敗...(image-add815-1514268060021)]

基類庫中主要包括開發(fā)常用的一些框架。

1熙掺、網(wǎng)絡請求(多任務下載和上傳判族,采用Retrofit+RxJava框架)

2雀瓢、圖片加載(策略模式,Glide與Picasso之間可以切換)

3、通信機制(RxBus)

4各薇、基類adapter的封裝(支持item動畫渡冻、多布局item踊兜、下拉和加載更多犹赖、item點擊事件)

5、基類RecyclerView的封裝(支持原生風格的下拉加載董瞻,item側(cè)滑等)

6寞蚌、mvp框架

7、各組件的數(shù)據(jù)庫實體類

8钠糊、通用的工具類

9挟秤、自定義view(包括對話框,ToolBar布局抄伍,圓形圖片等view的自定義)

10艘刚、dagger的封裝(用于初始化全局的變量和網(wǎng)絡請求等配置)

等等

4.2組件模式和集成模式切換的實現(xiàn)

music組件下的build.gradle文件,其他組件類似截珍。

//控制組件模式和集成模式
if (rootProject.ext.isAlone) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
apply plugin: 'com.neenbedankt.android-apt'
android {
    compileSdkVersion rootProject.ext.android.compileSdkVersion
    buildToolsVersion rootProject.ext.android.buildToolsVersion
    defaultConfig {
        if (rootProject.ext.isAlone) {
     //   組件模式下設(shè)置applicationId
            applicationId "com.example.cootek.music"
        }
        minSdkVersion rootProject.ext.android.minSdkVersion
        targetSdkVersion rootProject.ext.android.targetSdkVersion
        versionCode rootProject.ext.android.versionCode
        versionName rootProject.ext.android.versionName
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        if (!rootProject.ext.isAlone) {
//   集成模式下Arouter的配置昔脯,用于組件間通信的實現(xiàn)
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [moduleName: project.getName()]
                }
            }
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }
    sourceSets {
        main {
    //控制兩種模式下的資源和代碼配置情況
            if (rootProject.ext.isAlone) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
                java.srcDirs = ['src/main/java', 'src/main/module/java']
                res.srcDirs = ['src/main/res', 'src/main/module/res']
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
}
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
//   依賴基類庫
    compile project(':commonlibrary')
//用作顏色選擇器
    compile 'com.afollestad.material-dialogs:commons:0.9.1.0'
    apt rootProject.ext.dependencies.dagger2_compiler
    if (!rootProject.ext.isAlone) {
//  集成模式下需要編譯器生成路由通信的代碼
        apt rootProject.ext.dependencies.arouter_compiler
    }
    testCompile 'junit:junit:4.12'
}

集成模式

1啄糙、首先需要在config,gradle文件中設(shè)置isAlone=false

ext {
    isAlone = false;//false:作為Lib組件存在, true:作為application存在

2云稚、然后Sync 下。

3沈堡、最后選擇app運行即可静陈。

[圖片上傳失敗...(image-d84731-1514268060021)]

組件模式

1、首先需要在config,gradle文件中設(shè)置isAlone=true

ext {
    isAlone = true;//false:作為Lib組件存在诞丽, true:作為application存在

2鲸拥、然后Sync 下。

3僧免、最后相應的模塊(new刑赶、chat、live懂衩、music撞叨、app)進行運行即可。

4.3第三方開源庫和組件版本號的管理

config.gradle文件的配置情況

ext {
    isAlone = false;//false:作為集成模式存在浊洞, true:作為組件模式存在

//  各個組件版本號的統(tǒng)一管理
    android = [
            compileSdkVersion: 24,
            buildToolsVersion: "25.0.2",
            minSdkVersion    : 16,
            targetSdkVersion : 22,
            versionCode      : 1,
            versionName      : '1.0.0',
    ]

    libsVersion = [
            // 第三方庫版本號的管理
            supportLibraryVersion = "25.3.0",
            retrofitVersion = "2.1.0",
            glideVersion = "3.7.0",
            loggerVersion = "1.15",
//            eventbusVersion = "3.0.0",
            gsonVersion = "2.8.0",
            butterknife = "8.8.0",
            retrofit = "2.3.0",
            rxjava = "2.1.1",
            rxjava_android = "2.0.1",
            rxlifecycle = "2.1.0",
            rxlifecycle_components = "2.1.0",
            dagger_compiler = "2.11",
            dagger = "2.11",
            greenDao = "3.2.2",
            arouter_api = "1.2.2",
            arouter_compiler = "1.1.3",
            transformations = "2.0.2",
            rxjava_adapter = "2.3.0",
            gson_converter = "2.3.0",
            scalars_converter = "2.3.0",
            rxpermission = "0.9.4",
            eventbus="3.0.0",
            support_v4="25.4.0",
            okhttp3="3.8.1"
    ]

//  依賴庫管理
    dependencies = [
            appcompatV7               : "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion",
            design                    : "com.android.support:design:$rootProject.supportLibraryVersion",
            cardview                  : "com.android.support:cardview-v7:$rootProject.supportLibraryVersion",
            palette                   : "com.android.support:palette-v7:$rootProject.supportLibraryVersion",
            recycleview               : "com.android.support:recyclerview-v7:$rootProject.supportLibraryVersion",
            support_v4                : "com.android.support:support-v4:$rootProject.support_v4",
            annotations               : "com.android.support:support-annotations:$rootProject.supportLibraryVersion",
            eventBus                  : "org.greenrobot:eventbus:$rootProject.eventbus",
            glide                     : "com.github.bumptech.glide:glide:$rootProject.glideVersion",
            gson                      : "com.google.code.gson:gson:$rootProject.gsonVersion",
            logger                    : "com.orhanobut:logger:$rootProject.loggerVersion",
            butterknife               : "com.jakewharton:butterknife:$rootProject.butterknife",
            butterknife_compiler      : "com.jakewharton:butterknife-compiler:$rootProject.butterknife",
            retrofit                  : "com.squareup.retrofit2:retrofit:$rootProject.retrofit",
            okhttp3                   : "com.squareup.okhttp3:okhttp:$rootProject.retrofit",
            retrofit_adapter_rxjava2  : "com.squareup.retrofit2:adapter-rxjava2:$rootProject.rxjava_adapter",
            retrofit_converter_gson   : "com.squareup.retrofit2:converter-gson:$rootProject.gson_converter",
            retrofit_converter_scalars: "com.squareup.retrofit2:converter-scalars:$rootProject.scalars_converter",
            rxpermission              : "com.tbruyelle.rxpermissions2:rxpermissions:$rootProject.rxpermission@aar",
            rxjava2                   : "io.reactivex.rxjava2:rxjava:$rootProject.rxjava",
            rxjava2_android           : "io.reactivex.rxjava2:rxandroid:$rootProject.rxjava_android",
            rxlifecycle2              : "com.trello.rxlifecycle2:rxlifecycle:$rootProject.rxlifecycle",
            rxlifecycle2_components   : "com.trello.rxlifecycle2:rxlifecycle-components:$rootProject.rxlifecycle_components",
            dagger2_compiler          : "com.google.dagger:dagger-compiler:$rootProject.dagger_compiler",
            dagger2                   : "com.google.dagger:dagger:$rootProject.dagger",
            greenDao                  : "org.greenrobot:greendao:$rootProject.greenDao",
            transformations           : "jp.wasabeef:glide-transformations:$rootProject.transformations",
//路由通訊
            arouter_api               : "com.alibaba:arouter-api:$rootProject.arouter_api",
            arouter_compiler          : "com.alibaba:arouter-compiler:$rootProject.arouter_compiler"
    ]
}

4.4牵敷、組件間通信實現(xiàn)

組件間通信的實現(xiàn)是采用阿里開源的Arouter路由通信。

github地址:https://github.com/alibaba/ARouter

在App工程中法希,初始化組件通信數(shù)據(jù)

private List<MainItemBean> getDefaultData() {
        List<MainItemBean> result=new ArrayList<>();
        MainItemBean mainItemBean=new MainItemBean();
        mainItemBean.setName("校園");
        mainItemBean.setPath("/news/main");
        mainItemBean.setResId(R.mipmap.ic_launcher);
        MainItemBean music=new MainItemBean();
        music.setName("音樂");
        music.setResId(R.mipmap.ic_launcher);
        music.setPath("/music/main");
        MainItemBean live=new MainItemBean();
        live.setName("直播");
        live.setResId(R.mipmap.ic_launcher);
        live.setPath("/live/main");
        MainItemBean chat=new MainItemBean();
        chat.setName("聊天");
        chat.setPath("/chat/splash");
        chat.setResId(R.mipmap.ic_launcher);
        result.add(mainItemBean);
        result.add(music);
        result.add(live);
        result.add(chat);
        return result;
    }

然后在設(shè)置每個item的點擊事件時,啟動組件界面跳轉(zhuǎn)枷餐。

@Override
            public void onItemClick(int position, View view) {
                MainItemBean item=mainAdapter.getData(position);
                ARouter.getInstance().build(item.getPath()).navigation();
            }

每個組件入口界面的設(shè)置(比如直播Live組件,其它組件類似)

@Route(path = "/live/main")
public class MainActivity extends BaseActivity<List<CategoryLiveBean>, MainPresenter> implements View.OnClickListener {

5.組件合并時res資源和AndroidManifest配置的問題

我們通過判斷組件處于哪種模式來動態(tài)設(shè)置項目res資源和Manifest苫亦、以及代碼的位置毛肋。以直播組件為例,其它組件類似屋剑。

[圖片上傳失敗...(image-6955db-1514268060021)]

直播組件的build.gradle文件對代碼資源等位置的配置

sourceSets {
        main {
            if (rootProject.ext.isAlone) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
                java.srcDirs = ['src/main/java', 'src/main/module/java']
                res.srcDirs = ['src/main/res', 'src/main/module/res']
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }

6.組件全局application的實現(xiàn)和數(shù)據(jù)的初始化

采用類似于Glide在Manifest初始化配置的方式來初始化各個組件的Application润匙,以直播組件為例,其它類似饼丘。

在BaseApplication中趁桃,初始化ApplicationDelegate代理類

 @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        applicationDelegate = new ApplicationDelegate();
        applicationDelegate.attachBaseContext(base);
        MultiDex.install(this);
    }

ApplicationDelegate內(nèi)部是怎樣的呢?繼續(xù)看下去

public class ApplicationDelegate implements IAppLife {
    private List<IModuleConfig> list;
    private List<IAppLife> appLifes;
    private List<Application.ActivityLifecycleCallbacks> liferecycleCallbacks;

    public ApplicationDelegate() {
        appLifes = new ArrayList<>();
        liferecycleCallbacks = new ArrayList<>();
    }

    @Override
    public void attachBaseContext(Context base) {
//   初始化Manifest文件解析器肄鸽,用于解析組件在自己的Manifest文件配置的Application
        ManifestParser manifestParser = new ManifestParser(base);
        list = manifestParser.parse();
//解析得到的組件Application列表之后卫病,給每個組件Application注入
context,和Application的生命周期的回調(diào)典徘,用于實現(xiàn)application的同步
        if (list != null && list.size() > 0) {
            for (IModuleConfig configModule :
                    list) {
                configModule.injectAppLifecycle(base, appLifes);
                configModule.injectActivityLifecycle(base, liferecycleCallbacks);
            }
        }
        if (appLifes != null && appLifes.size() > 0) {
            for (IAppLife life :
                    appLifes) {
                life.attachBaseContext(base);
            }
        }
    }

    @Override
    public void onCreate(Application application) {
//  相應調(diào)用組件Application代理類的onCreate方法
        if (appLifes != null && appLifes.size() > 0) {
            for (IAppLife life :
                    appLifes) {
                life.onCreate(application);
            }
        }
        if (liferecycleCallbacks != null && liferecycleCallbacks.size() > 0) {
            for (Application.ActivityLifecycleCallbacks life :
                    liferecycleCallbacks) {
                application.registerActivityLifecycleCallbacks(life);
            }
        }
    }

    @Override
    public void onTerminate(Application application) {
//  相應調(diào)用組件Application代理類的onTerminate方法
        if (appLifes != null && appLifes.size() > 0) {
            for (IAppLife life :
                    appLifes) {
                life.onTerminate(application);
            }
        }
        if (liferecycleCallbacks != null && liferecycleCallbacks.size() > 0) {
            for (Application.ActivityLifecycleCallbacks life :
                    liferecycleCallbacks) {
                application.unregisterActivityLifecycleCallbacks(life);
            }
        }
    }
}

組件Manifest中application的全局配置

<meta-data
            android:name="com.example.live.LiveApplication"
            android:value="IModuleConfig" />

ManifestParser會對其中value為IModuleConfig的meta-data進行解析蟀苛,并通過反射生成實例。

public final class ManifestParser {
    private static final String MODULE_VALUE = "IModuleConfig";
    private final Context context;
    public ManifestParser(Context context) {
        this.context = context;
    }
    public List<IModuleConfig> parse() {
        List<IModuleConfig> modules = new ArrayList<>();
        try {
            ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(
                    context.getPackageName(), PackageManager.GET_META_DATA);
            if (appInfo.metaData != null) {
                for (String key : appInfo.metaData.keySet()) {
//會對其中value為IModuleConfig的meta-data進行解析逮诲,并通過反射生成實例
                    if (MODULE_VALUE.equals(appInfo.metaData.get(key))) {
                        modules.add(parseModule(key));
                    }
                }
            }
        } catch (PackageManager.NameNotFoundException e) {
            throw new RuntimeException("Unable to find metadata to parse IModuleConfig", e);
        }
        return modules;
    }

//通過類名生成實例
    private static IModuleConfig parseModule(String className) {
        Class<?> clazz;
        try {
            clazz = Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("Unable to find IModuleConfig implementation", e);
        }

        Object module;
        try {
            module = clazz.newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException("Unable to instantiate IModuleConfig implementation for " + clazz, e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Unable to instantiate IModuleConfig implementation for " + clazz, e);
        }

        if (!(module instanceof IModuleConfig)) {
            throw new RuntimeException("Expected instanceof IModuleConfig, but found: " + module);
        }
        return (IModuleConfig) module;
    }

這樣通過以上步驟就可以在Manifest文件中配置自己組件的Application帜平,用于初始化組件內(nèi)的數(shù)據(jù)幽告,比如在直播組件中初始化Dagger的全局配置

public class LiveApplication implements IModuleConfig,IAppLife {
    private static MainComponent mainComponent;
    @Override
    public void injectAppLifecycle(Context context, List<IAppLife> iAppLifes) {
//  這里需要把本引用添加到Application的生命周期的回調(diào)中,以便實現(xiàn)回調(diào)
        iAppLifes.add(this);
    }

    @Override
    public void injectActivityLifecycle(Context context, List<Application.ActivityLifecycleCallbacks> lifecycleCallbackses) {
    }

    @Override
    public void attachBaseContext(Context base) {
    }

    @Override
    public void onCreate(Application application) {
//     在onCreate方法中對Dagger進行初始化
            mainComponent= DaggerMainComponent.builder().mainModule(new MainModule()).appComponent(BaseApplication.getAppComponent()).build();
    }

    @Override
    public void onTerminate(Application application) {
        if (mainComponent != null) {
            mainComponent = null;
        }
    }

    public static MainComponent getMainComponent() {
        return mainComponent;
    }
}

7.組件內(nèi)網(wǎng)絡請求和攔截器的實現(xiàn)

由于每個組件的BaseUrl和網(wǎng)絡配置等可能不一樣裆甩,所以每個組件可以在自己配置的dagger中的 MainConponent實現(xiàn)自己的網(wǎng)絡請求和攔截器冗锁。

以直播組件為例,其它類似嗤栓。

MainComponent

@PerApplication
@Component(dependencies = AppComponent.class, modules = MainModule.class)
public interface MainComponent {
    public DaoSession getDaoSession();

    public MainRepositoryManager getMainRepositoryManager();
}

MainModule代碼

@Module
public class MainModule {
    @Provides
    @PerApplication
    public MainRepositoryManager provideRepositoryManager(@Named("live") Retrofit retrofit, DaoSession daoSession) {
        return new MainRepositoryManager(retrofit, daoSession);
    }
    @Provides
    @Named("live")
    @PerApplication
    public Retrofit provideRetrofit(@Named("live") OkHttpClient okHttpClient,@Nullable Gson gson){
        Retrofit.Builder builder=new Retrofit.Builder().baseUrl(LiveUtil.BASE_URL).addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(gson)).client(okHttpClient);
        return builder.build();
    }
    @Provides
    @Named("live")
    @PerApplication
    public OkHttpClient provideOkHttpClient(@Named("live")LiveInterceptor interceptor){
        OkHttpClient.Builder builder=new OkHttpClient.Builder();
        builder.connectTimeout(10, TimeUnit.SECONDS).readTimeout(10,TimeUnit.SECONDS);
        builder.addInterceptor(interceptor);
        return builder.build();
    }
    @Provides
    @Named("live")
    @PerApplication
    public LiveInterceptor provideNewsInterceptor(){
        return new LiveInterceptor();
    }
}

8.組件化實現(xiàn)的技術(shù)難點

8.1.greendao數(shù)據(jù)庫的實現(xiàn)

greendao數(shù)據(jù)庫初始化代碼冻河,在基類庫的NetClientModule.java中

public DaoSession provideDaoSession(Application application) {
        DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(application, "common_library_db", null);
        Database database = devOpenHelper.getWritableDb();
        DaoMaster master = new DaoMaster(database);
        return master.newSession();
    }

其中的DaoMaster是通過APT生成的,由于DaoMaster給全局的組件使用茉帅,所以只能將greendao 數(shù)據(jù)庫放在基類庫中叨叙,并且各個組件的實體類bean的創(chuàng)建也只能在基類庫中進行,以分包命名進行區(qū)分堪澎,如下圖擂错。因為如果在組件內(nèi)創(chuàng)建bean 會重新生成另一個副本DaoMaster并且不能操控其他組件的數(shù)據(jù)庫實體,有很大的局限性樱蛤。

[圖片上傳失敗...(image-b9fd79-1514268060021)]

8.2.資源命名沖突

官方說法是在每個module的build.gradle文件中配置資源文件名前綴

這種方法缺點就是钮呀,所有的資源名必須要以指定的字符串(moudle_prefix)做前綴,否則會異常報錯刹悴,而且這方法只限定xml里面的資源行楞,對圖片資源并不起作用,所以圖片資源仍然需要手動去修改資源名土匀。

所以不是很推薦使用這種方法來解決資源名沖突子房。所以只能自己注意點,在創(chuàng)建資源的時候就轧,盡量不讓其重復证杭。

resourcePrefix  "moudle_prefix"

8.3.butterKnife不能使用的原因

雖然Butterknife支持在lib中使用,但是條件是用 R2 代替 R 妒御,在組件模式和集成模式的切換中解愤,R2<->R之間的切換是無法完成轉(zhuǎn)換的,切換一次要改動全身乎莉,是非常麻煩的送讲!所以不推薦在組件化中使用Butterknife。

8.4.library重復依賴問題

1惋啃、可能大家會認為哼鬓,每個組件都依賴基類庫,基類庫library次不是重復依賴了边灭?其實并不會存在這樣的問題异希,因為在構(gòu)建APP的過程中Gradle會自動將重復的arr包排除,也就不會存在重復依賴基類庫的情況绒瘦。

2称簿、但是第三方開源庫依賴的包可能會與我們自己引用的包重復扣癣,所以我們需要將多余的包給排除出去。

基類庫(CommonLibrary)中build.gradle

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile(rootProject.ext.dependencies.appcompatV7) {
        exclude module: "support-v4"
        exclude module: "support-annotations"
    }
    compile rootProject.ext.dependencies.recycleview
    compile rootProject.ext.dependencies.design

    compile(rootProject.ext.dependencies.support_v4) {
       exclude module: "support-annotations"
    }
    compile rootProject.ext.dependencies.annotations
    compile(rootProject.ext.dependencies.butterknife) {
        exclude module: 'support-annotations'
    }
    compile rootProject.ext.dependencies.rxjava2
    compile(rootProject.ext.dependencies.rxjava2_android) {
        exclude module: "rxjava"
    }
    compile(rootProject.ext.dependencies.rxlifecycle2) {
        exclude module: 'rxjava'
        exclude module: 'jsr305'
    }
    compile(rootProject.ext.dependencies.rxlifecycle2_components) {
        exclude module: 'support-v4'
        exclude module: 'appcompat-v7'
        exclude module: 'support-annotations'
        exclude module: 'rxjava'
        exclude module: 'rxandroid'
        exclude module: 'rxlifecycle'
    }
    compile(rootProject.ext.dependencies.retrofit) {
        exclude module: 'okhttp'
        exclude module: 'okio'
    }
    compile(rootProject.ext.dependencies.retrofit_converter_gson) {
        exclude module: 'gson'
        exclude module: 'okhttp'
        exclude module: 'okio'
        exclude module: 'retrofit'
    }
    compile(rootProject.ext.dependencies.retrofit_adapter_rxjava2) {
        exclude module: 'rxjava'
        exclude module: 'okhttp'
        exclude module: 'retrofit'
        exclude module: 'okio'
    }
    compile rootProject.ext.dependencies.greenDao
    compile rootProject.ext.dependencies.okhttp3
    compile rootProject.ext.dependencies.gson
    compile rootProject.ext.dependencies.glide
    compile rootProject.ext.dependencies.eventBus
    compile rootProject.ext.dependencies.dagger2
    compile(rootProject.ext.dependencies.rxpermission) {
        exclude module: 'rxjava'
    }
    compile rootProject.ext.dependencies.retrofit_converter_scalars
    annotationProcessor rootProject.ext.dependencies.dagger2_compiler
    annotationProcessor rootProject.ext.dependencies.butterknife_compiler
    compile rootProject.ext.dependencies.butterknife
    compile rootProject.ext.dependencies.transformations
    compile rootProject.ext.dependencies.arouter_api
}

9.組件化與熱修復的無縫連接

本開源項目是基于騰訊的bugly平臺憨降,用于監(jiān)控異常信息父虑、熱修復和應用升級。

具體實現(xiàn):

1券册、在工程的根目錄build.gradle配置

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath "com.tencent.bugly:tinker-support:1.0.8"
    }
}

然后在App 的build.gradle進行以下配置

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    if (!rootProject.ext.isAlone) {
        compile project(':chat')
        compile project(':music')
        compile project(':news')
        compile project(':live')
        apt rootProject.ext.dependencies.arouter_compiler
    } else {
        compile project(':commonlibrary')
    }
    testCompile 'junit:junit:4.12'
//  依賴bugly相關(guān)SDK
    compile 'com.tencent.bugly:crashreport_upgrade:1.3.1'
    compile 'com.tencent.bugly:nativecrashreport:latest.release'
}
apply from: 'tinker-support.gradle'

然后依賴其中的插件腳本

apply from: 'tinker-support.gradle'

其中的tinker-support.gradle文件如下:

apply plugin: 'com.tencent.bugly.tinker-support'
def bakPath = file("${buildDir}/bakApk/")
/**
 * 此處填寫每次構(gòu)建生成的基準包目錄
 */
def baseApkDir = "app-0831-17-50-44"
/**
 * 對于插件各參數(shù)的詳細解析請參考
 */
tinkerSupport {
    // 開啟tinker-support插件频轿,默認值true
    enable = true
    // 自動生成tinkerId, 你無須關(guān)注tinkerId,默認為false
    autoGenerateTinkerId = true
    // 指定歸檔目錄烁焙,默認值當前module的子目錄tinker
    autoBackupApkDir = "${bakPath}"
    // 是否啟用覆蓋tinkerPatch配置功能,默認值false
    // 開啟后tinkerPatch配置不生效耕赘,即無需添加tinkerPatch
    overrideTinkerPatchConfiguration = true
    // 編譯補丁包時骄蝇,必需指定基線版本的apk,默認值為空
    // 如果為空操骡,則表示不是進行補丁包的編譯
    // @{link tinkerPatch.oldApk }
    baseApk =  "${bakPath}/${baseApkDir}/app-release.apk"
    // 對應tinker插件applyMapping
    baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-release-mapping.txt"
    // 對應tinker插件applyResourceMapping
    baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-release-R.txt"
    // 構(gòu)建基準包跟補丁包都要修改tinkerId九火,主要用于區(qū)分
      tinkerId = "1.0.5-base_patch"
    // 打多渠道補丁時指定目錄
    // buildAllFlavorsDir = "${bakPath}/${baseApkDir}"
    // 是否使用加固模式,默認為false
    // isProtectedApp = true
    // 是否采用反射Application的方式集成册招,無須改造Application
    enableProxyApplication = true
}
/**
 * 一般來說,我們無需對下面的參數(shù)做任何的修改
 * 對于各參數(shù)的詳細介紹請參考:
 * https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
 */
tinkerPatch {
    tinkerEnable = true
    ignoreWarning = false
    useSign = true
    dex {
        dexMode = "jar"
        pattern = ["classes*.dex"]
        loader = []
    }
    lib {
        pattern = ["lib/*/*.so"]
    }
    res {
        pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
        ignoreChange = []
        largeModSize = 100
    }
    packageConfig {
    }
    sevenZip {
        zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
//        path = "/usr/local/bin/7za"
    }
    buildConfig {
        keepDexApply = false
//      tinkerId = "base-2.0.1"
    }
}

然后需要在Manifest配置文件配置如下

<activity
            android:name="com.tencent.bugly.beta.ui.BetaActivity"   
      android:configChanges="keyboardHidden|orientation|screenSize|locale"
            android:theme="@android:style/Theme.Translucent" />
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.fileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>

最后在Application中初始化bugly

public class App extends BaseApplication {
    @Override
    public void onCreate() {
        super.onCreate();
        setStrictMode();
        // 設(shè)置是否開啟熱更新能力岔激,默認為true
        Beta.enableHotfix = true;
        // 設(shè)置是否自動下載補丁
        Beta.canAutoDownloadPatch = true;
        // 設(shè)置是否提示用戶重啟
        Beta.canNotifyUserRestart = true;
        // 設(shè)置是否自動合成補丁
        Beta.canAutoPatch = true;

        /**
         *  全量升級狀態(tài)回調(diào)
         */
        Beta.upgradeStateListener = new UpgradeStateListener() {
            @Override
            public void onUpgradeFailed(boolean b) {
            }

            @Override
            public void onUpgradeSuccess(boolean b) {
            }

            @Override
            public void onUpgradeNoVersion(boolean b) {
                Toast.makeText(getApplicationContext(), "最新版本", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onUpgrading(boolean b) {
                Toast.makeText(getApplicationContext(), "onUpgrading", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onDownloadCompleted(boolean b) {

            }
        };
        /**
         * 補丁回調(diào)接口,可以監(jiān)聽補丁接收是掰、下載虑鼎、合成的回調(diào)
         */
        Beta.betaPatchListener = new BetaPatchListener() {
            @Override
            public void onPatchReceived(String patchFileUrl) {
                Toast.makeText(getApplicationContext(), patchFileUrl, Toast.LENGTH_SHORT).show();
            }
            @Override
            public void onDownloadReceived(long savedLength, long totalLength) {
                Toast.makeText(getApplicationContext(), String.format(Locale.getDefault(),
                        "%s %d%%",
                        Beta.strNotificationDownloading,
                        (int) (totalLength == 0 ? 0 : savedLength * 100 / totalLength)), Toast.LENGTH_SHORT).show();
            }
            @Override
            public void onDownloadSuccess(String patchFilePath) {
                Toast.makeText(getApplicationContext(), patchFilePath, Toast.LENGTH_SHORT).show();
//                Beta.applyDownloadedPatch();
            }
            @Override
            public void onDownloadFailure(String msg) {
                Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
            }
            @Override
            public void onApplySuccess(String msg) {
                Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
            }
            @Override
            public void onApplyFailure(String msg) {
                Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onPatchRollback() {
                Toast.makeText(getApplicationContext(), "onPatchRollback", Toast.LENGTH_SHORT).show();
            }
        };
        long start = System.currentTimeMillis();
        // 這里實現(xiàn)SDK初始化,appId替換成你的在Bugly平臺申請的appId,調(diào)試時將第三個參數(shù)設(shè)置為true
        Bugly.init(this, "2e5309db50", true);
        long end = System.currentTimeMillis();
    }
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        // you must install multiDex whatever tinker is installed!
        MultiDex.install(base);
        // 安裝tinker
        Beta.installTinker();
    }
    @TargetApi(9)
    protected void setStrictMode() {
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitAll().build());
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build());
    }
}

10.結(jié)束語

該組件框架是自己在暑假實習期間做的键痛,由于實習公司的項目過于龐大和復雜炫彩,每次編譯都需要花費10幾分鐘,心都碎了絮短,所以才想嘗試下組件化框架江兢,摸索了很長時間,最后還是做出來了丁频,大概花費2個多月的時間杉允,由于最近項目上比較忙,所以沒什么時間來完善席里,界面有點簡陋叔磷,但邏輯基本實現(xiàn)了。歡迎fork and star胁勺。

有對組件化框架興趣的同學可以加本人QQ1981367757世澜,一起探討技術(shù)。

github上地址: https://github.com/HelloChenJinJun/NewFastFrame
原著:

作者:傻瓜愛笨蛋
鏈接:http://www.reibang.com/p/e6eb9c8d120f
來源:簡書
著作權(quán)歸作者所有署穗。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)寥裂,非商業(yè)轉(zhuǎn)載請注明出處嵌洼。</pre>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市封恰,隨后出現(xiàn)的幾起案子麻养,更是在濱河造成了極大的恐慌,老刑警劉巖诺舔,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鳖昌,死亡現(xiàn)場離奇詭異,居然都是意外死亡低飒,警方通過查閱死者的電腦和手機许昨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來褥赊,“玉大人糕档,你說我怎么就攤上這事“韬恚” “怎么了速那?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長尿背。 經(jīng)常有香客問我端仰,道長,這世上最難降的妖魔是什么田藐? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任荔烧,我火速辦了婚禮,結(jié)果婚禮上坞淮,老公的妹妹穿的比我還像新娘茴晋。我一直安慰自己,他們只是感情好回窘,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布诺擅。 她就那樣靜靜地躺著,像睡著了一般啡直。 火紅的嫁衣襯著肌膚如雪烁涌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天酒觅,我揣著相機與錄音撮执,去河邊找鬼。 笑死舷丹,一個胖子當著我的面吹牛抒钱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼谋币,長吁一口氣:“原來是場噩夢啊……” “哼仗扬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蕾额,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤早芭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后诅蝶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體退个,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年调炬,在試婚紗的時候發(fā)現(xiàn)自己被綠了语盈。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡缰泡,死狀恐怖黎烈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情匀谣,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布资溃,位于F島的核電站武翎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏溶锭。R本人自食惡果不足惜宝恶,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望趴捅。 院中可真熱鬧垫毙,春花似錦、人聲如沸拱绑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽猎拨。三九已至膀藐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間红省,已是汗流浹背额各。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吧恃,地道東北人虾啦。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親傲醉。 傳聞我的和親對象是個殘疾皇子蝇闭,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

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

  • 目錄 1.什么是組件化? 2.為什么需要組件化和組件化帶來的好處需频? 3.組件化的基本框架 4.組件化框架的具體實現(xiàn)...
    傻瓜愛笨蛋閱讀 6,716評論 4 44
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,180評論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫丁眼、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,107評論 4 62
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理昭殉,服務發(fā)現(xiàn)苞七,斷路器,智...
    卡卡羅2017閱讀 134,665評論 18 139
  • 熟悉的幼兒園挪丢,摸摸小凳子蹂风,看小朋友吃飯,跟老師嘮嗑 安靜地看書乾蓬,寒假是補充精神食糧的好時期 動靜結(jié)合惠啄,最愛芭蕾
    Yutao123閱讀 293評論 0 0