關於Android模塊化你須要知道的

最近公司一個項目使用了模塊化設計,本人蔘與其中的一個小模塊開發,可是總體的設計並非我架構設計的,開發半年有餘,在此記錄下來個人想法。java

模塊化場景

爲何須要模塊化?android

當一個App用戶量增多,業務量增加之後,就會有不少開發工程師參與同一個項目,人員增長了,原先小團隊的開發方式已經不合適了。git

原先的一份代碼,如今須要多我的來維護,每一個人的代碼質量也不相同,在進行代碼Review的時候,也是比較困難的,同時也容易會產生代碼衝突的問題。github

同時隨着業務的增多,代碼變的愈來愈複雜,每一個模塊之間的代碼耦合變得愈來愈嚴重,解耦問題急需解決,同時編譯時間也會愈來愈長。web

人員增多,每一個業務的組件各自實現一套,致使同一個App的UI風格不同,技術實現也不同,團隊技術沒法獲得沉澱。json

架構演變

在剛剛開始的時候,項目架構使用的是MVP模式,這也是最近幾年很流行的一個架構方式,下面是項目的原始設計。bash

MVP

隨着業務的增多,咱們添加了Domain的概念,Domain從Data中獲取數據,Data可能會是Net,File,Cache各類IO等,而後項目架構變成了這樣。服務器

MVP2

再而後隨着人員增多,各類基礎組件也變的愈來愈多,業務也很複雜,業務與業務之間還有很強的耦合,就變成了這樣的。微信

使用模塊化技術之後,架構變成了這樣。

組件化架構

技術要點

這裏簡單介紹下Android項目實現模塊化須要使用的技術以及技術難點。架構

Library module

在開始開始進行模塊化以前,須要把各個業務單獨抽取成Android Library Module,這個是Android Studio自帶一個功能,能夠把依賴較少的,做爲基本組件的抽取成一個單獨模塊。

如圖所示,我把各個模塊單獨分爲一個獨立的項目。

組件化架構

在主項目中使用gradle添加代碼依賴。

// common
    compile project(':ModuleBase')
    compile project(':ModuleComponent')
    compile project(':ModuleService')

    // biz
    compile project(':ModuleUser')
    compile project(':ModuleOrder')
    compile project(':ModuleShopping')

複製代碼

Library module開發問題

在把代碼抽取到各個單獨的Library Module中,會遇到各類問題。最多見的就是R文件問題,Android開發中,各個資源文件都是放在res目錄中,在編譯過程當中,會生成R.java文件。R文件中包含有各個資源文件對應的id,這個id是靜態常量,可是在Library Module中,這個id不是靜態常量,那麼在開發時候就要避開這樣的問題。

舉個常見的例子,同一個方法處理多個view的點擊事件,有時候會使用switch(view.getId())這樣的方式,而後用case R.id.btnLogin這樣進行判斷,這時候就會出現問題,由於id不是常常常量,那麼這種方式就用不了。

一樣開發時候,用的最多的一個第三方庫就是ButterKnife,ButterKnife也是不能夠用的,在使用ButterKnife的時候,須要用到註解配置一個id來找到對應view,或者綁定對應的各類事件處理,可是註解中的各個字段的賦值也是須要靜態常量,那麼就不可以使用ButterKnife了。

解決方案有下面幾種:

1.從新一個Gradle插件,生成一個R2.java文件,這個文件中各個id都是靜態常量,這樣就能夠正常使用了。

2.使用Android系統提供的最原始的方式,直接用findViewById以及setOnClickListener方式。

3.設置項目支持Databinding,而後使用Binding中的對象,可是會增長很多方法數,同時Databinding也會有編譯問題和學習成本,可是這些也是小問題,我的覺的問題不大。

上面是主流的解決方法,我的推薦的使用優先級爲 3 > 2 > 1。

