Android Gradle 实战之自动生成DeepLink配置信息

1.引言

android的deeplink 在项目中运用十分广泛,之前的一个项目大量使用deeplink。每完成一个新的功能,都得在AndroidManifest.xml写上deeplink的配置,同时还得在java文件中 写上deeplink的url静态变量。那个时候我就在想,能不能通过注解方式,代替手动抒写deeplink的配置?经过一番学习,终于有了实现的思路。

  • 目标:实现deeplink host path 等信息 自动注入到AndroidManifest.xml
    • 自定义注解,通过apt技术,得到每个activity的包名,host,scheme,path等信息
    • 得到上述信息之后,生成deepLink.xml 用于保存,配置deepLink相关的信息
    • 通过gradle去修改AndroidManifest.xml文件。自动生成<data android: host=""/> 等相关标签

用到的技术:apt,注解,gradle自定义插件以及插件的发布,XmlParser 操作xml,

2.正题

项目的文件目录:


1_image-20211121153429726.png

2.1 deeplink-annotation

首先new 一个 java 版的module,自定义一个DeeplinkAnnotation注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface DeeplinkAnnotation {
    String scheme();
    String host();
    String pathPrefix();
}

再自定义一个javaBean 用来保存scheme,host,pathPrefix三者信息:

DeepLinkBean.java

public class DeepLinkBean {

    private String host;
    private String scheme;
    private String pathPrefix;
}

之后在app的 build.gradle 中引入当前的注解:implementation project(path: ':deeplink-annotation')

2.2 deeplink-compiler2

再new 一个 java的 deeplink-compiler module 。主要对注解进行预处理操作。本身也是一个java的 module, 创建完毕引入相关的依赖:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //自动生成 META-INF
    api 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'    //动态生成Java 代码
    api 'com.squareup:javapoet:1.9.0'
    implementation project(':deeplink-annotation')
}

之后编译注解处理器。

注意:最好使用java 自定义注解处理器,博主用kotlin 以及build:gradle:7.0.3会报一些错,想来是最新的build:gradle有一些bug,这里建议最好用java 去弄

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.heytap.tv.deeplink_annotation.DeeplinkAnnotation")
public class DeeplinkAnnotationProcess extends AbstractProcessor {

    private Messager messager;
    private Elements elements;
    private HashMap<String, DeepLinkBean> maps = new HashMap<>();

