通过Small原理学习Gradle插件开发

Small的原理

Small的基本知识可以参考使用Small进行Android模块化开发

这里重复下基本原理:

  1. APK包运行时仅仅是一个宿主工程,Java部分一般只包括一个Applicaton和一个入口Activity,还需要在asset的bundle.json中声明会用到的插件
  2. 公共库插件和业务插件以so文件的形式动态加载,Small提供了命令来生成这些插件so文件
  3. 插件可通过网络下载,因此可以做到热更新
  4. 如果需要新增插件,或者更新老插件的协议,还需要更新宿主插件声明信息(bundle.json)。

要做到以上几点,需要处理以下三方面的问题

  • 动态加载类
  • 动态加载资源
  • 动态注册组件

动态加载类

这部分主要是通过向BaseDexClassLoader中的DexPathList添加插件类实现的。有很多文章解释这部分的原理,这里就不赘述了。

前面提到的插件so包可以像一个apk文件被解压,但不能独立运行。Small就是从这些插件so文件中加载Element,并添加到DexPathList中的。

动态加载资源

Android是通过AssetManager来加载资源的,默认情况下只会添加

  • /framework/base.apk" - Android基本资源
  • /data/app/*.apk" - 应用程序资源

编译过程中aapt会为资源分配id,并存储为PPTTNNNN的16进制整数

  • PP代表包信息
  • TT代表资源类型
  • NNNN定位具体的资源

Small是通过修改PP字段来避免模块间的资源冲突的。在后面Small的编译过程中会介绍这部分的逻辑。

动态注册组件

  1. Activty受Instrumentation监控,都需要通过Instrumentation#execStartActivity来启动并激活声明周期
  2. 而Activity的实例则是在ActivityThread中,通过Instrumentation#newActivty来构建的。

因此要动态注册Activity,需要在宿主的Manifest中注册一系列的假Activity,来获取Activity的声明周期。

再通过反射的方式修改系统的Instrumentation,在系统启动假Activity之前,将Activity信息替换为真正的Activity。这部分会在Small运行阶段中详细介绍。

Small项目的编译过程

了解完Small的基本原理后,就该Gradle出场了。因为没有插件so包,Small是没法运行的。

下面介绍Small插件的几个方面:

  • Gradle插件的初始化过程
  • 如何为每个模块分配资源ID
  • 打出的so包到底是什么

我们这里只介绍与此相关的Gradle知识,如需补充更多Gradle基础知识可参考最后的几篇文章。对Small源码的分析也仅限于此。

Gradle插件的初始化过程

Small的Smaple目录下的build.gradle中有apply plugin: 'net.wequick.small'classpath 'net.wequick.tools.build:gradle-small:1.1.0-beta3'。前者是指定编译插件,后者是编译插件在maven仓库中的位置。

而在DevSmaple目录中,我们没有指定classpath也能使用这个插件。这是因为DevSmaple下有一个buildSrc工程,Sample中使用的脚本正是来自buildSrc。

buildSrc下是一个默认的Groovy工程,专门用于存放相对比较复杂的Groovy脚本,避免build.gradle过大里面主要包括一些properties和Groovy源文件。

net.wequick.small.properties中指定apply plugin: 'net.wequick.small'时需要运行的Groovy类implementation-class=net.wequick.gradle.RootPlugin,apply是所有Plugin被调用的入口。


class RootPlugin extends BasePlugin {

    void apply(Project project) {
        super.apply(project)
    }
    
    [...]
}

public abstract class BasePlugin implements Plugin<Project> {
    
    void apply(Project project) {
        this.project = project

        [...]

        createExtension()

        configureProject()

        createTask()
    }
    
    [...]
    
}

这里可以看到RootPlugin主要做了3件事:创建Extension,配置Project,创建Task。

创建Task

    @Override
    protected void createTask() {
        super.createTask()
        project.task('cleanLib', group: 'small', description: 'Clean all libraries', type: Delete) {
            delete small.preBuildDir
        }
        project.task('buildLib', group: 'small', description: 'Build all libraries').doFirst {
            buildingLibIndex = 1
        }
        project.task('cleanBundle', group: 'small', description: 'Clean all bundles')
        project.task('buildBundle', group: 'small', description: 'Build all bundles')
        project.task('small') << {
        
            [...]
        
        }
        
    }

这里我们定义了之前用到的buildLib和buildTask任务,我们才能在Android Studio中看到这些任务。当然Small中用到的任务远远不止这四个,而且他们之间有很强的依赖关系。

small_tasks.png

创建Extention

Groovy中定义的Extention就是在Gradle中可以直接配置的一些扩展信息。Android插件的Extention中一定包括compileSdkVersionbuildToolsVersion两个字段,我们才能在Gradle中使用

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"
}

Small中的Gradle配置和Groovy的RootExtention也是对应的

small {
    buildToAssets = false
    aarVersion = '1.1.0-beta9'
    android {
        compileSdkVersion = 23
        buildToolsVersion = "23.0.3"
        supportVersion = "23.4.0"
    }
}
//RootExtention

    boolean buildToAssets = false
    String aarVersion
    protected AndroidConfig android

    class AndroidConfig {
        int compileSdkVersion
        String buildToolsVersion
        String supportVersion
    }

配置Project

这里只截取了configureProject,这里可以看到,根据项目子模块的类型,还会继续加载对应的Plugin。

    @Override
    protected void configureProject() {

      [...]

      switch (type) {
        case 'app':
          it.apply plugin: AppPlugin
          rootExt.appProjects.add(it)
          break;
        case 'lib':
          it.apply plugin: LibraryPlugin
          rootExt.libProjects.add(it)
          break;
        default: // Default to Asset
          it.apply plugin: AssetPlugin
          break;
      }

      [...]

   }

如何为每个模块分配资源ID

修改PP字段就是在AppPlugin中实现的,LibraryPlugin也是继承AppPlugin,所以这个策略对公共库插件生效。

   protected void initPackageId() {
        Integer pp
        String ppStr = null
        Integer usingPP = sPackageIds.get(project.name)
        boolean addsNewPP = true
        // Get user defined package id
        if (project.hasProperty('packageId')) {
            def userPP = project.packageId
            if (userPP instanceof Integer) {
                // Set in build.gradle with 'ext.packageId=0x7e' as an Integer
                pp = userPP
            } else {
                // Set in gradle.properties with 'packageId=7e' as a String
                ppStr = userPP
                pp = Integer.parseInt(ppStr, 16)
            }

            if (usingPP != null && pp != usingPP) {
                // TODO: clean last build
                throw new Exception("Package id for ${project.name} has changed! " +
                        "You should call clean first.")
            }
        } else {
            if (usingPP != null) {
                pp = usingPP
                addsNewPP = false
            } else {
                pp = genRandomPackageId(project.name)
            }
        }

        small.packageId = pp
        small.packageIdStr = ppStr != null ? ppStr : String.format('%02x', pp)
        if (!addsNewPP) return

        // Check if the new package id has been used
        sPackageIds.each { name, id ->
            if (id == pp) {
                throw new Exception("Duplicate package id 0x${String.format('%02x', pp)} " +
                        "with $name and ${project.name}!\nPlease redefine one of them " +
                        "in build.gradle (e.g. 'ext.packageId=0x7e') " +
                        "or gradle.properties (e.g. 'packageId=7e').")
            }
        }
        sPackageIds.put(project.name, pp)
    }

可以看到通过一个sPackageIds保存每个插件对应的PP值。

打出的so包到底是什么

这里我们主要关注LibraryPlugin <- AppPlugin <- BundlePlugin,三者是继承关系。

  • LibraryPlugin:会将库工程转换为普通工程,从而能生成apk包。除此之外,这里还将生成的资源id都保存在一个public.txt文件中
  • AppPlugin:分配PP字段,合并Manifest,合并R文件
  • 将apk文件命名为so,并放到正确的位置

Small项目的运行

Small.preSetUp

这里主要是Small框架的初始化,主要注册了三种Launcher

    public static void preSetUp(Application context) {
        if (sContext != null) {
            return;
        }

        sContext = context;

        // Register default bundle launchers
        registerLauncher(new ActivityLauncher());
        registerLauncher(new ApkBundleLauncher());
        registerLauncher(new WebBundleLauncher());
        Bundle.onCreateLaunchers(context);
    }

这里主要关注ApkBundleLauncher#onCreate的一小部分,这里通过反射完成两件事

  1. 获取ActivityThread#mInstrumentation,并修改为ApkBundleLauncher#InstrumentationWrapper:这是为了在启动Activity时,将Activity改为Manifest中声明的假Activity,从而能通过Instrumentation的检查。
  2. 获取ActivityThread#mCallback,并修改为ApkBundleLauncher#ActivityThreadHandlerCallback:这是在ActivityThread创建Activity对象前,将ActivityInfo改为真正的Activity。
        // Get activity thread
        thread = ReflectAccelerator.getActivityThread(app);

        // Replace instrumentation
        try {
            f = thread.getClass().getDeclaredField("mInstrumentation");
            f.setAccessible(true);
            base = (Instrumentation) f.get(thread);
            wrapper = new ApkBundleLauncher.InstrumentationWrapper(base);
            f.set(thread, wrapper);
        } catch (Exception e) {
            throw new RuntimeException("Failed to replace instrumentation for thread: " + thread);
        }

        // Inject message handler
        try {
            f = thread.getClass().getDeclaredField("mH");
            f.setAccessible(true);
            Handler ah = (Handler) f.get(thread);
            f = Handler.class.getDeclaredField("mCallback");
            f.setAccessible(true);
            f.set(ah, new ApkBundleLauncher.ActivityThreadHandlerCallback());
        } catch (Exception e) {
            throw new RuntimeException("Failed to replace message handler for thread: " + thread);
        }

Small.setUp和Small.openUri

Small.setUp是根据bundle.json去加载插件模块。插件的实际加载过程,在ApkBundleLauncher中完成。

  1. 解析apk文件
  2. 加载apk中的资源和类
  3. 调用模块Application#onCreate

除此之外,Small.setUp还做了更新检测,这里不再赘述。

Small.openUri更简单,主要是根据uri去匹配对应的Activity。

总结

这篇文章的编译部分写得有点多,但是关联性有不是太强。这是为了在这部分介绍一些Gradle插件开发的内容。但由于笔者在这方面经验尚浅,所以写完自己也觉得有些凌乱,有空的时候可能会再修改。

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,392评论 25 707
  • afinalAfinal是一个android的ioc,orm框架 https://github.com/yangf...
    passiontim阅读 15,394评论 2 45
  • 需要研究技术:类动态加载,资源动态加载,组件动态注册 如果需要更新插件,或者新增老插件的协议,还需要更新宿主插件声...
    乐之飞于阅读 874评论 0 1
  • 人多老赖也会怕 男女搭配干活不累 今日战果证明我们是有...
    微微一笑更倾城阅读 391评论 0 0
  • 街灯无声无息 站立成一条刻意的整齐 夜市的门前 闪烁着圣诞老人那窃窃的笑意 关着门窗的屋子里 温暖得没有生气 天南...
    浏巧棉阅读 257评论 0 0