當把個模塊分開之後,每一個人就能夠單獨分組對應的模塊就好了,不過會有資源衝突問題,我的建議是對各個模塊的資源名字添加前綴,好比user模塊中的登陸界面佈局爲activity_login.xml,那麼能夠寫成這樣us_activity_login.xml。這樣就能夠避免資源衝突問題。同時Gradle也提供的一個字段resourcePrefix,確保各個資源名字正確,具體用法能夠參考官方文檔。

依賴管理

當完成了Library module後,代碼基本上已經很清晰了,跟咱們上面的最終架構已經很類似了,有了最基本的骨架,可是仍是沒有完成,由於仍是多我的操做同一個git倉庫,各個開發小夥伴仍是須要對同一個倉庫進行各類fork和pr。

隨着對代碼的分割,可是主項目app的依賴變多了,若是修改了lib中的代碼,那麼編譯時間是很恐怖的,大概統計了一下,原先在同一個模塊的時候,編譯時間大概須要2-3min,可是分開之後大概須要5-6min,這個是絕對沒法忍受的。

上面的第一問題,能夠這樣解決,把各個子module分別使用單獨的一個git倉庫,這樣每一個人也只須要關注本身須要的git倉庫便可,主倉庫使用git submodule的方式,分別依賴各個子模塊。

可是這樣仍是沒法解決編譯時間過長的問題,咱們把各個模塊也單獨打包,每次子模塊開發完成之後,發佈到maven倉庫中,而後在主項目中使用版本進行依賴。

舉個例子,好比進行某一版本迭代,這個版本叫1.0.0,那麼各個模塊的版本也叫一樣的版本,當版本完成測試發佈後,對各個模塊打對應版本的tag,而後就很清楚的瞭解各模塊的代碼分佈。

gradle依賴以下。

// common
    compile 'cn.mycommons:base:1.0.0'
    compile 'cn.mycommons:component:1.0.0'
    compile 'cn.mycommons:service:1.0.0'

    // biz
    compile 'cn.mycommons:user:1.0.0'
    compile 'cn.mycommons:order:1.0.0'
    compile 'cn.mycommons:shopping:1.0.0'

複製代碼

可能有人會問,既然各個模塊已經分開開發,那麼若是進行開發聯調,別急,這個問題暫時保留,後面會對這個問題後面再表。

數據通訊

當一個大項目拆成若干小項目時候,調用的姿式發生了少量改變。我這邊總結了App各個模塊之間的數據通訊幾種方式。

  • 頁面跳轉,好比在訂單頁面下單時候,須要判斷用戶是否登陸,若是沒有則須要跳到登陸界面。
  • 主動獲取數據,好比在下單時候,用戶已經登陸,下單須要傳遞用戶的基本信息。
  • 被動得到數據,好比在切換用戶的時候,有時候須要更新數據,如訂單頁面,須要把原先用戶的購物車數據給清空。

再來看下App的架構。

App架構

第一個問題,原先的方式,直接指定某個頁面的ActivityClass,而後經過intent跳轉便可,可是在新的架構中,因爲shopping模塊不直接依賴user,那麼則不能使用原始的進行跳轉,咱們解決方式使用Router路由跳轉。

第二個問題,原先的方式有個專門的業務單利,好比UserManager,直接能夠調用便可,一樣因爲依賴發生了改變,不可以進行調用。解決方案是全部的須要的操做,定義成接口放在Service中。

第三個問題,原先的方式,能夠針對事件變化提供回調接口,當我須要監聽某個事件時候,設置回調便可。

頁面路由跳轉

如上分析,原先方式代碼以下。

Intent intent = new Intent(this, UserActivity.class);
    startActivity(intent);

複製代碼

可是使用Router後,調用方式改變了。

RouterHelper.dispatch(getContext(), "app://user");

複製代碼