    /**
     * 注释:可以得到ProcessingEnviroment,
     * ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
     * @param processingEnv
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        elements = processingEnv.getElementUtils();

    }


    /**
     * 注释:指定这个注解处理器是注册给哪个注解的
     * 时间:2021/2/18 0018 15:39
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(DeeplinkAnnotation.class.getCanonicalName());
        return supportTypes;
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(DeeplinkAnnotation.class);
        for (Element element : elements) {
            TypeMirror mTypeMirror = element.asType()  //获取当前Activity的全类名

            DeeplinkAnnotation bindAnnotation = element.getAnnotation(DeeplinkAnnotation.class);
            String host = bindAnnotation.host();
            String pathPrefix = bindAnnotation.pathPrefix();
            String scheme = bindAnnotation.scheme();
            DeepLinkBean deepLinkBean = new DeepLinkBean(host, scheme, pathPrefix);
            maps.put(mTypeMirror.toString(), deepLinkBean);
        }
  
      //SaxParser操作xml,生成一个xml
        
        String xmlString = SaxParser.saxCreateXML2(maps);
        messager.printMessage(Diagnostic.Kind.NOTE, xmlString);

        //获取app module下  build.gradle的位置,将xml保存到  app的build.gradle
        try {
            final FileObject fo = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "test");
            String temFilePath = fo.toUri().getPath();
            String rootPath = temFilePath.split("/build")[0];
            rootPath = rootPath.substring(1, rootPath.length());
            SaxParser.saveXml(rootPath, xmlString, "deepLink.xml");//将xml,保存到 app build.gradle下面
        } catch (Exception e) {
            e.printStackTrace();
            messager.printMessage(Diagnostic.Kind.NOTE, "error  :  " + e.getMessage());
        }

        return false;
    }
}

之后通过Rebbuild Project 重新构建下项目。生成的xml如下所示:

deepLink.xml

<?xml version="1.0" encoding="utf-8"?>
<deepLinks>
    <activity path="com.heytap.tv.myapplication.MainActivity" host="demo" scheme="test" pathPrefix="/mainActivity"/>
    <activity path="com.heytap.tv.myapplication.DeepLinkActivity" host="demo" scheme="test" pathPrefix="/deepLinkActivity"/>
</deepLinks>

有了这些数据,接下来,我们得将这些数据写入到AndroidManifest.xml

2.3 deeplink-gradle

创建一个java的 deeplink-gradle module 。自定义一个名为DeepLinkPlugin的插件,这个插件完成以下的事情:

  • 在target.afterEvaluate中 找到 android的拓展,

    • 找到各个变体(variant)的 processDebugManifest任务,为这个任务增加doLast操作(hook操作)
    • 修改AndroidManifest.xml 找到activity 向里面插入 deepLink的<data />
    • 发布插件

DeepLinkPlugin是整个deeplink-gradle的核心,主要是解析deepLink.xml得到对应的data数据,然后修改AndroidManifest.xml信息。

class DeepLinkPlugin implements Plugin<Project> {

    @Override
    void apply(Project target) {
        target.afterEvaluate {
            //获取 android 拓展 
            def mAndroid = target.getExtensions().getByName("android")
                
            //mAndroid.applicationVariants 得到是一个集合
            mAndroid.applicationVariants.all { variant ->   
               
                //variant:ApplicationVariant 代表每一种构建版本,如debug,release ,根据ApplicationVariant 我们可以得知 /签名信息
                def variantName = variant.name.capitalize()   
                def processManifestTask = target.tasks.getByName("process${variantName}Manifest")
                
                //找到一个名如processDebugManifest的task,通过给他添加last闭包完成hook操作
                
                processManifestTask.doLast {
                
                    //最终AndroidManifest.xml生成的地方
                    String manifestPath = target.getBuildDir().getAbsolutePath() + "/intermediates/merged_manifests/${variant.name}/AndroidManifest.xml"
                        
                    // 通过XmlParser deepLink.xml 并解析activity对应的data信息
                    HashMap<String, DeepLinkBean> deepLinkMaps = new HashMap<String, DeepLinkBean>();
                    String deepLinkXml = target.projectDir.getAbsolutePath() + "/deepLink.xml"
                    File file = new File(deepLinkXml);
                    if (!file.exists()) return
                    XmlParser mXmlParser = new XmlParser()
                    groovy.util.Node rootNode = mXmlParser.parseText(file.getText())
                    List list = rootNode.children()
                    for (i in 0..<list.size()) {
                        groovy.util.Node child = list.get(i)
                        println "child:  " + child.name()
                        String mPath = child.attribute("path")
                        String host = child.attribute("host")
                        String scheme = child.attribute("scheme")
                        String pathPrefix = child.attribute("pathPrefix")
                        DeepLinkBean mDeepLinkBean = new DeepLinkBean(host, scheme, pathPrefix);
                        deepLinkMaps.put(mPath, mDeepLinkBean)
                    }
                
                   // 通过XmlParser 去修改 AndroidManifest.xml ,为每个activity标签,添加<data>信息
                
                    File manifestFile = new File(manifestPath);
                    String manifest = manifestFile.getText()    
                    groovy.util.Node applicationNode = new XmlParser().parseText(manifest)
                    groovy.util.Node applicationNode2 = applicationNode.get("application").get(0);
                    NodeList activityNodeList = applicationNode2.get("activity")
                    List<String> hasExit = new ArrayList<String>();

                    for (i in 0..<activityNodeList.size()) {
                        groovy.util.Node mNode = activityNodeList.get(i)
                        Map map = mNode.attributes()
                        Iterator iterator = map.iterator();
                        while (iterator.hasNext()) {
                            Map.Entry<Object, Object> entrys = iterator.next();
                            String activityPath = entrys.value;
                            if (deepLinkMaps.containsKey(activityPath)) {
                                NodeList intentFilterList = mNode.get("intent-filter");
                                if (intentFilterList.size() > 0) {
                                    groovy.util.Node mIntentFilter = intentFilterList.get(0)
                                    DeepLinkBean mDeepLinkBean = deepLinkMaps.get(activityPath);
                                    HashMap<String, String> maps = new HashMap<>();
                                    maps.put("android:host", mDeepLinkBean.getHost())
                                    maps.put("android:scheme", mDeepLinkBean.getScheme())
                                    maps.put("android:path", mDeepLinkBean.getPathPrefix())
                                    mIntentFilter.appendNode("data", maps)
                                    //假如当前没有activity view 则需要添加
//                                   <action android:name="android.intent.action.VIEW" />

                                    NodeList mNodeList=mIntentFilter.get("action")
                                    if (mNodeList.size()==0){
                                        HashMap<String,String>actionMap=new HashMap<>();
                                        actionMap.put("android:name","android.intent.action.VIEW")
                                        mIntentFilter.appendNode("action",actionMap)
                                    }
                                    NodeList categoryNodeList=mIntentFilter.get("category")
                                    if (categoryNodeList.size()<2){
                                        HashMap<String,String>categoryMap=new HashMap<>();
                                        categoryMap.put("android:name","android.intent.category.DEFAULT")
                                        mIntentFilter.appendNode("category",categoryMap)
                                        HashMap<String,String>categoryMap2=new HashMap<>();
                                        categoryMap2.put("android:name","android.intent.category.BROWSABLE")
                                        mIntentFilter.appendNode("category",categoryMap2)
                                    }

                                    def stringConfig = XmlUtil.serialize(applicationNode)
                                    manifestFile.write(stringConfig)
                                    hasExit.add(activityPath)
                                } else {
                                    applicationNode2.remove(mNode)
                                }
                            }
                        }
                    }

                    for (m in deepLinkMaps) {
                        if (!hasExit.contains(m.key)) {
                            HashMap<String, String> maps = new HashMap<>();
                            maps.put("android:name", m.key);
                            groovy.util.Node mActivityNode = applicationNode2.appendNode("activity", maps)
                            groovy.util.Node filterNode = mActivityNode.appendNode("intent-filter")

                            NodeList mNodeList=filterNode.get("action")
                            if (mNodeList.size()==0){
                                HashMap<String,String>actionMap=new HashMap<>();
                                actionMap.put("android:name","android.intent.action.VIEW")
                                filterNode.appendNode("action",actionMap)
                            }
                            NodeList categoryNodeList=filterNode.get("category")
                            if (categoryNodeList.size()<2){
                                HashMap<String,String>categoryMap=new HashMap<>();
                                categoryMap.put("android:name","android.intent.category.DEFAULT")
                                filterNode.appendNode("category",categoryMap)
                                HashMap<String,String>categoryMap2=new HashMap<>();
                                categoryMap2.put("android:name","android.intent.category.BROWSABLE")
                                filterNode.appendNode("category",categoryMap2)
                            }

                            HashMap<String, String> dataMaps = new HashMap<>();
                            dataMaps.put("android:host", m.value.getHost())
                            dataMaps.put("android:scheme", m.value.getScheme())
                            dataMaps.put("android:path", m.value.getPathPrefix())
                            filterNode.appendNode("data", dataMaps)
                            def stringConfig = XmlUtil.serialize(applicationNode)
                            manifestFile.write(stringConfig)
                        }
                    }
                    boolean bl = file.delete()
                    println "delete success  :" + bl
                }
            }
        }
    }
}

以上代码主要是使用java写的,实现难度不是很复杂。在这里我主要想说说,

ApplicationVariant

ApplicationVariant 中文是应用变体,什么叫变体,初学者懵逼。通过下面打印名称:

   mAndroid.applicationVariants.all { variant ->   
                //variant:ApplicationVariant 代表每一种构建版本,如debug,release ,根据ApplicationVariant 我们可以得知 /签名信息
                def variantName = variant.name.capitalize()   
                pritfln  variantName;
  }

发现:variantName为debug, release 。假如在build.gradle中自定义了,产品如:productA,productB,productC。那么也会打印上述的名称。

通过查看api,可以看到,这个变体。能知道applicationId,签名信息,输出的outputs等重要信息。实际上build.gradle中经常使用ApplicationVariant ,来改变最终生成的apk的名称。

1_image-20211121163002968.png

之后在deeplink-gradle的gradle 中,加入发布到本地仓库的代码:

uploadArchives {
    def group='com.heytap.demo.deeplink' //组
    def version='1.0.0' //版本
    def artifactId='deeplinkPlugin' //唯一标示
    repositories {
        mavenDeployer {
            pom.groupId = group
            pom.artifactId = artifactId
            pom.version = version
            //指定本地maven的路径,在项目根目录下
            repository(url: uri('../repos'))
        }
    }
}

这样我们的demo 就算完成。最后在app module中引用一番


dependencies {
    //添加
    annotationProcessor project(path: ':deeplink-compiler2')
    implementation project(path: ':deeplink-annotation')
}

在新创建的Activity上面使用:

@DeeplinkAnnotation(host = "demo", scheme = "test", pathPrefix = "/mainActivity")

点击Rebuild project 就能生成deepLink.xml。 安装apk的时候会将此deeplink.xml删除,

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

推荐阅读更多精彩内容