終極元件化框架專案方案詳解

啊哈啊哈哈發表於2017-11-28

前言

本文所講的元件化案例是基於自己開源的元件化框架專案
github上地址github.com/HelloChenJi…
其中即時通訊(Chat)模組是單獨的專案
github上地址github.com/HelloChenJi…

1.什麼是元件化?

專案發展到一定階段時,隨著需求的增加以及頻繁地變更,專案會越來越大,程式碼變得越來越臃腫,耦合會越來越多,開發效率也會降低,這個時候我們就需要對舊專案進行重構即模組的拆分,官方的說法就是元件化。

2.為什麼需要元件化和元件化帶來的好處?

1、 現在Android專案中程式碼量達到一定程度,編譯將是一件非常痛苦的事情,一般都需要變異5到6分鐘。Android studio推出instant run由於各種缺陷和限制條件(比如採用熱修復tinker)一般情況下是被關閉的。而元件化框架可以使模組單獨編譯除錯,可以有效地減少編譯的時間。
2、通過元件化可以更好的進行並行開發,因為我們可以為每一個模組進行單獨的版本控制,甚至每一個模組的負責人可以選擇自己的設計架構而不影響其他模組的開發,與此同時元件化還可以避免模組之間的交叉依賴,每一個模組的開發人員可以對自己的模組進行獨立測試,獨立編譯和執行,甚至可以實現單獨的部署。從而極大的提高了並行開發效率。

3.元件化的基本框架

3.1元件框架圖
3.1元件框架圖

3.2專案結構圖
3.2專案結構圖

4.元件化框架的具體實現

4.1、基類庫的封裝

4.1基類庫圖
4.1基類庫圖

基類庫中主要包括開發常用的一些框架。
1、網路請求(多工下載和上傳,採用Retrofit+RxJava框架)
2、圖片載入(策略模式,Glide與Picasso之間可以切換)
3、通訊機制(RxBus)
4、基類adapter的封裝(支援item動畫、多佈局item、下拉和載入更多、item點選事件)
5、基類RecyclerView的封裝(支援原生風格的下拉載入,item側滑等)
6、mvp框架
7、各元件的資料庫實體類
8、通用的工具類
9、自定義view(包括對話方塊,ToolBar佈局,圓形圖片等view的自定義)
10、dagger的封裝(用於初始化全域性的變數和網路請求等配置)
等等