具體的原理是什麼,很簡單的,作一個簡單的映射匹配便可,把app://user與UserActivity.class配對,具體的就是定義一個Map,key是對應的Router字符,value是Activity的class。在跳轉時候從map中獲取對應的ActivityClass,而後在使用原始的方式。

可能有人的會問,要向另一個頁面傳遞參數怎麼辦,沒事咱們能夠在router後面直接添加參數,若是是一個複雜的對象那麼能夠把對象序列化成json字符串,而後再從對應的頁面經過反序列化的方式,獲得對應的對象。

例如:

RouterHelper.dispatch(getContext(), "app://user?id=123&obj={"name":"admin"}");

複製代碼

注: 上面的router中json字符串是須要url編碼的,否則會有問題的,這裏只是作個示例。

除了使用Router進行跳轉外,我想了一下,能夠參考Retrofit方式,直接定義跳轉Java接口,若是須要傳遞額外參數,則以函數參數的方式定義。

這個Java接口是沒有實現類的,可使用動態代理方式,而後接下來的方式,和使用Router的方式同樣。

那麼這總兩種方式有什麼優缺點呢。

Router方式:

  • 有點:不須要高難度的技術點,使用方便,直接使用字符串定義跳轉,能夠好的日後兼容
  • 缺點:由於使用的是字符串配置,若是字符輸入字符,則很難發現bug,同時也很難知道某個參數對應的含義

仿Retrofit方式:

  • 由於是Java接口定義,因此能夠很簡單找到對應的跳轉方法,參數定義也很明確,能夠直接寫在接口定義處,方便查閱。
  • 一樣由於是Java接口定義,那麼若是須要擴展參數,只能從新定義新方法,這樣會出現多個方法重載,若是在原先接口上修改,對應的原先調用方也要作響應的修改,比較麻煩。

上面是兩種實現方式,若是有相應同窗要實現模塊化,能夠根據實際狀況作出選擇。

Interface和Implement

如上分析,若是須要從某個業務中獲取數據,咱們分別須要定義接口以及實現類,然在獲取的時候在經過反射來實例化對象。

下面是簡單的代碼示例

接口定義

public interface IUserService {

    String getUserName();
}

複製代碼

實現類

class UserServiceImpl implements IUserService {

    @Override
    public String getUserName() {
        return "UserServiceImpl.getUserName";
    }
}

複製代碼

反射生成對象

public class InjectHelper {

    @NonNull
    public static AppContext getAppContext() {
        return AppContext.getAppContext();
    }

    @NonNull
    public static IModuleConfig getIModuleConfig() {
        return getAppContext().getModuleConfig();
    }

    @Nullable
    public static <T> T getInstance(Class<T> tClass) {
        IModuleConfig config = getIModuleConfig();
        Class<? extends T> implementClass = config.getServiceImplementClass(tClass);
        if (implementClass != null) {
            try {
                return implementClass.newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

複製代碼

實際調用

IUserService userService = InjectHelper.getInstance(IUserService.class);
    if (userService != null) {
        Toast.makeText(getContext(), userService.getUserName(), Toast.LENGTH_SHORT).show();
    }

複製代碼

本示例中每次調用都是用反射生成新的對象,實際應用中可能與IoC工具結合使用,好比Dagger2.

EventBus

針對上面的第三個問題,原先設計的使用方式也是能夠的,只須要把回調接口定義到對應的service接口中,而後調用方就可使用。

可是我建議可使用另一個方式——EventBus,EventBus也是利用觀察者模式,對事件進行監聽,是設置回調更優雅方式的實現。

優勢:不須要定義不少個回調接口,只須要定義事件Class,而後經過Claas的惟一性來進行事件匹配。

缺點:須要定義不少額外的類來表示事件,同時也須要關注EventBus的生命週期,在不須要使用事件時候,須要註銷事件綁定,否則容易發生內存泄漏。

映射匹配

上面的介紹的各個模塊之間通訊,都運涉及到映射匹配問題,在此我總結了一下,主要涉及到一下三種方式。

Map register

Map register是這樣的,全局定義一個Map,各個模塊在初始化的時候,分別在初始化的時候註冊映射關係。

下面是簡單的代碼示例,好比咱們定義一個模塊生命週期,用於初始化各個模塊。

public interface IModuleLifeCycle {

    void onCreate(IModuleConfig config);

    void onTerminate();
}

複製代碼

User模塊初始化

public class UserModuleLifeCycle extends SimpleModuleLifeCycle {

    public UserModuleLifeCycle(@NonNull Application application) {
        super(application);
    }

    @Override
    public void onCreate(@NonNull IModuleConfig config) {
        config.registerService(IUserService.class, UserServiceImpl.class);
        config.registerRouter("app://user", UserActivity.class);
    }
}

複製代碼

在Application中完成初始化

public class AppContext extends Application {

    private ModuleLifeCycleManager lifeCycleManager;

    @Override
    public void onCreate() {
        super.onCreate();

        lifeCycleManager = new ModuleLifeCycleManager(this);
        lifeCycleManager.onCreate();
    }

    @Override
    public void onTerminate() {
        super.onTerminate();

        lifeCycleManager.onTerminate();
    }

    @NonNull
    public IModuleConfig getModuleConfig() {
        return lifeCycleManager.getModuleConfig();
    }
}

public class ModuleLifeCycleManager {

    @NonNull
    private ModuleConfig moduleConfig;
    @NonNull
    private final List<IModuleLifeCycle> moduleLifeCycleList;

    ModuleLifeCycleManager(@NonNull Application application) {
        moduleConfig = new ModuleConfig();
        moduleLifeCycleList = new ArrayList<>();
        moduleLifeCycleList.add(new UserModuleLifeCycle(application));
        moduleLifeCycleList.add(new OrderModuleLifeCycle(application));
        moduleLifeCycleList.add(new ShoppingModuleLifeCycle(application));
    }

    void onCreate() {
        for (IModuleLifeCycle lifeCycle : moduleLifeCycleList) {
            lifeCycle.onCreate(moduleConfig);
        }
    }

    void onTerminate() {
        for (IModuleLifeCycle lifeCycle : moduleLifeCycleList) {
            lifeCycle.onTerminate();
        }
    }

    @NonNull
    IModuleConfig getModuleConfig() {
        return moduleConfig;
    }
}

複製代碼

APT

使用註解的方式配置映射信息,而後生成一個相似Database同樣的文件,而後Database文件中包含一個Map字段,Map中記錄各個映射信息。

首先須要定義個Annotation。

如:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Implements {

    Class parent();
}

複製代碼

須要實現一個 Annotation Process Tool,來解析本身定義的Annotation。

代碼略,此代碼有點複雜,暫時不貼了。

編譯產生的文件,大概以下所示。

public class Implement_$$_Database {

    @NonNull
    private final Map<Class<?>, Class<?>> serviceConfig;

    public Implement_$$_Database() {

        serviceConfig = new HashMap<>();
        serviceConfig.put(IUserService.class, UserServiceImpl.class);
    }

    public <T> Class<? extends T> getServiceImplementClass(Class<T> serviceClass) {
        return (Class<? extends T>) serviceConfig.get(serviceClass);
    }
}

複製代碼

而後利用反射找到Implement_$$_Database這個類,而後從方法中找到配對。

public class InjectHelper {

    @Nullable
    public static <T> T getInstanceByDatabase(Class<T> tClass) {
        Implement_$$_Database database = new Implement_$$_Database();
        Class<? extends T> implementClass = database.getServiceImplementClass(tClass);
        if (implementClass != null) {
            try {
                return implementClass.newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

複製代碼

而後在須要配置的地方添加註解便可。

@Implements(parent = IUserService.class)
class UserServiceImpl implements IUserService {

    @Override
    public String getUserName() {
        return "UserServiceImpl.getUserName";
    }
}

複製代碼

調用姿式。

binding.button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            IUserService userService = InjectHelper.getInstanceByDatabase(IUserService.class);
            if (userService != null) {
                Toast.makeText(getContext(), userService.getUserName(), Toast.LENGTH_SHORT).show();
            }
        }
    });

複製代碼

注意點:

有時候,在生成最終的配置文件的時候,文件的名字是固定的,好比上面的Implement_$$_Database,最終的路徑是這樣的

cn.mycommons.implements.database.Implement_$$_Database.java
複製代碼

而後經過編譯到apk中或則是aar中。

可是有個問題,若是各個子模塊都使用了這樣的插件,那麼每一個子模塊的就會有這個Implement_$$_Database.class,那麼就會編譯出錯。

由於aar中包含的時候class文件,不是java文件,不能在使用APT作處理了。下面有2中解決方案。

  1. 子工程的插件生成的文件包含必定的規則,好比包含模塊名字,如User_Implement_$$_Database.java,同時修改編譯過程,把java文件也打包到aar中,主工程的插件在編譯時候,提取aar中的文件,而後合併子工程的全部的代碼,這個思路是可行的,不過技術實現起來比較麻煩。

  2. 同一的方式相似,也是生成有必定規則的的文件,或者在特意package下生成class,這些class再經過接下來的所講的Gradle Transform方式,生成一個新的Database.class文件。

Gradle Transform

這是Android Gradle編譯提供的一個接口,能夠供開發自定義一些功能,而咱們就能夠根據這個功能生成映射匹配,這種方式和APT相似,APT是運行在代碼編譯時期,並且Transform是直接掃描class,而後再生成新的class,class中包含Map映射信息。修改class文件,使用的是javassist一個第三方庫。

下面簡單講述代碼實現,後面有機會單獨寫一篇文章講解。

首先定義一個註解,這個註解用於標註一個實現類的接口。

package cn.mycommons.modulebase.annotations;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Implements {
    Class parent();
}

複製代碼

一個測試用的接口以及實現類。

public interface ITest {
}

@Implements(parent = ITest.class)
public class TestImpl implements ITest {

}

複製代碼

定義一個靜態方法,用於獲取某個接口的實現類。

package cn.mycommons.modulebase.annotations;

public class ImplementsManager {

    private static final Map<Class, Class> CONFIG = new HashMap<>();

    public static Class getImplementsClass(Class parent) {
        return CONFIG.get(parent);
    }
}

複製代碼

若是不使用任何黑科技,直接使用Java技術,那麼在定義時候須要主動的往CONFIG這個map中添加配置,可是這裏咱們利用transform,直接動態的添加。

定義一個ImplementsPlugin gradle插件。

public class ImplementsPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        AppExtension app = project.getExtensions().getByType(AppExtension.class);
        app.registerTransform(new ImplementsTransform(project));
    }
}

複製代碼

自定義的Transform實現。

public class ImplementsTransform extends Transform {

    static final String IMPLEMENTS_MANAGER = "cn/mycommons/modulebase/annotations/ImplementsManager.class"
    static final String IMPLEMENTS_MANAGER_NAME = "cn.mycommons.modulebase.annotations.ImplementsManager"
    Project project

    ImplementsTransform(Project project) {
        this.project = project
    }

    void log(String msg, Object... args) {
        String text = String.format(msg, args)

        project.getLogger().error("[ImplementsPlugin]:${text}")
    }

    @Override
    public String getName() {
        return "ImplementsTransform"
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return ImmutableSet.of(QualifiedContent.DefaultContentType.CLASSES)
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return ImmutableSet.of(
                QualifiedContent.Scope.PROJECT,
                QualifiedContent.Scope.PROJECT_LOCAL_DEPS,
                QualifiedContent.Scope.SUB_PROJECTS,
                QualifiedContent.Scope.SUB_PROJECTS_LOCAL_DEPS,
                QualifiedContent.Scope.EXTERNAL_LIBRARIES
        )
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        long time1 = System.currentTimeMillis();
        log(this.toString() + ".....transform")

        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        outputProvider.deleteAll()

        def classPool = new ClassPool()
        classPool.appendSystemPath()

        // 記錄全部的符合掃描條件的記錄
        List<Entry> implementsList = []
        // ImplementsManager 註解所在的jar文件
        JarInput implementsManagerJar = null

        // 掃描全部的文件
        transformInvocation.inputs.each {
            it.directoryInputs.each {
                classPool.appendClassPath(it.file.absolutePath)
                def dst = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(it.file, dst)

                project.fileTree(dst).each {
                    String clazzPath = it.absolutePath.replace(dst.absolutePath, "")
                    clazzPath = clazzPath.replace("/", ".").substring(1)
                    if (clazzPath.endsWith(".class")) {
                        clazzPath = clazzPath.substring(0, clazzPath.size() - 6)
                        CtClass clazz = classPool.get(clazzPath)
                        // 若是class中的類包含註解則先收集起來
                        Implements annotation = clazz.getAnnotation(Implements.class)
                        if (annotation != null) {
                            implementsList.add(new Entry(annotation, clazz))
                        }
                    }
                }
            }
            it.jarInputs.each {
                classPool.appendClassPath(it.file.absolutePath)

                if (implementsManagerJar == null && isImplementsManager(it.file)) {
                    implementsManagerJar = it
                } else {
                    def dst = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.JAR)
                    FileUtils.copyFile(it.file, dst)

                    def jarFile = new JarFile(it.file)
                    def entries = jarFile.entries()

                    // 若是jar中的class中的類包含註解則先收集起來
                    while (entries.hasMoreElements()) {
                        def jarEntry = entries.nextElement()
                        String clazzPath = jarEntry.getName()
                        clazzPath = clazzPath.replace("/", ".")
                        if (clazzPath.endsWith(".class")) {
                            clazzPath = clazzPath.substring(0, clazzPath.size() - 6)
                            def clazz = classPool.get(clazzPath)
                            Implements annotation = clazz.getAnnotation(Implements.class)
                            if (annotation != null) {
                                implementsList.add(new Entry(annotation, clazz))
                            }
                        }
                    }
                }
            }
        }

        log("implementsManagerJar = " + implementsManagerJar)

        Map<String, String> config = new LinkedHashMap<>()

        implementsList.each {
            def str = it.anImplements.toString();
            log("anImplements =" + it.anImplements)
            def parent = str.substring(str.indexOf("(") + 1, str.indexOf(")")).replace("parent=", "").replace(".class", "")
            log("parent =" + parent)
            log("sub =" + it.ctClass.name)

            // 收集全部的接口以及實現類的路徑
            config.put(parent, it.ctClass.name)
        }

        log("config = " + config)

        long time2 = System.currentTimeMillis();

        if (implementsManagerJar != null) {
            def implementsManagerCtClass = classPool.get(IMPLEMENTS_MANAGER_NAME)
            log("implementsManagerCtClass = " + implementsManagerCtClass)

            // 修改class,在class中插入靜態代碼塊,作初始化
            def body = "{\n"
            body += "CONFIG = new java.util.HashMap();\n"

            for (Map.Entry<String, String> entry : config.entrySet()) {
                body += "CONFIG.put(${entry.key}.class, ${entry.value}.class);\n"
            }

            body += "}\n"
            log("body = " + body)

            implementsManagerCtClass.makeClassInitializer().body = body

            def jar = implementsManagerJar
            def dst = outputProvider.getContentLocation(jar.name, jar.contentTypes, jar.scopes, Format.JAR)
            println dst.absolutePath

            // 修改完成後,完成後再寫入到jar文件中
            rewriteJar(implementsManagerJar.file, dst, IMPLEMENTS_MANAGER, implementsManagerCtClass.toBytecode())
        }

        log("time = " + (time2 - time1) / 1000)
    }

    static boolean isImplementsManager(File file) {
        return new JarFile(file).getEntry(IMPLEMENTS_MANAGER) != null
    }

    static void rewriteJar(File src, File dst, String name, byte[] bytes) {
        dst.getParentFile().mkdirs()

        def jarOutput = new JarOutputStream(new FileOutputStream(dst))
        def rcJarFile = new JarFile(src)

        jarOutput.putNextEntry(new JarEntry(name))
        jarOutput.write(bytes)

        def buffer = new byte[1024]
        int bytesRead
        def entries = rcJarFile.entries()

        while (entries.hasMoreElements()) {
            def entry = entries.nextElement()
            if (entry.name == name) continue
            jarOutput.putNextEntry(entry)

            def jarInput = rcJarFile.getInputStream(entry)
            while ((bytesRead = jarInput.read(buffer)) != -1) {
                jarOutput.write(buffer, 0, bytesRead)
            }
            jarInput.close()
        }

        jarOutput.close()
    }
}

複製代碼

具體代碼能夠參考這裏

映射匹配總結

優勢:

  • Map:簡單明瞭,很容易入手,不會對編譯時間產生任何影響,不會隨着Gradle版本的升級而受影響,代碼混淆時候不會有影響,無需配置混淆文件。
  • APT:使用簡單,使用註解配置,代碼優雅,原理是用代碼生成的方式生成新的文件。
  • Transform:使用簡單,使用註解配置,代碼優雅,原理是用代碼生成的方式生成新的文件,不過生成的文件的時期和APT不一樣,會編譯時間產生少量影響。

缺點:

  • Map:在須要新添加映射的時候,須要手動添加,否則不會生效,代碼不優雅。
  • APT:在編譯時期生成文件,會編譯時間產生少量影響,同時在不一樣的Gradle的版本中可能會產生錯誤或者兼容問題。須要配置混淆設置,否則會丟失文件。技術實現複雜,較難維護。
  • Transform:在編譯時期生成文件,會編譯時間產生少量影響,同時在不一樣的Gradle的版本中可能會產生錯誤或者兼容問題。須要配置混淆設置,否則會丟失文件。技術實現複雜,較難維護。

從技術複雜性以及維護性來看,Map > APT = Transform

從使用複雜性以及代碼優雅性來看,Transform > APT > Map

開發調試技巧

Debug

上面介紹了不少關於模塊化的概念以及技術難題,當模塊化完成之後,再進行完成開發時候仍是會遇到很多問題。不如原先代碼在一塊兒的時候很方便的進行代碼調試。可是進行模塊化之後,直接使用的是aar依賴,不能直接修改代碼,可使用下面技巧,能夠直接進行代碼調試。

在根目錄下面建立一個module目錄以及module.gradle文件,這個目錄和文件是git ignore的,而後把對應的模塊代碼clone到裏面,根目錄的setting.gradlew apply module.gradle文件,以下所示,若是須要源碼調試,則在module中添加對應的模塊。而後在app的依賴中去掉aar依賴,同時添加項目依賴便可。當不須要源碼調試好,再修改成到原先代碼便可。

try {
    apply from: "./module.gradle"
} catch (e) {
}

複製代碼

module.gradle

include ':ModuleShopping'

複製代碼

好比調試shopping模塊

// common
    compile 'cn.mycommons:base:1.0.0'
    compile 'cn.mycommons:component:1.0.0'
    compile 'cn.mycommons:service:1.0.0'

    // biz
    compile 'cn.mycommons:user:1.0.0'
    compile 'cn.mycommons:order:1.0.0'
    // compile 'cn.mycommons:shopping:1.0.0'
    compile project(':ModuleShopping')

複製代碼

固然還有個更具技術挑戰性方案,使用gradle插件的形式,若是發現root項目中包含的模塊化的源碼,則不適用aar依賴,直接使用源碼依賴,固然這個想法是不錯的,不過具備技術挑戰性,同時有可能隨着Gradle版本的升級,編寫的gradle插件也要作相對於的兼容風險,這是隻是簡單提示一下。

容器設計

上面講到的若是要調試代碼時候,須要完整的運行的整個項目,隨着項目的增大,編譯時間可能變得很長。

咱們能夠作一個簡單的,相似與主app模塊同樣,好比我是負責user模塊的開發者,那麼我只要調試我這個模塊就好了,若是須要其餘的模塊,我能夠簡單的作一個mock,不是把其餘的模塊直接依賴過來,這樣能夠作到調試做用。等到再須要完整項目調試時候,咱們在使用上面介紹的方式,這樣能夠節省很多開發時間。

還有一種實現調試的方式,好比上面的user模塊,目錄下面的build.gradle文件是這樣的

apply plugin: 'com.android.library'

xxx
xxx

複製代碼

咱們能夠在gradle.properties中設置編譯變參數isLibModule,當須要完整調試好,設置爲isLibModule=false,這樣我這個子模塊就是一個apply plugin: 'com.android.application'這樣的模塊,是能夠單獨運行的一個項目

try {
    if (isLibModule) {
        apply from: "./build_lib.gradle"
    }else{
        apply from: "./build_app.gradle"
    }
} catch (e) {
}

複製代碼

可能有時候仍是須要單獨的運行環境,android編譯方式有2中,一種是debug,一種是release。當打包成aar的時候,使用的是release方式,咱們能夠把須要調試的代碼所有放到debug中,這樣打包的時候就不會把調試的文件發佈到aar中。不過這種實現方式,須要對Android項目的目錄有較高的認識,才能夠熟練使用。

CI

上面介紹的各個模塊須要單獨到獨立的git倉庫,同時打包到單獨的maven倉庫,當開發完成後,這時候就須要進行打包,但這個是一個簡單和重複的事情,因此咱們須要一個工具來完成這些事情,咱們能夠利用CI系統來搞定這件事情,這裏我推薦Jenkins,主流廠商使用jenkins做爲CI服務器這個方案。

具體的步驟就是,須要對每一個模塊的git倉庫作web hook,咱們公司使用的是git lab,能夠對git的各類操做作hook,好比push,merge,tag等。

當代碼發送了變化了,咱們能夠發送事件到CI服務器,CI服務器再對各個事件作處理,好比user模塊develop分支有代碼變化,這個變化多是merge,也有多是push。咱們能夠把主項目代碼和user項目的代碼單獨clone下拉,而後編譯一下,確認是否有編譯問題,若是有編譯經過,那麼在使用相關gradle命令發佈到maven倉庫中。

無論每次編譯結果怎樣,是成功仍是失敗,咱們都應該把結果回饋給開發者,常見的方式是郵件,不過這個信息郵件方式可能很頻繁,咱們建議使用slack。

最後咱們要注意的

若公司業務發展單一,是否組件化意義並不大,反而會加大自身開發成本,當業務已經成熟在回頭來優化組件化也何嘗不可。

總結

模塊化架構主要思路就是分而治之,把依賴整理清楚,減小代碼冗餘和耦合,在把代碼抽取到各自的模塊後,瞭解各個模塊的通訊方式,以及可能發生的問題,規避問題或者解決問題。最後爲了開發和調試方便,開發一些周邊工具,幫助開發更好的完成任務。

轉載請註明原文地址 : https://mp.weixin.qq.com/s/K4mzcUvp3KLQS7IJiVI-VQ

相信本身,沒有作不到的,只有想不到的

若是你以爲此文對您有所幫助,歡迎入羣 QQ交流羣 :644196190 微信公衆號:終端研發部

技術+職場
相關文章
相關標籤/搜索