4.2元件模式和整合模式切換的實現

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) {
     //   元件模式下設定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的配置,用於元件間通訊的實現
            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檔案中設定isAlone=false

ext {
    isAlone = false;//false:作為Lib元件存在, true:作為application存在複製程式碼

2、然後Sync 下。
3、最後選擇app執行即可。

執行.png
執行.png

元件模式

1、首先需要在config,gradle檔案中設定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:作為元件模式存在

//  各個元件版本號的統一管理
    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、元件間通訊實現

元件間通訊的實現是採用阿里開源的Arouter路由通訊。
github地址:github.com/alibaba/ARo…
在App工程中,初始化元件通訊資料

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;
    }複製程式碼

然後在設定每個item的點選事件時,啟動元件介面跳轉。

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

每個元件入口介面的設定(比如直播Live元件,其它元件類似)

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

5.元件合併時res資源和AndroidManifest配置的問題

我們通過判斷元件處於哪種模式來動態設定專案res資源和Manifest、以及程式碼的位置。以直播元件為例,其它元件類似。

直播元件框架
直播元件框架

直播元件的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的實現和資料的初始化

採用類似於Glide在Manifest初始化配置的方式來初始化各個元件的Application,以直播元件為例,其它類似。

在BaseApplication中,初始化ApplicationDelegate代理類

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

ApplicationDelegate內部是怎樣的呢?繼續看下去

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的生命週期的回撥,用於實現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) {
//  相應呼叫元件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) {
//  相應呼叫元件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,用於初始化元件內的資料,比如在直播元件中初始化Dagger的全域性配置

public class LiveApplication implements IModuleConfig,IAppLife {
    private static MainComponent mainComponent;
    @Override
    public void injectAppLifecycle(Context context, List<IAppLife> iAppLifes) {
//  這裡需要把本引用新增到Application的生命週期的回撥中,以便實現回撥
        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.元件內網路請求和攔截器的實現

由於每個元件的BaseUrl和網路配置等可能不一樣,所以每個元件可以在自己配置的dagger中的 MainConponent實現自己的網路請求和攔截器。
以直播元件為例,其它類似。
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.元件化實現的技術難點

8.1.greendao資料庫的實現

greendao資料庫初始化程式碼,在基類庫的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 資料庫放在基類庫中,並且各個元件的實體類bean的建立也只能在基類庫中進行,以分包命名進行區分,如下圖。因為如果在元件內建立bean 會重新生成另一個副本DaoMaster並且不能操控其他元件的資料庫實體,有很大的侷限性。

基類庫元件實體分包圖
基類庫元件實體分包圖

8.2.資源命名衝突

官方說法是在每個module的build.gradle檔案中配置資原始檔名字首
這種方法缺點就是,所有的資源名必須要以指定的字串(moudle_prefix)做字首,否則會異常報錯,而且這方法只限定xml裡面的資源,對圖片資源並不起作用,所以圖片資源仍然需要手動去修改資源名。
所以不是很推薦使用這種方法來解決資源名衝突。所以只能自己注意點,在建立資源的時候,儘量不讓其重複。

resourcePrefix  "moudle_prefix"複製程式碼

8.3.butterKnife不能使用的原因

雖然Butterknife支援在lib中使用,但是條件是用 R2 代替 R ,在元件模式和整合模式的切換中,R2<->R之間的切換是無法完成轉換的,切換一次要改動全身,是非常麻煩的!所以不推薦在元件化中使用Butterknife。

8.4.library重複依賴問題

1、可能大家會認為,每個元件都依賴基類庫,基類庫library次不是重複依賴了?其實並不會存在這樣的問題,因為在構建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平臺,用於監控異常資訊、熱修復和應用升級。
具體實現:
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相關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/")
/**
 * 此處填寫每次構建生成的基準包目錄
 */
def baseApkDir = "app-0831-17-50-44"
/**
 * 對於外掛各引數的詳細解析請參考
 */
tinkerSupport {
    // 開啟tinker-support外掛,預設值true
    enable = true
    // 自動生成tinkerId, 你無須關注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"
    // 構建基準包跟補丁包都要修改tinkerId,主要用於區分
      tinkerId = "1.0.5-base_patch"
    // 打多渠道補丁時指定目錄
    // buildAllFlavorsDir = "${bakPath}/${baseApkDir}"
    // 是否使用加固模式,預設為false
    // isProtectedApp = true
    // 是否採用反射Application的方式整合,無須改造Application
    enableProxyApplication = true
}
/**
 * 一般來說,我們無需對下面的引數做任何的修改
 * 對於各引數的詳細介紹請參考:
 * 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();
        // 設定是否開啟熱更新能力,預設為true
        Beta.enableHotfix = true;
        // 設定是否自動下載補丁
        Beta.canAutoDownloadPatch = true;
        // 設定是否提示使用者重啟
        Beta.canNotifyUserRestart = true;
        // 設定是否自動合成補丁
        Beta.canAutoPatch = true;

        /**
         *  全量升級狀態回撥
         */
        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) {

            }
        };
        /**
         * 補丁回撥介面,可以監聽補丁接收、下載、合成的回撥
         */
        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();
        // 這裡實現SDK初始化,appId替換成你的在Bugly平臺申請的appId,除錯時將第三個引數設定為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.參考的專案

MVPArms
github.com/JessYanCodi…
全民直播
github.com/jenly1314/K…
音樂專案
github.com/hefuyicoder…
github.com/aa112901/re…
大象:PHPHub客戶端
github.com/Freelander/…
MvpApp
github.com/Rukey7/MvpA…
CloudReader
github.com/youlookwhat…
非常感謝以上開源專案的作者!謝謝!

11.結束語

該元件框架是自己在暑假實習期間做的,由於實習公司的專案過於龐大和複雜,每次編譯都需要花費10幾分鐘,心都碎了,所以才想嘗試下元件化框架,摸索了很長時間,最後還是做出來了,大概花費2個多月的時間,由於最近專案上比較忙,所以沒什麼時間來完善,介面有點簡陋,但邏輯基本實現了。歡迎fork and star。
有對元件化框架興趣的同學可以加本人QQ1981367757,一起探討技術。
github上地址: github.com/HelloChenJi…

相關文章