├── .gitignore ├── .travis.yml ├── README.md ├── annotation ├── .gitignore ├── build.gradle └── src │ └── main │ └── kotlin │ └── com │ └── omooo │ └── annotation │ └── MethodTrace.kt ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── runtime │ └── devRelease │ │ └── runtimeSchemes.json ├── singer └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── unused.json │ ├── unused2.json │ └── unused3.json │ ├── java │ └── com │ │ └── omooo │ │ └── plugin │ │ ├── DemoActivity.java │ │ ├── MainActivity.kt │ │ └── MyApplication.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable-xxhdpi │ └── bg.jpg │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_demo.xml │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── refs.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── buildSrc ├── .gitignore ├── build.gradle └── src │ └── main │ └── kotlin │ └── com │ └── omooo │ └── buildsrc │ └── Config.kt ├── frontend ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── top │ │ └── omooo │ │ └── frontend │ │ ├── Main.kt │ │ ├── bean │ │ ├── AarAnalyseReporter.kt │ │ ├── AarFile.kt │ │ ├── AppFile.kt │ │ ├── AppReporter.kt │ │ └── FileType.kt │ │ ├── chart │ │ ├── ApexCharts.kt │ │ ├── BarChartConfig.kt │ │ ├── ChartConfig.kt │ │ ├── ChartUtils.kt │ │ ├── ChartsComponent.kt │ │ └── LineChartConfig.kt │ │ ├── common │ │ ├── Header.kt │ │ ├── MissedWrapper.kt │ │ ├── ThemeModule.kt │ │ └── Themes.kt │ │ ├── component │ │ ├── AarAccordion.kt │ │ ├── AarList.kt │ │ ├── AarTitle.kt │ │ ├── OwnerSelects.kt │ │ └── Summary.kt │ │ ├── page │ │ └── ApkAnalysePage.kt │ │ └── util │ │ ├── AarAnalyseData.kt │ │ └── Format.kt │ └── resources │ ├── index.html │ └── report.json ├── gradle.properties ├── gradle ├── plugin.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kotlin-js-store └── yarn.lock ├── lavender-plugin └── ownership.yaml ├── library ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── omooo │ │ └── library │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── library.json │ │ └── lottie │ │ │ └── lottie.json │ ├── java │ │ └── com │ │ │ └── omooo │ │ │ └── library │ │ │ ├── LibraryActivity.kt │ │ │ └── LibraryMain.kt │ └── res │ │ └── layout │ │ └── activity_library.xml │ └── test │ └── java │ └── com │ └── omooo │ └── library │ └── ExampleUnitTest.kt ├── plugin ├── .gitignore ├── build.gradle └── src │ └── main │ ├── kotlin │ └── com │ │ └── omooo │ │ └── plugin │ │ ├── Lavender.kt │ │ ├── bean │ │ ├── CheckSchemeModifiedExtension.kt │ │ ├── Constants.kt │ │ ├── InvokeCheckExtension.kt │ │ └── WebpToolBean.kt │ │ ├── internal │ │ ├── ArtifactType.kt │ │ ├── aar │ │ │ └── AarAnalyse.kt │ │ ├── apk │ │ │ ├── ApkIncrementAnalyse.kt │ │ │ ├── ApkParser.kt │ │ │ ├── AppFileCleaner.kt │ │ │ ├── ClassCleaner.kt │ │ │ ├── ICleaner.kt │ │ │ ├── ResourceCleaner.kt │ │ │ └── TypeAssigningCleaner.kt │ │ └── cha │ │ │ ├── ClassSetCache.kt │ │ │ ├── ComponentHandler.kt │ │ │ ├── LayoutHandler.kt │ │ │ └── ReferenceAnalyser.kt │ │ ├── reporter │ │ ├── AarAnalyseReporter.kt │ │ ├── AppReporter.kt │ │ ├── HtmlReporter.kt │ │ ├── Insight.kt │ │ └── common │ │ │ ├── AarFile.kt │ │ │ ├── AppFile.kt │ │ │ └── FileType.kt │ │ ├── scan │ │ └── BuildScan.kt │ │ ├── spi │ │ └── VariantProcessor.kt │ │ ├── task │ │ ├── AarAnalyseTask.kt │ │ ├── AarAnalyseTaskProcessor.kt │ │ ├── ApkAnalyseTask.kt │ │ ├── ApkAnalyseVariantProcessor.kt │ │ ├── CheckExportedTask.kt │ │ ├── CheckExportedVariantProcessor.kt │ │ ├── CheckSchemeModifiedProcessor.kt │ │ ├── CheckSchemeModifiedTask.kt │ │ ├── CheckServiceTypeTask.kt │ │ ├── CheckServiceTypeVariantProcessor.kt │ │ ├── DetectTranslucentActivityTask.kt │ │ ├── DetectTranslucentActivityVariantProcessor.kt │ │ ├── FragmentNonConstructCheckTask.kt │ │ ├── FragmentNonConstructCheckVariantProcessor.kt │ │ ├── ListAarSizeTask.kt │ │ ├── ListAarSizeVariantProcessor.kt │ │ ├── ListAssetsTask.kt │ │ ├── ListAssetsVariantProcessor.kt │ │ ├── ListClassOwnerMapTask.kt │ │ ├── ListClassOwnerMapVariantProcessor.kt │ │ ├── ListImageTask.kt │ │ ├── ListImageVariantProcessor.kt │ │ ├── ListPackageNameTask.kt │ │ ├── ListPackageNameVariantProcessor.kt │ │ ├── ListPermissionTask.kt │ │ ├── ListPermissionVariantProcessor.kt │ │ ├── ListSchemeTask.kt │ │ ├── ListSchemeVariantProcessor.kt │ │ ├── ListUnusedAssetsTask.kt │ │ ├── ListUnusedAssetsVariantProcessor.kt │ │ ├── ListUnusedClassTask.kt │ │ ├── ListUnusedClassVariantProcessor.kt │ │ ├── ListUnusedResTask.kt │ │ ├── ListUnusedResVariantProcessor.kt │ │ ├── RepeatResDetectorTask.kt │ │ └── RepeatResDetectorVariantProcessor.kt │ │ ├── transform │ │ ├── BaseClassNode.kt │ │ ├── BaseClassVisitor.kt │ │ ├── invoke │ │ │ ├── InvokeCheckClassNode.kt │ │ │ ├── InvokeCheckCvFactory.kt │ │ │ └── InvokeCheckParams.kt │ │ ├── shrinkres │ │ │ ├── IdentifierCheckClassNode.kt │ │ │ └── IdentifierCheckCvFactory.kt │ │ └── systrace │ │ │ ├── SystraceClassVisitor.kt │ │ │ └── SystraceCvFactory.kt │ │ └── util │ │ ├── ArtifactExt.kt │ │ ├── ClassDataExt.kt │ │ ├── ClassNodeExt.kt │ │ ├── CollectionsExt.kt │ │ ├── ConsoleExt.kt │ │ ├── FileExt.kt │ │ ├── InsnNodeExt.kt │ │ ├── ManifestFileExt.kt │ │ ├── OpcodesExt.kt │ │ ├── ProjectExt.kt │ │ ├── StringExt.kt │ │ ├── TransformReporter.kt │ │ ├── Utils.kt │ │ ├── VariantExt.kt │ │ ├── WebpToolUtil.kt │ │ ├── XmlParseExt.kt │ │ └── ZipUtils.kt │ └── resources │ ├── META-INF │ └── gradle-plugins │ │ └── com.omooo.lavender.properties │ ├── aarAnalyse-Template.html │ └── apkAnalyse-Template.html ├── settings.gradle ├── tools └── cwebp │ ├── linux │ └── cwebp │ ├── mac │ └── cwebp │ └── windows │ └── cwebp.exe └── wiki ├── 包体积优化 ├── APK 增量分析.md ├── 删除无用 Assets 资源.md ├── 图片压缩使用文档.md ├── 无用 Assets 资源检测.md ├── 无用资源监测.md ├── 输出 App 依赖 AAR 下的 assets 资源.md ├── 输出 App 依赖的 AAR 大小.md ├── 输出图片列表.md └── 重复资源检测.md └── 静态分析 ├── exported 属性检测.md ├── scheme 变更检查.md ├── 依赖权限检测.md └── 方法、字段、常量调用检测.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | /repo 10 | lavender-plugin/apk/previous.json 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | dist: trusty 3 | android: 4 | components: 5 | # Uncomment the lines below if you want to 6 | # use the latest revision of Android SDK Tools 7 | # - tools 8 | # - platform-tools 9 | 10 | # The BuildTools version used by your project 11 | - build-tools-29.0.2 12 | 13 | # The SDK version used to compile your project 14 | - android-29 15 | 16 | # Additional components 17 | - extra-google-google_play_services 18 | - extra-google-m2repository 19 | - extra-android-m2repository 20 | 21 | # Specify at least one system image, 22 | # if you need to run emulator(s) during your tests 23 | - sys-img-x86-android-26 24 | - sys-img-armeabi-v7a-android-17 25 | before_cache: 26 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 27 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 28 | cache: 29 | directories: 30 | - $HOME/.gradle/caches/ 31 | - $HOME/.gradle/wrapper/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | 概览 3 | --- 4 | 5 | ## 一、Lavener 是什么 6 | 7 | Lavener 是一个 Gradle Plugin,提供一系列静态检测的能力,其目标主要是为了解决随着 APP 复杂度的提升而带来的性能、稳定性、包体积等一系列质量问题。 8 | 9 | ## 二、为什么以此命名? 10 | 11 | Lavender 直译为薰衣草。薰衣草的香味能够帮助我们缓解压力、减少沮丧。 12 | 13 | 希望这个 Gradle Plugin 也能够帮助我们减少重复的劳动工作,使我们的工作更加轻松顺畅。 14 | 15 | ## 三、功能列表 16 | 17 | 目前包含包体积瘦身和安全合规检查相关功能。 18 | 19 | ### 包体积瘦身 20 | 21 | | 功能 | 使用文档 | 22 | | ------------------------------------- | ------------------------------------------------------------ | 23 | | 重复资源监测 | [重复资源检测](/wiki/包体积优化/重复资源检测.md) | 24 | | 输出 AAR 大小 | [输出 App 依赖的 AAR 大小](/wiki/包体积优化/输出%20App%20依赖的%20AAR%20大小.md) | 25 | | 打包时自动 png 转 webp、webp 图片压缩 | [图片压缩使用文档](/wiki/包体积优化/图片压缩使用文档.md) | 26 | | 无用资源监测 | [无用资源监测](/wiki/包体积优化/无用资源监测.md) | 27 | | 无用 Assets 资源监测 | [无用 Assets 资源检测](/wiki/包体积优化/无用%20Assets%20资源检测.md) | 28 | | 输出 App 依赖的 AAR 下的 assets 资源 | [输出 App 依赖的所有 assets 资源](/wiki/包体积优化/输出%20App%20依赖%20AAR%20下的%20assets%20资源.md) | 29 | | 删除无用 Assets 资源 | [删除无用 Assets 资源](/wiki/包体积优化/删除无用%20Assets%20资源.md) | 30 | | APK 增量分析 | [APK 增量分析](/wiki/包体积优化/APK%20增量分析.md) | 31 | | 输出图片列表 | [输出图片列表](/wiki/包体积优化/输出图片列表.md) | 32 | 33 | ### 静态分析 34 | 35 | | 功能 | 使用 | 36 | | ---------------------------------------------------- | ------------------------------------------------------ | 37 | | 输出 App 及其依赖的 AAR 权限 | [依赖权限检测]() | 38 | | 检测 Manifest 注册组件是否声明 android:exported 属性 | [exported 属性检测]() | 39 | | 类、方法、常量、字段调用检测 | [方法调用检测]() | 40 | | 输出无用类文件 | [输出无用类文件]() | 41 | | 输出类及所属负责人的映射 | [输出类及所属的负责人的映射]() | 42 | | 输出 Manifest 定义的 scheme | [输出 Manifest 定义的 scheme ]() | 43 | | 检测透明 Activity 设置了 screenOrientation 属性 | [检测透明 Activity 设置了 screenOrientation 属性]() | 44 | | 输出依赖的包名列表 | [输出依赖的包名列表]() | 45 | | scheme 变更检查 | [scheme 变更检查](/wiki/静态分析/scheme%20变更检查.md) | -------------------------------------------------------------------------------- /annotation/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /annotation/build.gradle: -------------------------------------------------------------------------------- 1 | apply from: '../gradle/plugin.gradle' 2 | 3 | //apply plugin: 'maven' 4 | apply plugin: 'java' 5 | 6 | //uploadArchives { 7 | // repositories { 8 | // mavenDeployer { 9 | // repository(url: "http://192.168.9.230:8081/repository/app-releases/") { 10 | // authentication(userName: "admin", password: "admin123") 11 | // } 12 | // 13 | // pom.groupId = 'com.omooo.plugin' 14 | // pom.artifactId = 'annotation' 15 | // pom.version = '0.1.1' 16 | // 17 | // pom.project { 18 | // licenses { 19 | // license { 20 | // name 'The Apache Software License, Version 2.0' 21 | // url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 22 | // } 23 | // } 24 | // } 25 | // } 26 | // } 27 | //} 28 | java { 29 | sourceCompatibility = JavaVersion.VERSION_17 30 | targetCompatibility = JavaVersion.VERSION_17 31 | } 32 | 33 | kotlin { 34 | jvmToolchain(17) 35 | } -------------------------------------------------------------------------------- /annotation/src/main/kotlin/com/omooo/annotation/MethodTrace.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.annotation 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2019/10/11 6 | * Version: v0.1.0 7 | * Desc: 方法耗时打点注解 8 | */ 9 | 10 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 11 | @Retention(AnnotationRetention.BINARY) 12 | annotation class MethodTrace -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'com.omooo.lavender' 6 | //invokeCheckConfig { 7 | // methodList = [ 8 | // "android.widget.Toast#makeText", 9 | // "android.widget.Toast", 10 | // "android.widget.Toast#show()V", 11 | // ] 12 | // packageList = [ 13 | // "com.omooo.library", 14 | // ] 15 | //} 16 | 17 | //checkSchemeModifiedConfig { 18 | // enable = true 19 | // baselineSchemeFile = file("runtime/devRelease/runtimeSchemes.json") 20 | //} 21 | 22 | apply plugin: 'com.spotify.ruler' 23 | ruler { 24 | abi.set("arm64-v8a") 25 | locale.set("zh") 26 | screenDensity.set(480) 27 | sdkVersion.set(33) 28 | } 29 | 30 | android { 31 | compileSdkVersion 34 32 | namespace 'com.omooo.plugin' 33 | defaultConfig { 34 | applicationId "com.omooo.plugin" 35 | minSdkVersion 23 36 | targetSdkVersion 34 37 | versionCode 1 38 | versionName "1.0" 39 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 40 | } 41 | buildTypes { 42 | release { 43 | minifyEnabled true 44 | shrinkResources true 45 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 46 | } 47 | } 48 | } 49 | 50 | dependencies { 51 | implementation fileTree(dir: 'libs', include: ['*.jar']) 52 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.10" 53 | implementation 'androidx.appcompat:appcompat:1.6.1' 54 | implementation 'androidx.core:core-ktx:1.10.1' 55 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 56 | 57 | implementation(project(":library")) 58 | } 59 | 60 | kotlin { 61 | jvmToolchain(17) 62 | } 63 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/runtime/devRelease/runtimeSchemes.json: -------------------------------------------------------------------------------- 1 | { 2 | "app.ui.activity.DemoActivity": { 3 | "first": "owner@demo.com", 4 | "second": "scheme://host/path" 5 | } 6 | } -------------------------------------------------------------------------------- /app/singer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/app/singer -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/assets/unused.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /app/src/main/assets/unused2.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /app/src/main/assets/unused3.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "value" 3 | } -------------------------------------------------------------------------------- /app/src/main/java/com/omooo/plugin/DemoActivity.java: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin; 2 | 3 | import android.os.Bundle; 4 | import android.os.PersistableBundle; 5 | 6 | import androidx.annotation.Nullable; 7 | import androidx.appcompat.app.AppCompatActivity; 8 | 9 | /** 10 | * Author: Omooo 11 | * Date: 2023/4/4 12 | * Desc: 13 | */ 14 | public class DemoActivity extends AppCompatActivity { 15 | @Override 16 | public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { 17 | super.onCreate(savedInstanceState, persistentState); 18 | setContentView(R.layout.activity_demo); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/omooo/plugin/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin 2 | 3 | import android.os.Bundle 4 | import android.widget.Toast 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.omooo.library.LibraryMain 7 | 8 | class MainActivity : AppCompatActivity() { 9 | 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | setContentView(R.layout.activity_main) 13 | 14 | LibraryMain().show(this) 15 | 16 | assets.open("unused.json").close() 17 | show() 18 | } 19 | 20 | private fun show() { 21 | Toast.makeText(this, "2333", Toast.LENGTH_SHORT).show() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/omooo/plugin/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin 2 | 3 | import android.app.Application 4 | 5 | /** 6 | * @author Omooo 7 | * @version v1.0 8 | * @Date 2020/03/11 16:46 9 | * desc : 10 | */ 11 | class MyApplication : Application() { 12 | override fun onCreate() { 13 | super.onCreate() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/app/src/main/res/drawable-xxhdpi/bg.jpg -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/refs.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Lavender 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.9.0' 5 | repositories { 6 | mavenLocal() 7 | google() 8 | mavenCentral() 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:8.2.0' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | 17 | classpath "com.omooo:lavender:0.0.1" 18 | classpath("com.spotify.ruler:ruler-gradle-plugin:2.0.1") 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | mavenLocal() 25 | google() 26 | mavenCentral() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /buildSrc/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.9.0' 3 | repositories { 4 | mavenLocal() 5 | google() 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 | } 11 | } 12 | 13 | apply plugin: 'java' 14 | apply plugin: 'maven-publish' 15 | apply plugin: 'kotlin' 16 | 17 | repositories { 18 | mavenLocal() 19 | google() 20 | mavenCentral() 21 | } 22 | 23 | dependencies { 24 | implementation gradleApi() 25 | implementation localGroovy() 26 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 27 | implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 28 | testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 29 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" 30 | } -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/com/omooo/buildsrc/Config.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.buildsrc 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2019/9/27 6 | * Version: v0.1.0 7 | * Desc: 统一依赖版本配置 8 | */ 9 | class Config { 10 | companion object { 11 | const val kotlin_version = "1.9.0" 12 | } 13 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /externals -------------------------------------------------------------------------------- /frontend/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("js") 3 | kotlin("plugin.serialization") 4 | } 5 | 6 | fun kotlinw(target: String): String = 7 | "org.jetbrains.kotlin-wrappers:kotlin-$target" 8 | 9 | dependencies { 10 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") 11 | implementation("org.jetbrains.kotlin-wrappers:kotlin-extensions:1.0.1-pre.343") 12 | 13 | implementation(enforcedPlatform(kotlinw("wrappers-bom:1.0.0-pre.477"))) 14 | 15 | implementation(kotlinw("react")) 16 | implementation(kotlinw("react-dom")) 17 | implementation(kotlinw("react-router-dom")) 18 | 19 | implementation(kotlinw("emotion")) 20 | implementation(kotlinw("mui")) 21 | implementation(kotlinw("mui-icons")) 22 | implementation(npm("apexcharts", "3.41.0")) 23 | 24 | implementation(npm("date-fns", "2.29.3")) 25 | implementation(npm("@date-io/date-fns", "2.16.0")) 26 | } 27 | 28 | kotlin { 29 | js(IR) { 30 | browser { 31 | commonWebpackConfig { 32 | cssSupport { 33 | this.enabled = true 34 | } 35 | } 36 | } 37 | binaries.executable() 38 | } 39 | } 40 | 41 | // 注册 browserPackage Task 42 | tasks.register("browserPackage") { 43 | dependsOn("browserDistribution") 44 | mustRunAfter("browserDistribution") 45 | doLast { 46 | val rootDir = buildDir.resolve("distributions") 47 | val html = rootDir.resolve("index.html").readText() 48 | val javascript = rootDir.resolve("frontend.js").readText() 49 | val reportFile = rootDir.resolve("report.html") 50 | reportFile.writeText( 51 | html.replace("", "") 52 | ) 53 | println("Wrote HTML report to ${reportFile.toPath().toUri()}") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/bean/AarAnalyseReporter.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.bean 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2023/7/14 6 | * Desc: AAR 分析报告 7 | */ 8 | @kotlinx.serialization.Serializable 9 | data class AarAnalyseReporter( 10 | /** 描述信息 */ 11 | val desc: String, 12 | /** 文档链接 */ 13 | val documentLink: String, 14 | /** 包名 */ 15 | val packageName: String, 16 | /** AAR 列表 */ 17 | val aarList: List>>, 18 | /** 所属人映射 */ 19 | val ownerMap: Map> = emptyMap(), 20 | ) { 21 | 22 | /** 23 | * 获取 AAR 趋势表格的 x 轴标签 24 | */ 25 | fun getChartLabels(): Array { 26 | return aarList.map { 27 | it.first 28 | }.toTypedArray().reversedArray() 29 | } 30 | 31 | /** 32 | * 获取 AAR 趋势表格的 y 轴数据 33 | */ 34 | fun getChartSeries(owner: String, aarName: String): Pair { 35 | val series = LongArray(aarList.size) 36 | aarList.forEachIndexed { index, pair -> 37 | if (aarName == "All") { 38 | series[index] = pair.second.filter { 39 | if (owner == "All") true else it.owner == owner 40 | }.totalSize() 41 | } else { 42 | series[index] = pair.second.find { it.name == aarName }?.size ?: 0 43 | } 44 | } 45 | return Pair("大小", series.reversedArray()) 46 | } 47 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/bean/AarFile.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.bean 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Author: Omooo 7 | * Date: 2023/2/3 8 | * Desc: 9 | */ 10 | @Serializable 11 | data class AarFile( 12 | val name: String, 13 | val size: Long, 14 | val owner: String, 15 | val fileList: List = emptyList(), 16 | ) 17 | 18 | internal fun List.totalSize(): Long { 19 | if (isEmpty()) { 20 | return 0 21 | } 22 | return map { it.size }.reduce { acc, l -> acc + l } 23 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/bean/AppFile.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.bean 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Author: Omooo 7 | * Date: 2023/2/2 8 | * Desc: 9 | */ 10 | @Serializable 11 | data class AppFile( 12 | val name: String, 13 | val size: Long, 14 | val desc: String, 15 | var fileType: FileType = FileType.OTHER, 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/bean/AppReporter.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.bean 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2023/2/3 6 | * Desc: App 报告类 7 | */ 8 | @kotlinx.serialization.Serializable 9 | data class AppReporter( 10 | /** 描述信息 */ 11 | val desc: String, 12 | /** 文档链接 */ 13 | val documentLink: String, 14 | /** 版本号 */ 15 | val versionName: String, 16 | /** 构建类型名 */ 17 | val variantName: String, 18 | /** AAR 列表 */ 19 | val aarList: List, 20 | ) -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/bean/FileType.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.bean 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2023/3/17 6 | * Desc: 文件类型 7 | */ 8 | enum class FileType { 9 | CLASS, 10 | RESOURCE, 11 | ASSET, 12 | NATIVE_LIB, 13 | OTHER, 14 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/chart/ApexCharts.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.chart 2 | 3 | 4 | import org.w3c.dom.Element 5 | 6 | typealias NumberFormatter = (Number) -> String 7 | typealias TooltipAxisFormatter = (Number, TooltipAxisFormatterOptions) -> String 8 | 9 | @JsModule("apexcharts") 10 | @JsNonModule 11 | @Suppress("UnusedPrivateMember") 12 | external class ApexCharts(element: Element?, options: dynamic) { 13 | fun render() 14 | fun destroy() 15 | } 16 | 17 | external interface ApexChartOptions { 18 | var chart: ChartOptions 19 | var dataLabels: DataLabelOptions 20 | var fill: FillOptions 21 | var grid: GridOptions 22 | var legend: LegendOptions 23 | var plotOptions: PlotOptions 24 | var series: Array 25 | var stroke: StrokeOptions 26 | var tooltip: TooltipOptions 27 | var xaxis: AxisOptions 28 | var yaxis: AxisOptions 29 | var title: TitleOptions 30 | // var annotations: AnnotationsOptions 31 | var colors: Array 32 | } 33 | 34 | external interface AnnotationsOptions { 35 | var yaxis: Array 36 | } 37 | 38 | external interface AnnotationYaxisOptions { 39 | var y: Long 40 | var y2: Long 41 | var borderColor: String 42 | var fillColor: String 43 | var label: AnnotationLabelOptions 44 | var strokeDashArray: Array 45 | } 46 | 47 | external interface AnnotationLabelOptions { 48 | var borderColor: String 49 | var text: String 50 | var style: AnnotationLabelStyleOptions 51 | } 52 | 53 | external interface AnnotationLabelStyleOptions { 54 | var color: String 55 | var background: String 56 | } 57 | 58 | external interface AxisLabelOptions { 59 | var style: AxisLabelStyleOptions 60 | var formatter: NumberFormatter 61 | } 62 | 63 | external interface AxisLabelStyleOptions { 64 | var fontSize: Int 65 | } 66 | 67 | external interface AxisOptions { 68 | var categories: Array 69 | var labels: AxisLabelOptions 70 | } 71 | 72 | external interface BarPlotOptions { 73 | var horizontal: Boolean 74 | } 75 | 76 | external interface ChartOptions { 77 | var fontFamily: String 78 | var height: Int 79 | var toolbar: ToolbarOptions 80 | var type: String 81 | var zoom: ChartZoomOptions 82 | } 83 | 84 | external interface ChartZoomOptions { 85 | var enabled: Boolean 86 | } 87 | 88 | external interface DataLabelOptions { 89 | var enabled: Boolean 90 | var formatter: NumberFormatter 91 | } 92 | 93 | external interface FillOptions { 94 | var opacity: Double 95 | } 96 | 97 | external interface GridAxisLineOptions { 98 | var show: Boolean 99 | } 100 | 101 | external interface GridAxisOptions { 102 | var lines: GridAxisLineOptions 103 | } 104 | 105 | external interface GridRowOptions { 106 | var colors: Array 107 | var opacity: Float 108 | } 109 | 110 | external interface GridOptions { 111 | var xaxis: GridAxisOptions 112 | var yaxis: GridAxisOptions 113 | var row: GridRowOptions 114 | } 115 | 116 | external interface LegendMarkerOptions { 117 | var width: Int 118 | var height: Int 119 | } 120 | 121 | external interface LegendOptions { 122 | var fontSize: Int 123 | var markers: LegendMarkerOptions 124 | } 125 | 126 | external interface PlotOptions { 127 | var bar: BarPlotOptions 128 | } 129 | 130 | external interface Series { 131 | var type: String 132 | var name: String 133 | var data: Array 134 | } 135 | 136 | external interface StrokeOptions { 137 | var show: Boolean 138 | var colors: Array 139 | var width: Int 140 | var curve: String 141 | } 142 | 143 | external interface ToolbarOptions { 144 | var show: Boolean 145 | } 146 | 147 | external interface TooltipAxisFormatterOptions { 148 | var series: Array> 149 | var seriesIndex: Int 150 | } 151 | 152 | external interface TooltipAxisOptions { 153 | var formatter: TooltipAxisFormatter 154 | } 155 | 156 | external interface TooltipOptions { 157 | var x: TooltipAxisOptions 158 | var y: TooltipAxisOptions 159 | } 160 | 161 | external interface TitleOptions { 162 | var text: String 163 | var align: String 164 | } 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/chart/BarChartConfig.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.chart 2 | 3 | import top.omooo.frontend.util.formatPercentage 4 | 5 | /** Chart config for bar charts. */ 6 | @Suppress("LongParameterList") 7 | class BarChartConfig( 8 | private val chartLabels: Array, 9 | private val chartSeries: Array, 10 | private val chartHeight: Int, 11 | private val horizontal: Boolean = false, 12 | private val xAxisFormatter: NumberFormatter = Number::toString, 13 | private val yAxisFormatter: NumberFormatter = Number::toString, 14 | private val chartSeriesTotals: LongArray? = null, 15 | ) : ChartConfig() { 16 | 17 | override fun getOptions() = buildOptions { 18 | series = chartSeries 19 | xaxis.categories = chartLabels 20 | chart.type = "bar" 21 | chart.height = chartHeight 22 | 23 | grid.xaxis.lines.show = horizontal 24 | grid.yaxis.lines.show = !horizontal 25 | plotOptions.bar.horizontal = horizontal 26 | 27 | xaxis.labels.formatter = xAxisFormatter 28 | yaxis.labels.formatter = yAxisFormatter 29 | tooltip.y.formatter = ::formatTooltip 30 | } 31 | 32 | private fun formatTooltip(number: Number, options: TooltipAxisFormatterOptions): String { 33 | val axisFormatter = if (horizontal) xAxisFormatter else yAxisFormatter 34 | val total = if (chartSeriesTotals != null) { 35 | chartSeriesTotals[options.seriesIndex] 36 | } else { 37 | options.series[options.seriesIndex].sumOf(Number::toLong) 38 | } 39 | return "${axisFormatter.invoke(number)} (${formatPercentage(number, total)})" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/chart/ChartConfig.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.chart 2 | 3 | import js.core.jso 4 | 5 | /** Base config for displaying charts. Check https://apexcharts.com/docs/options/ for all chart types and options. */ 6 | abstract class ChartConfig { 7 | 8 | /** Returns the chart options for this config used by ApexCharts. */ 9 | abstract fun getOptions(): ApexChartOptions 10 | 11 | /** Utility function which allows concrete configs to start with a common sets of defaults. */ 12 | protected fun buildOptions(builder: ApexChartOptions.() -> Unit) = jso { 13 | chart = jso { 14 | fontFamily = FONT_FAMILY 15 | toolbar = jso { 16 | show = false 17 | } 18 | zoom = jso { 19 | enabled = false 20 | } 21 | } 22 | dataLabels = jso { 23 | enabled = false 24 | formatter = jso() 25 | } 26 | fill = jso { 27 | opacity = 1.0 28 | } 29 | grid = jso { 30 | xaxis = jso { 31 | lines = jso() 32 | } 33 | yaxis = jso { 34 | lines = jso() 35 | } 36 | row = jso { 37 | colors = jso() 38 | opacity = jso() 39 | } 40 | } 41 | legend = jso { 42 | fontSize = FONT_SIZE 43 | markers = jso { 44 | width = FONT_SIZE 45 | height = FONT_SIZE 46 | } 47 | } 48 | plotOptions = jso { 49 | bar = jso() 50 | } 51 | stroke = jso { 52 | show = true 53 | colors = arrayOf("transparent") 54 | width = STROKE_WIDTH 55 | curve = jso() 56 | } 57 | tooltip = jso { 58 | x = jso() 59 | y = jso() 60 | } 61 | xaxis = jso { 62 | labels = jso { 63 | style = jso { 64 | fontSize = FONT_SIZE 65 | } 66 | } 67 | } 68 | yaxis = jso { 69 | labels = jso { 70 | style = jso { 71 | fontSize = FONT_SIZE 72 | } 73 | } 74 | } 75 | title = jso { 76 | text = jso() 77 | align = jso() 78 | } 79 | // annotations = jso { 80 | // yaxis = arrayOf( 81 | // jso { 82 | // y = jso() 83 | // y2 = jso() 84 | // borderColor = jso() 85 | // fillColor = jso() 86 | // label = jso { 87 | // borderColor = jso() 88 | // text = jso() 89 | // style = jso { 90 | // color = jso() 91 | // background = jso() 92 | // } 93 | // } 94 | // strokeDashArray = jso() 95 | // } 96 | // ) 97 | // } 98 | colors = jso() 99 | }.apply(builder) 100 | 101 | private companion object { 102 | const val FONT_FAMILY = "var(--bs-body-font-family)" 103 | const val FONT_SIZE = 14 104 | const val STROKE_WIDTH = 3 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/chart/ChartUtils.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.chart 2 | 3 | import js.core.jso 4 | 5 | fun seriesOf(name: String, data: LongArray): Series = jso { 6 | this.name = name 7 | this.data = data.map(Long::toInt).toTypedArray() 8 | } 9 | 10 | fun annotationYaxisOptionsOf(y: Long, desc: String): AnnotationYaxisOptions = jso { 11 | this.y = y 12 | this.borderColor = "#00E396" 13 | this.label = jso { 14 | this.text = desc 15 | this.borderColor = "#00E396" 16 | this.style = jso { 17 | color = "#FFFFFF" 18 | background = "#00E396" 19 | } 20 | } 21 | this.strokeDashArray = arrayOf(5f, 5f) 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/chart/ChartsComponent.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.chart 2 | 3 | import kotlinx.browser.document 4 | import react.FC 5 | import react.Props 6 | import react.dom.html.ReactHTML.div 7 | import react.useEffect 8 | import top.omooo.frontend.util.formatSize 9 | import kotlin.math.roundToLong 10 | 11 | val ChartsComponent = FC { props-> 12 | div { 13 | id = "id-charts" 14 | val thresholdSize = props.chartSeries.second.find { 15 | it != 0L 16 | }?.times(1.2f)?.roundToLong() ?: 0L 17 | val thresholdSeries = props.chartSeries.second.map { 18 | if (it != 0L) thresholdSize else it 19 | }.toLongArray() 20 | val config = LineChartConfig( 21 | chartLabels = props.chartLabels, 22 | chartSeries = arrayOf( 23 | seriesOf(props.chartSeries.first, props.chartSeries.second), 24 | seriesOf("阈值", thresholdSeries), 25 | ), 26 | chartHeight = 350, 27 | yAxisFormatter = Number::formatSize, 28 | ) 29 | useEffect { 30 | val chart = ApexCharts(document.getElementById("id-charts"), config.getOptions()) 31 | chart.render() 32 | cleanup { 33 | chart.destroy() 34 | } 35 | } 36 | 37 | } 38 | } 39 | 40 | external interface ChartsComponentProps : Props { 41 | var chartSeries: Pair 42 | var chartLabels: Array 43 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/chart/LineChartConfig.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.chart 2 | 3 | /** 4 | * Chart config for line charts. 5 | * 文档:https://apexcharts.com/javascript-chart-demos/line-charts/basic/ 6 | */ 7 | @Suppress("LongParameterList") 8 | class LineChartConfig( 9 | private val chartLabels: Array, 10 | private val chartSeries: Array, 11 | private val chartHeight: Int, 12 | private val xAxisFormatter: NumberFormatter = Number::toString, 13 | private val yAxisFormatter: NumberFormatter = Number::toString, 14 | ) : ChartConfig() { 15 | 16 | override fun getOptions() = buildOptions { 17 | series = chartSeries 18 | xaxis.categories = chartLabels 19 | chart.type = "line" 20 | chart.height = chartHeight 21 | chart.zoom.enabled = false 22 | 23 | grid.xaxis.lines.show = true 24 | 25 | grid.row.apply { 26 | colors = arrayOf("#f3f3f3", "transparent") 27 | opacity = 0.5f 28 | } 29 | 30 | dataLabels.enabled = true 31 | dataLabels.formatter = yAxisFormatter 32 | 33 | yaxis.labels.formatter = yAxisFormatter 34 | 35 | stroke.apply { 36 | show = true 37 | curve = "straight" 38 | colors = arrayOf("#4095E5", "#BD3124") 39 | } 40 | title.apply { 41 | text = "阈值为最初版本的 120% 大小" 42 | align = "middle" 43 | } 44 | colors = arrayOf("#4095E5", "#BD3124") 45 | // annotations.yaxis = arrayOf(annotationYaxisOptionsOf(5, "阈值")) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/common/Header.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.common 2 | 3 | import csstype.integer 4 | import csstype.number 5 | import kotlinx.browser.window 6 | import mui.icons.material.Brightness4 7 | import mui.icons.material.Brightness7 8 | import mui.icons.material.MenuBook 9 | import mui.material.* 10 | import mui.material.styles.TypographyVariant.h6 11 | import mui.system.sx 12 | import react.* 13 | import react.dom.html.ReactHTML.div 14 | 15 | /** 16 | * Author: Omooo 17 | * Date: 2023/2/1 18 | * Desc: 通用顶部 Header 19 | */ 20 | 21 | external interface HeaderProps : Props { 22 | /** 标题 */ 23 | var title: String 24 | 25 | /** 文档链接 */ 26 | var documentLink: String 27 | } 28 | 29 | val Header = FC { props -> 30 | var theme by useContext(ThemeContext) 31 | 32 | AppBar { 33 | position = AppBarPosition.sticky 34 | sx { 35 | zIndex = integer(1_500) 36 | } 37 | 38 | Toolbar { 39 | Typography { 40 | sx { flexGrow = number(1.0) } 41 | variant = h6 42 | noWrap = true 43 | component = div 44 | 45 | +props.title 46 | } 47 | 48 | Tooltip { 49 | title = ReactNode("Theme") 50 | 51 | Switch { 52 | icon = Brightness7.create() 53 | checkedIcon = Brightness4.create() 54 | checked = theme == Themes.Dark 55 | 56 | onChange = { _, checked -> 57 | theme = if (checked) Themes.Dark else Themes.Light 58 | } 59 | } 60 | } 61 | 62 | Tooltip { 63 | title = ReactNode("Read Documentation") 64 | 65 | IconButton { 66 | size = Size.large 67 | color = IconButtonColor.inherit 68 | onClick = { 69 | window.open(props.documentLink) 70 | } 71 | 72 | MenuBook() 73 | } 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/common/MissedWrapper.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.common 2 | 3 | import mui.material.GridProps 4 | import mui.material.TypographyProps 5 | 6 | /** 7 | * Author: Omooo 8 | * Date: 2023/7/17 9 | * Desc: Remove when it will be implemented in MUI wrappers 10 | */ 11 | 12 | inline var GridProps.xs: Int 13 | get() = TODO("Prop is write-only!") 14 | set(value) { 15 | asDynamic().xs = value 16 | } 17 | 18 | inline var TypographyProps.color: String 19 | get() = TODO("Prop is write-only!") 20 | set(value) { 21 | asDynamic().color = value 22 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/common/ThemeModule.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.common 2 | 3 | import mui.material.CssBaseline 4 | import mui.material.styles.Theme 5 | import mui.material.styles.ThemeProvider 6 | import react.* 7 | 8 | typealias ThemeState = StateInstance 9 | 10 | val ThemeContext = createContext() 11 | 12 | val ThemeModule = FC { props -> 13 | val state = useState(Themes.Light) 14 | val (theme) = state 15 | 16 | ThemeContext(state) { 17 | ThemeProvider { 18 | this.theme = theme 19 | 20 | CssBaseline() 21 | +props.children 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/common/Themes.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.common 2 | 3 | import js.core.jso 4 | import mui.material.PaletteMode.dark 5 | import mui.material.PaletteMode.light 6 | import mui.material.styles.createTheme 7 | 8 | object Themes { 9 | val Light = createTheme( 10 | jso { 11 | palette = jso { mode = light } 12 | } 13 | ) 14 | 15 | val Dark = createTheme( 16 | jso { 17 | palette = jso { mode = dark } 18 | } 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/component/AarAccordion.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.component 2 | 3 | import csstype.* 4 | import mui.icons.material.ExpandMore 5 | import mui.material.* 6 | import mui.material.Size 7 | import mui.system.sx 8 | import react.FC 9 | import react.Props 10 | import react.ReactNode 11 | import react.create 12 | import top.omooo.frontend.bean.AarFile 13 | import top.omooo.frontend.bean.AppFile 14 | import top.omooo.frontend.util.formatSize 15 | 16 | /** 17 | * Author: Omooo 18 | * Date: 2023/2/1 19 | * Desc: Aar 可展开列表 20 | */ 21 | 22 | external interface AarAccordionProps : Props { 23 | var aarList: List 24 | } 25 | 26 | val AarAccordion = FC { props -> 27 | props.aarList.forEach { aarData -> 28 | Accordion { 29 | sx { 30 | paddingLeft = 20.px 31 | flexGrow = number(1.0) 32 | } 33 | AccordionSummary { 34 | expandIcon = ExpandMore.create() 35 | Typography { 36 | +aarData.name 37 | } 38 | Chip { 39 | sx { 40 | marginLeft = 18.px 41 | } 42 | size = Size.small 43 | label = ReactNode(aarData.owner) 44 | variant = ChipVariant.outlined 45 | } 46 | if (aarData.size != 0L) { 47 | Typography { 48 | sx { 49 | flexGrow = number(1.0) 50 | marginRight = 5.px 51 | textAlign = TextAlign.right 52 | } 53 | +aarData.size.formatSize() 54 | } 55 | } 56 | } 57 | AccordionDetails { 58 | AppFileList { 59 | list = aarData.fileList.sortedByDescending { 60 | it.size 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | external interface AppFileListProps : Props { 69 | var list: List 70 | } 71 | 72 | private val AppFileList = FC { props -> 73 | List { 74 | sx { 75 | paddingRight = 10.px 76 | } 77 | props.list.forEach { appFile -> 78 | ListItem { 79 | ListItemText { 80 | +appFile.name 81 | } 82 | if (appFile.size != 0L || appFile.desc.isNotEmpty()) { 83 | ListItemText { 84 | sx { 85 | textAlign = TextAlign.right 86 | } 87 | +if (appFile.size != 0L) appFile.size.formatSize() else appFile.desc 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/component/AarList.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.component 2 | 3 | import csstype.* 4 | import mui.material.Divider 5 | import mui.material.List 6 | import mui.material.ListItem 7 | import mui.material.ListItemText 8 | import mui.system.sx 9 | import react.FC 10 | import react.Props 11 | import top.omooo.frontend.bean.AarFile 12 | import top.omooo.frontend.util.formatSize 13 | 14 | /** 15 | * Author: Omooo 16 | * Date: 2023/7/17 17 | * Desc: Aar 列表 18 | */ 19 | 20 | val AarList = FC { props -> 21 | List { 22 | sx { 23 | paddingRight = 10.px 24 | } 25 | props.aarList.forEach { appFile -> 26 | ListItem { 27 | ListItemText { 28 | +appFile.name 29 | } 30 | ListItemText { 31 | sx { 32 | textAlign = TextAlign.right 33 | if (props.showDiff) { 34 | color = if (appFile.size < 0) Color("#4095E5") else Color("#BD3124") 35 | } 36 | } 37 | +"${if (!props.showDiff || appFile.size < 0) "" else "+"}${appFile.size.formatSize()}" 38 | } 39 | } 40 | Divider() 41 | } 42 | } 43 | } 44 | 45 | external interface AarListProps : Props { 46 | var showDiff: Boolean 47 | var aarList: List 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/component/AarTitle.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.component 2 | 3 | import csstype.AlignItems 4 | import csstype.Border 5 | import csstype.Color 6 | import csstype.LineStyle 7 | import csstype.px 8 | import emotion.react.css 9 | import mui.material.* 10 | import react.FC 11 | import react.Props 12 | import top.omooo.frontend.bean.AarAnalyseReporter 13 | import top.omooo.frontend.bean.totalSize 14 | import top.omooo.frontend.common.color 15 | import top.omooo.frontend.common.xs 16 | import top.omooo.frontend.util.formatSize 17 | 18 | /** 19 | * Author: Omooo 20 | * Date: 2023/7/17 21 | * Desc: AAR 标题 22 | */ 23 | 24 | val AarTitle = FC { props -> 25 | Box { 26 | css { 27 | border = Border(1.px, LineStyle.solid, Color("#C0C0C0")) 28 | } 29 | Grid { 30 | container = true 31 | css { 32 | alignItems = AlignItems.flexEnd 33 | } 34 | Grid { 35 | item = true 36 | xs = 8 37 | Box { 38 | css { 39 | padding = 16.px 40 | } 41 | Typography { 42 | +props.data.packageName 43 | } 44 | Typography { 45 | color = "text.secondary" 46 | +"Version: ${props.data.aarList.getOrNull(0)?.first} (previous: ${ 47 | props.data.aarList.getOrNull(1)?.first 48 | })" 49 | } 50 | } 51 | } 52 | Grid { 53 | item = true 54 | xs = 2 55 | Box { 56 | css { 57 | padding = 16.px 58 | } 59 | Typography { 60 | +"Total Count" 61 | } 62 | Typography { 63 | color = "text.secondary" 64 | +props.data.aarList.getOrNull(0)?.second.orEmpty().size.toString() 65 | } 66 | } 67 | } 68 | Grid { 69 | item = true 70 | xs = 2 71 | Box { 72 | css { 73 | padding = 16.px 74 | } 75 | Typography { 76 | +"Total Size" 77 | } 78 | Typography { 79 | color = "text.secondary" 80 | +props.data.aarList.getOrNull(0)?.second.orEmpty().totalSize().formatSize() 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | external interface AarTitleProps : Props { 89 | var data: AarAnalyseReporter 90 | } 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/component/OwnerSelects.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.component 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2023/2/5 6 | * Desc: 7 | */ 8 | import csstype.TextAlign 9 | import csstype.px 10 | import mui.material.* 11 | import mui.system.sx 12 | import react.FC 13 | import react.Props 14 | import react.ReactNode 15 | import react.useState 16 | 17 | /** 18 | * Author: Omooo 19 | * Date: 2023/2/5 20 | * Desc: Owner 下拉选择器 21 | */ 22 | 23 | external interface OwnerSelectsProps : Props { 24 | var ownerList: List 25 | var defaultSelect: String 26 | var onSelect: (String) -> Unit 27 | } 28 | 29 | val OwnerSelects = FC { props -> 30 | var owner by useState(props.defaultSelect) 31 | 32 | Box { 33 | sx { 34 | minWidth = 300.px 35 | marginRight = 18.px 36 | textAlign = TextAlign.center 37 | } 38 | FormControl { 39 | fullWidth = true 40 | InputLabel { 41 | id = "select-label" 42 | +"Owner" 43 | } 44 | Select { 45 | labelId = "select-label" 46 | id = "" 47 | value = owner 48 | label = ReactNode("Owner") 49 | onChange = { event, _ -> 50 | owner = event.target.value 51 | props.onSelect(event.target.value) 52 | } 53 | props.ownerList.forEach { 54 | MenuItem { 55 | value = it 56 | +it 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/component/Summary.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.component 2 | 3 | import csstype.Position 4 | import mui.material.* 5 | import mui.system.sx 6 | import react.FC 7 | import react.Props 8 | import react.create 9 | 10 | /** 11 | * Author: Omooo 12 | * Date: 2023/2/3 13 | * Desc: 14 | */ 15 | 16 | external interface SummaryProps : Props { 17 | var title: String 18 | var subtitle: String 19 | var ownerList: List 20 | var defaultSelect: String 21 | var onSelect: (String) -> Unit 22 | } 23 | 24 | val Summary = FC { props -> 25 | Alert { 26 | sx { 27 | position = Position.sticky 28 | } 29 | severity = AlertColor.info 30 | color = AlertColor.info 31 | 32 | AlertTitle { 33 | +props.title 34 | } 35 | +props.subtitle 36 | 37 | if (props.ownerList.isNotEmpty()) { 38 | action = OwnerSelects.create { 39 | ownerList = props.ownerList 40 | defaultSelect = props.defaultSelect 41 | onSelect = props.onSelect 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/page/ApkAnalysePage.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.page 2 | 3 | import kotlinext.js.require 4 | import kotlinx.serialization.json.Json 5 | import mui.system.Box 6 | import react.FC 7 | import react.Props 8 | import react.create 9 | import react.dom.client.createRoot 10 | import react.useState 11 | import top.omooo.frontend.bean.AppReporter 12 | import top.omooo.frontend.common.Header 13 | import top.omooo.frontend.common.ThemeModule 14 | import top.omooo.frontend.component.AarAccordion 15 | import top.omooo.frontend.component.Summary 16 | import top.omooo.frontend.util.* 17 | import web.dom.document 18 | 19 | fun main() { 20 | val text = require("./report.json").toString() 21 | val data = Json.decodeFromString(AppReporter.serializer(), text) 22 | createRoot(document.getElementById("root")!!).render( 23 | App.create { 24 | appReporter = data 25 | } 26 | ) 27 | } 28 | 29 | private external interface AppProps : Props { 30 | var appReporter: AppReporter 31 | } 32 | 33 | private val App = FC { props -> 34 | var owner by useState("none") 35 | ThemeModule { 36 | Box { 37 | Header { 38 | title = props.appReporter.desc 39 | documentLink = props.appReporter.documentLink 40 | } 41 | 42 | Summary { 43 | title = props.appReporter.aarList.filter { 44 | if (owner == "none") true else it.owner == owner 45 | }.let { list -> 46 | if ((list.firstOrNull()?.size ?: 0) > 0) { 47 | "A total of ${list.size} components belong to $owner, including ${ 48 | list.map { it.size }.reduce { acc, l -> acc + l }.formatSize() 49 | } of resources." 50 | } else { 51 | "A total of ${list.size} components belong to $owner, including ${ 52 | if (list.isEmpty()) { 53 | 0 54 | } else { 55 | list.map { it.fileList.size }.reduce { acc, l -> acc + l } 56 | } 57 | } items need to check." 58 | } 59 | } 60 | subtitle = props.appReporter.let { 61 | "Version: ${it.versionName} (${it.variantName})" 62 | } 63 | ownerList = mutableListOf("none").apply { 64 | addAll(props.appReporter.aarList.map { it.owner }.toSet()) 65 | } 66 | defaultSelect = "none" 67 | onSelect = { 68 | owner = it 69 | } 70 | } 71 | 72 | AarAccordion { 73 | aarList = props.appReporter.aarList.filter { 74 | if (owner == "none") true else it.owner == owner 75 | } 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /frontend/src/main/kotlin/top/omooo/frontend/util/Format.kt: -------------------------------------------------------------------------------- 1 | package top.omooo.frontend.util 2 | 3 | import kotlin.math.abs 4 | 5 | /** 6 | * Author: Omooo 7 | * Date: 2023/2/3 8 | * Desc: 数字相关格式化 9 | */ 10 | 11 | /** 12 | * 格式化数字 13 | * 14 | * ag: 21578 字节 -> 21.1 KB 15 | * ag: -21578 字节 -> -21.1 KB 16 | */ 17 | fun Number.formatSize(): String { 18 | val units = mutableListOf("B", "KB", "MB", "GB", "TB", "PB") 19 | var remainder = this.toDouble() 20 | var negative = remainder < 0 21 | if (remainder < 0) { 22 | remainder = abs(remainder) 23 | } 24 | while (remainder > BYTE_FACTOR) { 25 | remainder /= BYTE_FACTOR 26 | units.removeFirst() 27 | } 28 | return "${if (negative) "-" else ""}${remainder.asDynamic().toFixed(1)} ${units.first()}" 29 | } 30 | 31 | fun formatPercentage(fraction: Number, total: Number): String { 32 | val percentage = PERCENT_FACTOR * fraction.toDouble() / total.toDouble() 33 | return "${percentage.asDynamic().toFixed(2)} %" 34 | } 35 | 36 | private const val BYTE_FACTOR = 1024 37 | 38 | private const val PERCENT_FACTOR = 100 -------------------------------------------------------------------------------- /frontend/src/main/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Lavender-Frontend 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/src/main/resources/report.json: -------------------------------------------------------------------------------- 1 | {"key":"REPLACE_ME"} -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx3g 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | 23 | # display all warnings in sync.(mode: all/fail/none/summary) 24 | # https://docs.gradle.org/6.5.1/userguide/command_line_interface.html#sec:command_line_warnings 25 | Dorg.gradle.warning.mode=all 26 | 27 | #org.gradle.jvmargs=-XX:MaxPermSize=4g -XX:+HeapDumpOnOutOfMemoryError -Xmx4g -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006 28 | 29 | #org.gradle.configuration-cache=true -------------------------------------------------------------------------------- /gradle/plugin.gradle: -------------------------------------------------------------------------------- 1 | import com.omooo.buildsrc.Config 2 | 3 | apply plugin: 'kotlin' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | sourceSets { 7 | main { 8 | java { 9 | srcDirs += [] 10 | } 11 | kotlin { 12 | srcDirs += ['src/main/kotlin', 'src/main/java'] 13 | } 14 | } 15 | test { 16 | java { 17 | srcDirs += [] 18 | } 19 | kotlin { 20 | srcDirs += ['src/main/kotlin', 'src/main/java'] 21 | } 22 | } 23 | } 24 | 25 | compileKotlin { 26 | kotlinOptions.jvmTarget = JavaVersion.VERSION_17 27 | } 28 | 29 | compileTestKotlin { 30 | kotlinOptions.jvmTarget = JavaVersion.VERSION_17 31 | } 32 | 33 | dependencies { 34 | implementation "org.jetbrains.kotlin:kotlin-stdlib:${Config.kotlin_version}" 35 | implementation "org.jetbrains.kotlin:kotlin-reflect:${Config.kotlin_version}" 36 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Nov 14 16:11:02 CST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /lavender-plugin/ownership.yaml: -------------------------------------------------------------------------------- 1 | Omooo: 2 | - app-startup 3 | 4 | Android: 5 | - appcompat 6 | - material 7 | - recyclerview 8 | - fragment 9 | - constraintlayout-solver 10 | - constraintlayout 11 | 12 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | compileSdk 34 8 | namespace 'com.omooo.plugin.library' 9 | defaultConfig { 10 | minSdk 21 11 | targetSdk 34 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles "consumer-rules.pro" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation 'androidx.core:core-ktx:1.10.1' 27 | implementation 'androidx.appcompat:appcompat:1.6.1' 28 | implementation 'com.google.android.material:material:1.10.0' 29 | } 30 | 31 | kotlin { 32 | jvmToolchain(17) 33 | } -------------------------------------------------------------------------------- /library/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/library/consumer-rules.pro -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /library/src/androidTest/java/com/omooo/library/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.library 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.omooo.library.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /library/src/main/assets/library.json: -------------------------------------------------------------------------------- 1 | { 2 | ":library": [ 3 | { 4 | "fileName": "library.json", 5 | "size": 4 6 | }, 7 | { 8 | "fileName": "lottie/lottie.json", 9 | "size": 4 10 | } 11 | ], 12 | "app": [ 13 | { 14 | "fileName": "unused.json", 15 | "size": 4 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /library/src/main/assets/lottie/lottie.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /library/src/main/java/com/omooo/library/LibraryActivity.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.library 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import com.omooo.plugin.library.R 6 | 7 | /** 8 | * Author: Omooo 9 | * Date: 2024/4/28 10 | * Desc: 11 | */ 12 | internal class LibraryActivity : Activity() { 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | setContentView(R.layout.activity_library) 17 | } 18 | } -------------------------------------------------------------------------------- /library/src/main/java/com/omooo/library/LibraryMain.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.library 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | 6 | class LibraryMain { 7 | fun show(context: Context) { 8 | Toast.makeText(context, "LibraryMain", Toast.LENGTH_SHORT).show() 9 | } 10 | 11 | fun main(context: Context) { 12 | show(context) 13 | } 14 | } -------------------------------------------------------------------------------- /library/src/main/res/layout/activity_library.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /library/src/test/java/com/omooo/library/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.library 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | src/main/resources/ownership.yaml 3 | -------------------------------------------------------------------------------- /plugin/build.gradle: -------------------------------------------------------------------------------- 1 | //apply from: '../gradle/plugin.gradle' 2 | 3 | apply plugin: 'kotlin' 4 | apply plugin: 'kotlin-kapt' 5 | apply plugin: 'maven-publish' 6 | apply plugin: 'java-library' 7 | apply plugin: 'kotlinx-serialization' 8 | 9 | dependencies { 10 | 11 | implementation localGroovy() 12 | implementation gradleApi() 13 | 14 | implementation "com.android.tools.build:gradle-api:8.2.0" 15 | compileOnly "com.android.tools.build:gradle:8.2.0" 16 | implementation 'org.json:json:20220924' 17 | implementation "com.google.auto.service:auto-service:1.0.1" 18 | kapt "com.google.auto.service:auto-service:1.0.1" 19 | compileOnly 'com.android.tools:common:30.4.1' 20 | compileOnly 'com.android.tools:sdklib:30.4.1' 21 | 22 | implementation "org.ow2.asm:asm:9.4" 23 | implementation "org.ow2.asm:asm-tree:9.4" 24 | implementation "org.ow2.asm:asm-util:9.4" 25 | implementation "org.ow2.asm:asm-commons:9.4" 26 | 27 | implementation "org.yaml:snakeyaml:1.30" 28 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1") 29 | implementation("org.smali:dexlib2:2.5.2") 30 | implementation("com.android.tools.apkparser:apkanalyzer:30.1.2") { 31 | exclude group: 'com.android.tools.lint' 32 | } 33 | } 34 | 35 | //sourceSets.main { 36 | // resources.srcDir(project(":frontend").tasks.named("browserDistribution")) 37 | //} 38 | 39 | java { 40 | sourceCompatibility = JavaVersion.VERSION_17 41 | targetCompatibility = JavaVersion.VERSION_17 42 | } 43 | 44 | kotlin { 45 | jvmToolchain(17) 46 | } 47 | 48 | publishing { 49 | publications { 50 | mavenJava(MavenPublication) { 51 | from components.java 52 | groupId = 'com.omooo' 53 | artifactId = 'lavender' 54 | version = '0.0.1' 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/bean/CheckSchemeModifiedExtension.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.bean 2 | 3 | import com.omooo.plugin.task.CheckSchemeModifiedTask 4 | import java.io.File 5 | 6 | /** 7 | * Author: Omooo 8 | * Date: 2023/08/22 9 | * Desc: [CheckSchemeModifiedTask] 配置 10 | */ 11 | open class CheckSchemeModifiedExtension { 12 | 13 | /** 开启该任务,默认不开启 */ 14 | var enable = false 15 | 16 | /** 基线对比文件 */ 17 | var baselineSchemeFile: File? = null 18 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/bean/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.bean 2 | 3 | import org.objectweb.asm.Opcodes 4 | 5 | /** LAVENDER */ 6 | internal const val LAVENDER = "lavender" 7 | /** ASM 版本号 */ 8 | internal const val ASM_VERSION = Opcodes.ASM9 9 | 10 | /** gradle properties key artifact id */ 11 | internal const val KEY_ARTIFACT_ID = "POM_ARTIFACT_ID" -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/bean/InvokeCheckExtension.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.bean 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2022/11/6 6 | * Desc: 检测方法调用的扩展类,使用方配置 7 | */ 8 | open class InvokeCheckExtension { 9 | /** 10 | * 方法调用列表(方法的全限定名) 11 | * 12 | * ag: ["android/widget/Toast#show()V", "xxx"] 13 | */ 14 | var methodList = arrayOf() 15 | 16 | /** 17 | * 包名列表 18 | * 19 | * ag: ["android/", "xxx"] 20 | */ 21 | var packageList = arrayOf() 22 | 23 | /** 24 | * 常量列表 25 | * 26 | * ag: ["android.permission.READ_EXTERNAL_STORAGE", "xxx"] 27 | */ 28 | var constantsList = arrayOf() 29 | 30 | /** 31 | * 字段调用列表(字段的全限定名) 32 | * 33 | * ag: ["android.os.Build$VERSION.SDK_INT:I", "xxx"] 34 | */ 35 | var fieldList = arrayOf() 36 | 37 | 38 | 39 | /* -------------------------- internal ----------------------- */ 40 | 41 | /** 42 | * 是否开启,true: 开启 43 | * 校验条件: 检测列表有一项不为空 44 | */ 45 | internal fun enable(): Boolean { 46 | return methodList.isNotEmpty() || packageList.isNotEmpty() 47 | || constantsList.isNotEmpty() || fieldList.isNotEmpty() 48 | } 49 | 50 | /** 51 | * 获取方法列表 52 | * 53 | * @return Tripe 54 | */ 55 | internal fun getMethodList(): List> { 56 | return methodList.filter { 57 | it.isNotEmpty() 58 | }.map { 59 | val owner = it.substringBefore("#").replace(".", "/") 60 | val name = it.substringAfter("#", "").substringBefore("(") 61 | val desc = if (name.isNotEmpty()) it.substringAfterLast(name, "") else "" 62 | Triple(owner, name, desc) 63 | } 64 | } 65 | 66 | /** 67 | * 获取包名列表 68 | */ 69 | internal fun getPackageList(): List { 70 | return packageList.filter { 71 | it.isNotEmpty() 72 | }.map { 73 | it.replace(".", "/") 74 | } 75 | } 76 | 77 | /** 78 | * 获取字段列表 79 | * 80 | * @return Tripe 81 | */ 82 | internal fun getFieldList(): List> { 83 | return fieldList.filter { 84 | it.isNotEmpty() 85 | }.map { 86 | val owner = it.substringBeforeLast(".").replace(".", "/") 87 | val name = it.substringBefore(":").substringAfterLast(".") 88 | val desc = it.substringAfter(":") 89 | Triple(owner, name, desc) 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/bean/WebpToolBean.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.bean 2 | 3 | import java.io.File 4 | 5 | /** 6 | * Created by Omooo 7 | * Date: 2020-02-13 8 | * Desc: cwebp 工具路径 9 | */ 10 | object WebpToolBean { 11 | private lateinit var rootDir: String 12 | 13 | fun setRootDir(rootDir: String) { 14 | this.rootDir = rootDir 15 | } 16 | 17 | fun getRootDirPath(): String { 18 | return rootDir 19 | } 20 | 21 | fun getToolsDir(): File { 22 | return File("$rootDir/tools/cwebp") 23 | } 24 | 25 | fun getToolsDirPath(): String { 26 | return "$rootDir/tools/cwebp" 27 | } 28 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/internal/ArtifactType.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.internal 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2024/5/7 6 | * Desc: 产物类型 7 | */ 8 | internal enum class ArtifactType(val type: String, val prefix: String) { 9 | CLASS("android-classes", ""), 10 | RES("android-res", "res"), 11 | ASSETS("android-assets", "assets"), 12 | NATIVE_LIB("android-jni", "lib"), 13 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/internal/aar/AarAnalyse.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.internal.aar 2 | 3 | import com.omooo.plugin.reporter.AarAnalyseReporter 4 | import com.omooo.plugin.reporter.Insight 5 | import com.omooo.plugin.reporter.common.AarFile 6 | import com.omooo.plugin.util.getArtifactIdFromAarName 7 | import com.omooo.plugin.util.getOwnerMap 8 | import com.omooo.plugin.util.getOwnerShip 9 | import com.omooo.plugin.util.writeToJson 10 | import kotlinx.serialization.json.Json 11 | import org.gradle.api.Project 12 | import java.io.File 13 | 14 | /** 15 | * Author: Omooo 16 | * Date: 2023/07/25 17 | * Desc: AAR 配额分析 18 | */ 19 | internal class AarAnalyse(private val project: Project) { 20 | 21 | private val previousDataPath: String by lazy { 22 | "${project.parent?.projectDir}/lavender-plugin/aar/previous.json" 23 | } 24 | 25 | /** 26 | * 增量、趋势分析 27 | * 28 | * @param pkgName 包名 29 | * @param currentAarPair 当前 AAR 数据 30 | * 31 | * @return 返回分析报告 32 | */ 33 | fun analyse(pkgName: String, currentAarPair: Pair>): AarAnalyseReporter { 34 | val ownerMap = project.getOwnerMap() 35 | val reporter = getPreviousReporter().apply { 36 | this.packageName = pkgName 37 | if (this.aarList.getOrNull(0)?.first == currentAarPair.first) { 38 | this.aarList.removeAt(0) 39 | } 40 | this.aarList.add(0, currentAarPair) 41 | // 说明 owner 配置文件发生变更,则需要重新给 AAR 打 owner 标签 42 | if (ownerMap != this.ownerMap) { 43 | this.ownerMap = ownerMap 44 | val ownership = project.getOwnerShip() 45 | aarList.flatMap { it.second }.forEach { 46 | it.owner = ownership.getOrDefault(it.name.getArtifactIdFromAarName(), "unknown") 47 | } 48 | } 49 | } 50 | if (project.hasProperty("forceRefresh")) { 51 | val f = File(previousDataPath) 52 | if (f.exists()) { 53 | f.delete() 54 | } 55 | f.mkdirs() 56 | reporter.writeToJson(previousDataPath) 57 | } 58 | return reporter 59 | } 60 | 61 | /** 62 | * 获取上一个版本的报告 63 | */ 64 | private fun getPreviousReporter(): AarAnalyseReporter { 65 | val jsonFile = project.parent?.projectDir?.resolve(previousDataPath) 66 | if (jsonFile?.exists() == true && jsonFile.readText().isNotEmpty()) { 67 | return Json.decodeFromString(AarAnalyseReporter.serializer(), jsonFile.readText()) 68 | } 69 | return AarAnalyseReporter( 70 | desc = Insight.Title.AAR_ANALYSE, 71 | documentLink = Insight.DocumentLink.AAR_ANALYSE, 72 | packageName = "", 73 | aarList = ArrayList(), 74 | ownerMap = project.getOwnerMap(), 75 | ) 76 | } 77 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/internal/apk/ApkIncrementAnalyse.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.internal.apk 2 | 3 | import com.omooo.plugin.reporter.common.AarFile 4 | import com.omooo.plugin.reporter.common.AppFile 5 | import com.omooo.plugin.reporter.common.totalSize 6 | import com.omooo.plugin.util.writeToJson 7 | import kotlinx.serialization.builtins.ListSerializer 8 | import kotlinx.serialization.json.Json 9 | import org.gradle.api.Project 10 | import java.io.File 11 | 12 | /** 13 | * Author: Omooo 14 | * Date: 2023/4/3 15 | * Desc: APK 增量分析 16 | */ 17 | internal class ApkIncrementAnalyse(private val project: Project) { 18 | 19 | private val previousDataPath: String by lazy { 20 | "${project.parent?.projectDir}/lavender-plugin/apk/previous.json" 21 | } 22 | 23 | /** 24 | * 增量分析 25 | * 26 | * @return 返回差异列表 27 | */ 28 | fun analyse(currentList: List): List { 29 | val previousList = getPreviousAarFileList() 30 | if (previousList.isEmpty() || project.hasProperty("forceRefresh")) { 31 | val f = File(previousDataPath) 32 | if (f.exists()) { 33 | f.delete() 34 | } 35 | f.mkdirs() 36 | return currentList.apply { 37 | writeToJson(previousDataPath) 38 | } 39 | } 40 | val map = previousList.associateBy { it.name.substringBeforeLast(":") } 41 | val result = mutableListOf() 42 | currentList.forEach { aarFile -> 43 | // 过滤掉版本号 44 | // com.xxx:xx:2.8.0 -> com.xxx:xx 45 | val aarId = aarFile.name.substringBeforeLast(":") 46 | if (map.containsKey(aarId)) { 47 | separateChange(aarFile.fileList, map[aarId]!!.fileList).takeIf { 48 | it.isNotEmpty() 49 | }?.let { 50 | result.add( 51 | AarFile(aarFile.name, it.totalSize(), aarFile.owner, it.toMutableList()) 52 | ) 53 | } 54 | } else { 55 | result.add(aarFile) 56 | } 57 | } 58 | return result.sortedByDescending { it.size } 59 | } 60 | 61 | /** 62 | * 分离变更 63 | */ 64 | private fun separateChange(cList: List, pList: List): List { 65 | val map = pList.associateBy { it.name.substringBeforeLast(":") } 66 | return cList.filterNot { 67 | map.containsKey(it.name.substringBeforeLast(":")) 68 | } 69 | } 70 | 71 | private fun getPreviousAarFileList(): List { 72 | val jsonFile = project.parent?.projectDir?.resolve(previousDataPath) 73 | if (jsonFile?.exists() == true && jsonFile.readText().isNotEmpty()) { 74 | return Json.decodeFromString(ListSerializer(AarFile.serializer()), jsonFile.readText()) 75 | } 76 | return emptyList() 77 | } 78 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/internal/apk/ApkParser.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.internal.apk 2 | 3 | import com.android.SdkConstants 4 | import com.android.tools.apk.analyzer.dex.DexFiles 5 | import com.omooo.plugin.reporter.common.AppFile 6 | import com.omooo.plugin.reporter.common.FileType 7 | import java.io.File 8 | import java.util.zip.ZipFile 9 | 10 | /** 11 | * Author: Omooo 12 | * Date: 2023/3/17 13 | * Desc: APK 解析 14 | */ 15 | internal class ApkParser { 16 | 17 | /** 18 | * 解析 Apk 19 | */ 20 | fun parse(apkFile: File): List { 21 | val result = mutableListOf() 22 | ZipFile(apkFile).use { zipFile -> 23 | zipFile.entries().iterator().forEach { entry -> 24 | if (entry.name.endsWith(SdkConstants.DOT_DEX, true)) { 25 | result.addAll(parseDex(zipFile.getInputStream(entry).readBytes())) 26 | } else { 27 | result.add(AppFile(entry.name, entry.compressedSize)) 28 | } 29 | } 30 | } 31 | return result 32 | } 33 | 34 | /** 35 | * 解析 Dex 文件 36 | */ 37 | private fun parseDex(byteArray: ByteArray): List { 38 | return DexFiles.getDexFile(byteArray).classes.map { 39 | AppFile(it.type, it.size.toLong(), fileType = FileType.CLASS) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/internal/apk/AppFileCleaner.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.internal.apk 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.Variant 5 | import com.omooo.plugin.reporter.common.AppFile 6 | import com.omooo.plugin.util.project 7 | import com.omooo.plugin.util.variantImpl 8 | 9 | /** 10 | * Author: Omooo 11 | * Date: 2023/3/19 12 | * Desc: [AppFile] 清洗 13 | */ 14 | 15 | internal fun List.clear(variant: Variant): List { 16 | 17 | val mappingFile = variant.variantImpl.artifacts.get(SingleArtifact.OBFUSCATION_MAPPING_FILE) 18 | val clearList: List = listOf( 19 | ClassCleaner(mappingFile.get().asFile), 20 | ResourceCleaner(variant.project.buildDir, variant), 21 | TypeAssigningCleaner() 22 | ) 23 | return clearList.flatMap { cleaner -> 24 | this.filter { 25 | cleaner.isApplicable(it) 26 | }.map { 27 | cleaner.clean(it) 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/internal/apk/ClassCleaner.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.internal.apk 2 | 3 | import com.android.tools.proguard.ProguardMap 4 | import com.omooo.plugin.reporter.common.AppFile 5 | import com.omooo.plugin.reporter.common.FileType 6 | import com.omooo.plugin.util.formatDollar 7 | import java.io.File 8 | 9 | /** 10 | * Author: Omooo 11 | * Date: 2023/3/17 12 | * Desc: 类文件解混淆 13 | */ 14 | internal class ClassCleaner(mappingFile: File) : ICleaner { 15 | 16 | private val proguardMap = ProguardMap().apply { 17 | readFromFile(mappingFile) 18 | } 19 | 20 | override fun isApplicable(appFile: AppFile): Boolean { 21 | return appFile.fileType == FileType.CLASS 22 | } 23 | 24 | override fun clean(appFile: AppFile): AppFile { 25 | val className = appFile.name 26 | .removeSurrounding("L", ";") 27 | .removeSuffix(".class") 28 | .replace("/", ".") 29 | return appFile.apply { 30 | name = proguardMap.getClassName(className).formatDollar() 31 | fileType = FileType.CLASS 32 | } 33 | } 34 | 35 | 36 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/internal/apk/ICleaner.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.internal.apk 2 | 3 | import com.omooo.plugin.reporter.common.AppFile 4 | 5 | /** 6 | * Author: Omooo 7 | * Date: 2023/3/17 8 | * Desc: 解混淆、类型赋值等 9 | */ 10 | internal interface ICleaner { 11 | 12 | fun isApplicable(appFile: AppFile): Boolean 13 | 14 | fun clean(appFile: AppFile): AppFile 15 | 16 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/internal/apk/TypeAssigningCleaner.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.internal.apk 2 | 3 | import com.omooo.plugin.reporter.common.AppFile 4 | import com.omooo.plugin.reporter.common.FileType 5 | 6 | /** 7 | * Author: Omooo 8 | * Date: 2023/3/19 9 | * Desc: 类型赋值 10 | */ 11 | internal class TypeAssigningCleaner : ICleaner { 12 | 13 | override fun isApplicable(appFile: AppFile): Boolean { 14 | return appFile.fileType == FileType.OTHER 15 | } 16 | 17 | override fun clean(appFile: AppFile): AppFile { 18 | return appFile.apply { 19 | if (name.startsWith("lib/")) { 20 | fileType = FileType.NATIVE_LIB 21 | } 22 | if (name.startsWith("assets/")) { 23 | fileType = FileType.ASSET 24 | } 25 | } 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/internal/cha/ClassSetCache.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.internal.cha 2 | 3 | import org.objectweb.asm.ClassReader 4 | import org.objectweb.asm.tree.ClassNode 5 | import java.io.File 6 | import java.io.FileInputStream 7 | import java.io.InputStream 8 | import java.util.zip.ZipInputStream 9 | 10 | /** 11 | * Author: Omooo 12 | * Date: 2023/3/12 13 | * Desc: 14 | */ 15 | internal class ClassSetCache(private val file: File) { 16 | 17 | private val classCacheMap: Map by lazy { 18 | loadClasses(ZipInputStream(FileInputStream(file))).associateBy { 19 | it.name 20 | } 21 | } 22 | 23 | fun get(className: String): ClassNode? { 24 | return classCacheMap[className] 25 | } 26 | 27 | private fun loadClasses(zip: ZipInputStream): List { 28 | fun parse(input: InputStream): ClassNode = ClassNode().also { klass -> 29 | ClassReader(input.readBytes()).accept(klass, 0) 30 | } 31 | val classes = mutableListOf() 32 | while (true) { 33 | val entry = zip.nextEntry ?: break 34 | classes += when { 35 | entry.name.endsWith(".class", true) -> listOf(parse(zip)) 36 | entry.name == "classes.jar" -> loadClasses(ZipInputStream(zip)) 37 | else -> emptyList() 38 | } 39 | } 40 | return classes 41 | } 42 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/internal/cha/ComponentHandler.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.internal.cha 2 | 3 | import org.xml.sax.Attributes 4 | import org.xml.sax.helpers.DefaultHandler 5 | import java.io.File 6 | import javax.xml.parsers.SAXParserFactory 7 | 8 | /** 9 | * Author: Omooo 10 | * Date: 2023/3/12 11 | * Desc: 12 | */ 13 | internal class ComponentHandler(private val manifest: File) : DefaultHandler() { 14 | 15 | val applications = mutableSetOf() 16 | val activities = mutableSetOf() 17 | val services = mutableSetOf() 18 | val providers = mutableSetOf() 19 | val receivers = mutableSetOf() 20 | 21 | /** 22 | * 获取组件列表 23 | * 24 | * @return {"com.xxx.MyApplication", "com.xxx.MainActivity"} 25 | */ 26 | fun getComponentSet(): Set { 27 | SAXParserFactory.newInstance().newSAXParser().parse(manifest, this) 28 | return applications + activities + services + receivers 29 | } 30 | 31 | override fun startElement( 32 | uri: String, 33 | localName: String, 34 | qName: String, 35 | attributes: Attributes 36 | ) { 37 | val name: String = attributes.getValue(ATTR_NAME) ?: return 38 | 39 | when (qName) { 40 | "application" -> { 41 | applications.add(name) 42 | } 43 | "activity" -> { 44 | activities.add(name) 45 | } 46 | "service" -> { 47 | services.add(name) 48 | } 49 | "provider" -> { 50 | providers.add(name) 51 | } 52 | "receiver" -> { 53 | receivers.add(name) 54 | } 55 | } 56 | } 57 | 58 | } 59 | 60 | private const val ATTR_NAME = "android:name" -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/internal/cha/LayoutHandler.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.internal.cha 2 | 3 | import org.xml.sax.Attributes 4 | import org.xml.sax.helpers.DefaultHandler 5 | import java.io.File 6 | import javax.xml.parsers.SAXParserFactory 7 | 8 | /** 9 | * Author: Omooo 10 | * Date: 2023/3/13 11 | * Desc: 12 | */ 13 | internal class LayoutHandler(private val layoutFile: File) : DefaultHandler() { 14 | 15 | private val views = mutableSetOf() 16 | 17 | fun getViews(): Set { 18 | SAXParserFactory.newInstance().newSAXParser().parse(layoutFile, this) 19 | return views 20 | } 21 | 22 | override fun startElement( 23 | uri: String, 24 | localName: String, 25 | qName: String, 26 | attributes: Attributes 27 | ) { 28 | views += qName 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/internal/cha/ReferenceAnalyser.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.internal.cha 2 | 3 | import org.objectweb.asm.tree.ClassNode 4 | import org.objectweb.asm.tree.FieldInsnNode 5 | import org.objectweb.asm.tree.MethodInsnNode 6 | 7 | /** 8 | * Author: Omooo 9 | * Date: 2023/3/13 10 | * Desc: 11 | */ 12 | internal class ReferenceAnalyser( 13 | private val entryPoints: Set, 14 | private val classNodeMap: Map, 15 | ) { 16 | 17 | fun analyze() { 18 | entryPoints.forEach { entryPoint -> 19 | classNodeMap[entryPoint]?.methods?.forEach { methodNode -> 20 | methodNode.instructions.forEach { absInsnNode -> 21 | when (absInsnNode) { 22 | is MethodInsnNode -> { 23 | absInsnNode.name 24 | } 25 | is FieldInsnNode -> { 26 | absInsnNode.name 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | private fun analyzeInternal(isMethod: Boolean, owner: String, name: String, desc: String) { 35 | if (isMethod) { 36 | classNodeMap[owner] 37 | } else { 38 | 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/reporter/AarAnalyseReporter.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.reporter 2 | 3 | import com.omooo.plugin.reporter.common.AarFile 4 | 5 | /** 6 | * Author: Omooo 7 | * Date: 2023/7/14 8 | * Desc: AAR 分析报告 9 | */ 10 | @kotlinx.serialization.Serializable 11 | internal data class AarAnalyseReporter( 12 | /** 描述信息 */ 13 | val desc: String, 14 | /** 文档链接 */ 15 | val documentLink: String, 16 | /** 包名 */ 17 | var packageName: String, 18 | /** AAR 列表 */ 19 | var aarList: ArrayList>>, 20 | /** 所属人映射 */ 21 | var ownerMap: Map> = emptyMap(), 22 | ) 23 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/reporter/AppReporter.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.reporter 2 | 3 | import com.omooo.plugin.reporter.common.AarFile 4 | 5 | /** 6 | * Author: Omooo 7 | * Date: 2023/2/3 8 | * Desc: App 报告类 9 | */ 10 | @kotlinx.serialization.Serializable 11 | internal data class AppReporter( 12 | /** 描述信息 */ 13 | val desc: String, 14 | /** 文档链接 */ 15 | val documentLink: String, 16 | /** 版本号 */ 17 | val versionName: String, 18 | /** 构建类型名 */ 19 | val variantName: String, 20 | /** AAR 列表 */ 21 | val aarList: List, 22 | ) 23 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/reporter/HtmlReporter.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.reporter 2 | 3 | import groovy.json.JsonOutput 4 | import java.io.File 5 | 6 | /** 7 | * Author: Omooo 8 | * Date: 2023/3/24 9 | * Desc: Html 报告生成器 10 | */ 11 | internal class HtmlReporter { 12 | 13 | /** 14 | * 生成报告 15 | */ 16 | fun generateReport(data: AppReporter, filePath: String): File { 17 | val file = File(filePath) 18 | if (file.exists()) { 19 | file.delete() 20 | } 21 | file.createNewFile() 22 | var html = readResourceFile("apkAnalyse-Template.html") 23 | html = html.replaceFirst("{key:\"REPLACE_ME\"}", "`${JsonOutput.toJson(data)}`") 24 | return file.apply { 25 | writeText(html) 26 | println("Reporter: ${toPath().toUri()}") 27 | } 28 | } 29 | 30 | /** 31 | * 生成 AAR 分析报告 32 | */ 33 | fun generateAarAnalyseReport(data: AarAnalyseReporter, filePath: String): File { 34 | val file = File(filePath) 35 | if (file.exists()) { 36 | file.delete() 37 | } 38 | file.createNewFile() 39 | var html = readResourceFile("aarAnalyse-Template.html") 40 | html = html.replaceFirst("{key:\"REPLACE_ME\"}", "`${JsonOutput.toJson(data)}`") 41 | return file.apply { 42 | writeText(html) 43 | println("Reporter: ${toPath().toUri()}") 44 | } 45 | } 46 | 47 | private fun readResourceFile(fileName: String): String { 48 | val url = requireNotNull(javaClass.getResource("/$fileName")) 49 | return url.readText() 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/reporter/Insight.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.reporter 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2023/2/3 6 | * Desc: 可视化相关用到的常量 7 | */ 8 | 9 | internal object Insight { 10 | /** 11 | * 文档链接 12 | */ 13 | object DocumentLink { 14 | const val LIST_UNUSED_ASSETS = "" 15 | const val LIST_UNUSED_RES = "" 16 | const val INVOKE_CHECK = "" 17 | const val LIST_UNUSED_CLASS = "" 18 | const val APK_ANALYSE = "" 19 | const val DETECT_TRANSLUCENT_ACTIVITY = "" 20 | const val LIST_IMAGE = "" 21 | const val AAR_ANALYSE = "" 22 | const val CHECK_SCHEME_MODIFIED = "" 23 | const val CHECK_SERVICE_TYPE = "" 24 | const val CHECK_FRAGMENT_CONSTRUCT = "" 25 | } 26 | 27 | /** 28 | * 标题 29 | */ 30 | object Title { 31 | const val LIST_UNUSED_ASSETS = "Lavender - List Unused Assets" 32 | const val LIST_UNUSED_RES = "Lavender - List Unused Res" 33 | const val INVOKE_CHECK = "Lavender - Invoke Check" 34 | const val LIST_UNUSED_CLASS = "Lavender - List Unused Class" 35 | const val APK_ANALYSE = "Lavender - Apk Analyse" 36 | const val DETECT_TRANSLUCENT_ACTIVITY = "Lavender - Detect Translucent Activity" 37 | const val LIST_IMAGE = "Lavender - List Image" 38 | const val AAR_ANALYSE = "Lavender - Aar Analyse" 39 | const val CHECK_SCHEME_MODIFIED = "Lavender - Check Scheme Modified" 40 | const val CHECK_SERVICE_TYPE = "Lavender - Check Service Type" 41 | const val CHECK_FRAGMENT_CONSTRUCT = "" 42 | } 43 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/reporter/common/AarFile.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.reporter.common 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2023/2/2 6 | * Desc: 7 | */ 8 | @kotlinx.serialization.Serializable 9 | internal data class AarFile( 10 | val name: String, 11 | var size: Long, 12 | var owner: String, 13 | var fileList: MutableList = ArrayList(), 14 | ) 15 | 16 | internal fun List.totalSize(): Long { 17 | if (isEmpty()) { 18 | return 0 19 | } 20 | return map { it.size }.reduce { acc, l -> acc + l } 21 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/reporter/common/AppFile.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.reporter.common 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2023/2/2 6 | * Desc: 表示一个 Apk 里的文件 7 | */ 8 | 9 | @kotlinx.serialization.Serializable 10 | internal data class AppFile( 11 | var name: String, 12 | var size: Long = 0, 13 | var desc: String = "", 14 | var fileType: FileType = FileType.OTHER, 15 | ) 16 | 17 | internal fun List.totalSize(): Long { 18 | if (isEmpty()) { 19 | return 0 20 | } 21 | return map { it.size }.reduce { acc, l -> acc + l } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/reporter/common/FileType.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.reporter.common 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2023/3/17 6 | * Desc: 文件类型 7 | */ 8 | enum class FileType { 9 | CLASS, 10 | RESOURCE, 11 | ASSET, 12 | NATIVE_LIB, 13 | OTHER, 14 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/scan/BuildScan.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.scan 2 | 3 | import com.omooo.plugin.util.green 4 | import com.omooo.plugin.util.red 5 | import groovy.json.JsonOutput 6 | import org.gradle.api.Project 7 | import org.gradle.api.internal.GradleInternal 8 | import org.gradle.internal.operations.* 9 | import org.gradle.internal.operations.trace.BuildOperationTrace 10 | 11 | /** 12 | * Author: Omooo 13 | * Date: 2024/4/24 14 | * Desc: 15 | */ 16 | internal class BuildScan { 17 | 18 | fun scan(project: Project) { 19 | (project.gradle as? GradleInternal)?.services?.get(BuildOperationListenerManager::class.java) 20 | ?.addListener(object : BuildOperationListener { 21 | override fun started( 22 | buildOperation: BuildOperationDescriptor, 23 | startEvent: OperationStartEvent 24 | ) { 25 | // ignore 26 | } 27 | 28 | override fun progress( 29 | operationIdentifier: OperationIdentifier, 30 | progressEvent: OperationProgressEvent 31 | ) { 32 | // ignore 33 | } 34 | 35 | override fun finished( 36 | buildOperation: BuildOperationDescriptor, 37 | finishEvent: OperationFinishEvent 38 | ) { 39 | val duration = finishEvent.endTime - finishEvent.startTime 40 | val details = BuildOperationTrace.toSerializableModel(buildOperation.details) 41 | val result = BuildOperationTrace.toSerializableModel(finishEvent.result) 42 | println(""" 43 | ${green("Finished: ${buildOperation.name}")} 44 | ids: ${buildOperation.id} ${buildOperation.parentId} 45 | displayName: ${buildOperation.displayName} 46 | progressDisplayName: ${buildOperation.progressDisplayName} 47 | metadata: ${buildOperation.metadata} 48 | duration: $duration 49 | details: ${JsonOutput.toJson(details)} 50 | result: ${JsonOutput.toJson(result)} 51 | """.trimIndent()) 52 | } 53 | 54 | }) 55 | } 56 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/spi/VariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.spi 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.android.build.gradle.api.BaseVariant 5 | import org.gradle.api.Project 6 | 7 | /** 8 | * Author: Omooo 9 | * Date: 2019/9/27 10 | * Version: v0.1.0 11 | * Desc: Task 注册接口 12 | */ 13 | interface VariantProcessor { 14 | 15 | @Deprecated( 16 | message = "BaseVariant is deprecated, please use process(variant: Variant) method instead", 17 | replaceWith = ReplaceWith( 18 | expression = "process(variant: Variant)" 19 | ) 20 | ) 21 | fun process(project: Project, variant: BaseVariant) = Unit 22 | 23 | fun process(variant: Variant) = Unit 24 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/AarAnalyseTask.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.android.build.gradle.internal.publishing.AndroidArtifacts 5 | import com.omooo.plugin.internal.aar.AarAnalyse 6 | import com.omooo.plugin.reporter.HtmlReporter 7 | import com.omooo.plugin.reporter.common.AarFile 8 | import com.omooo.plugin.util.* 9 | import com.omooo.plugin.util.getArtifactIdFromAarName 10 | import com.omooo.plugin.util.getArtifactName 11 | import com.omooo.plugin.util.getOwnerShip 12 | import com.omooo.plugin.util.green 13 | import org.gradle.api.DefaultTask 14 | import org.gradle.api.tasks.Internal 15 | import org.gradle.api.tasks.TaskAction 16 | import java.io.File 17 | 18 | /** 19 | * Author: Omooo 20 | * Date: 2023/07/25 21 | * Desc: Aar 分析任务 22 | * Use: ./gradlew aarAnalyse 23 | * Output: projectDir/aarAnalyse.html 24 | */ 25 | internal abstract class AarAnalyseTask : DefaultTask() { 26 | @get:Internal 27 | lateinit var variant: Variant 28 | 29 | @TaskAction 30 | fun doAction() { 31 | println( 32 | """ 33 | ********************************************* 34 | ********** -- AarAnalyseTask -- ************* 35 | ***** -- projectDir/aarAnalyse.html -- ****** 36 | ********************************************* 37 | """.trimIndent() 38 | ) 39 | val startTime = System.currentTimeMillis() 40 | val ownerShip = project.getOwnerShip() 41 | val aarList = 42 | variant.variantImpl.variantDependencies.getArtifactCollection( 43 | AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH, 44 | AndroidArtifacts.ArtifactScope.ALL, 45 | AndroidArtifacts.ArtifactType.AAR_OR_JAR 46 | ).artifacts.map { artifact -> 47 | AarFile( 48 | name = artifact.getArtifactName().removeVersionFromAarName(), 49 | size = File(artifact.file.absolutePath).length(), 50 | owner = ownerShip.getOrDefault( 51 | artifact.getArtifactName().getArtifactIdFromAarName(), "unknown" 52 | ), 53 | ) 54 | }.toSet() 55 | val reporter = AarAnalyse(project).analyse( 56 | variant.variantImpl.applicationId.get(), 57 | Pair(variant.versionName, aarList.sortedByDescending { it.size }) 58 | ) 59 | HtmlReporter().generateAarAnalyseReport(reporter, "${project.parent?.projectDir}/aarAnalyse.html") 60 | 61 | println(green("Spend time: ${System.currentTimeMillis() - startTime}ms")) 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/AarAnalyseTaskProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.omooo.plugin.spi.VariantProcessor 5 | import com.google.auto.service.AutoService 6 | import com.omooo.plugin.bean.LAVENDER 7 | import com.omooo.plugin.util.project 8 | 9 | /** 10 | * Author: Omooo 11 | * Date: 2023/07/25 12 | * Desc: 注册 [AarAnalyseTask] 13 | */ 14 | @AutoService(VariantProcessor::class) 15 | class AarAnalyseTaskProcessor : VariantProcessor { 16 | 17 | override fun process(variant: Variant) { 18 | if (variant.project.tasks.findByName("aarAnalyse") != null) { 19 | return 20 | } 21 | variant.project.tasks.register("aarAnalyse", AarAnalyseTask::class.java) { 22 | it.variant = variant 23 | it.group = LAVENDER 24 | it.description = "Analyse the aar size in app project" 25 | } 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ApkAnalyseVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.Variant 5 | import com.google.auto.service.AutoService 6 | import com.omooo.plugin.bean.LAVENDER 7 | import com.omooo.plugin.spi.VariantProcessor 8 | import com.omooo.plugin.util.nameCapitalize 9 | import com.omooo.plugin.util.project 10 | 11 | /** 12 | * Author: Omooo 13 | * Date: 2023/3/17 14 | * Desc: 注册 [ApkAnalyseTask] 15 | */ 16 | @AutoService(VariantProcessor::class) 17 | class ApkAnalyseVariantProcessor : VariantProcessor { 18 | 19 | override fun process(variant: Variant) { 20 | if (variant.name.contains("debug", true)) { 21 | return 22 | } 23 | variant.project.tasks.register( 24 | "apkAnalyseFor${variant.nameCapitalize()}", 25 | ApkAnalyseTask::class.java 26 | ) { 27 | it.variant = variant 28 | it.apkFileDir.set(variant.artifacts.get(SingleArtifact.APK)) 29 | it.group = LAVENDER 30 | it.description = "Analyse the apk output from app project for ${variant.name}." 31 | it.outputs.upToDateWhen { false } 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/CheckExportedTask.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.omooo.plugin.util.* 5 | import com.omooo.plugin.util.attributeMap 6 | import com.omooo.plugin.util.getArtifactName 7 | import com.omooo.plugin.util.toList 8 | import org.gradle.api.DefaultTask 9 | import org.gradle.api.artifacts.ArtifactCollection 10 | import org.gradle.api.file.RegularFileProperty 11 | import org.gradle.api.provider.Property 12 | import org.gradle.api.tasks.InputFile 13 | import org.gradle.api.tasks.Internal 14 | import org.gradle.api.tasks.TaskAction 15 | import java.io.File 16 | import javax.xml.parsers.DocumentBuilderFactory 17 | 18 | /** 19 | * Author: Omooo 20 | * Date: 2022/12/20 21 | * Desc: 检测声明了 的组件是否包含 android:exported 属性(Android 12 强制需要包含该属性) 22 | * Use: ./gradlew checkExported 23 | * Output: projectDir/checkExported.json 24 | */ 25 | internal abstract class CheckExportedTask : DefaultTask() { 26 | 27 | @get:Internal 28 | lateinit var variant: Variant 29 | 30 | @get:Internal 31 | abstract val manifests: Property 32 | 33 | @get:InputFile 34 | abstract val mainManifest: Property 35 | 36 | // @get:InputFile 37 | // abstract val mergedManifest: RegularFileProperty 38 | 39 | /** 检测的节点列表 */ 40 | private val checkNodeList = listOf("activity", "service", "receiver") 41 | 42 | @TaskAction 43 | fun run() { 44 | println( 45 | """ 46 | ********************************************* 47 | ********** -- CheckExportedTask -- ********** 48 | **** -- projectDir/checkExported.json -- **** 49 | ********************************************* 50 | """.trimIndent() 51 | ) 52 | val appProjectResult = project.name to mainManifest.get().getComponentList() 53 | manifests.get().artifacts.associate { artifact -> 54 | artifact.getArtifactName() to artifact.file.getComponentList() 55 | }.plus(appProjectResult).filter { 56 | it.value.isNotEmpty() 57 | }.also { 58 | it.writeToJson("${project.parent?.projectDir}/checkExported.json") 59 | } 60 | } 61 | 62 | /** 63 | * 获取检测的组件列表 64 | * 65 | * 入参: Manifest 文件 66 | */ 67 | private fun File.getComponentList(): List { 68 | return checkNodeList.asSequence().map { 69 | DocumentBuilderFactory.newInstance().newDocumentBuilder() 70 | .parse(this).getElementsByTagName(it).toList() 71 | }.flatten().filter { 72 | it.childNodes.toList().map { it.nodeName }.contains("intent-filter") 73 | }.filterNot { 74 | it.attributeMap().containsKey("android:exported") 75 | }.map { 76 | it.attributeMap().getOrDefault("android:name", "unknown") 77 | }.toList() 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/CheckExportedVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.artifact.Artifact 4 | import com.android.build.api.artifact.SingleArtifact 5 | import com.android.build.api.variant.Variant 6 | import com.android.build.gradle.internal.publishing.AndroidArtifacts 7 | import com.android.build.gradle.internal.scope.InternalArtifactType 8 | import com.android.build.gradle.internal.tasks.factory.dependsOn 9 | import com.omooo.plugin.spi.VariantProcessor 10 | import com.google.auto.service.AutoService 11 | import com.omooo.plugin.bean.LAVENDER 12 | import com.omooo.plugin.util.getArtifactCollection 13 | import com.omooo.plugin.util.nameCapitalize 14 | import com.omooo.plugin.util.project 15 | import com.omooo.plugin.util.variantImpl 16 | import org.gradle.api.file.Directory 17 | 18 | /** 19 | * Author: Omooo 20 | * Date: 2022/12/20 21 | * Desc: 注册 [CheckExportedTask] 22 | */ 23 | @AutoService(VariantProcessor::class) 24 | class CheckExportedVariantProcessor : VariantProcessor { 25 | 26 | override fun process(variant: Variant) { 27 | val project = variant.project 28 | project.tasks.register("checkExportedFor${variant.nameCapitalize()}", CheckExportedTask::class.java) { 29 | it.variant = variant 30 | it.manifests.set(variant.getArtifactCollection(AndroidArtifacts.ArtifactType.MANIFEST)) 31 | // it.mergedManifest.set(variant.artifacts.get(SingleArtifact.MERGED_MANIFEST)) 32 | it.mainManifest.set(variant.variantImpl.sources.manifestFile) 33 | it.group = LAVENDER 34 | it.description = "Check exported attribute in Manifest." 35 | it.outputs.upToDateWhen { false } 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/CheckSchemeModifiedProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.Variant 5 | import com.android.build.gradle.internal.publishing.AndroidArtifacts 6 | import com.android.build.gradle.internal.tasks.factory.dependsOn 7 | import com.google.auto.service.AutoService 8 | import com.omooo.plugin.bean.CheckSchemeModifiedExtension 9 | import com.omooo.plugin.bean.LAVENDER 10 | import com.omooo.plugin.spi.VariantProcessor 11 | import com.omooo.plugin.util.getArtifactCollection 12 | import com.omooo.plugin.util.nameCapitalize 13 | import com.omooo.plugin.util.project 14 | import org.gradle.api.UnknownTaskException 15 | 16 | /** 17 | * Author: Omooo 18 | * Date: 2023/8/21 19 | * Desc: 注册 [CheckSchemeModifiedTask] 20 | */ 21 | @AutoService(VariantProcessor::class) 22 | class CheckSchemeModifiedProcessor : VariantProcessor { 23 | 24 | @Suppress("SwallowedException") 25 | override fun process(variant: Variant) { 26 | val project = variant.project 27 | val task = try { 28 | project.tasks.named("checkSchemeModified") 29 | } catch (e: UnknownTaskException) { 30 | project.tasks.register("checkSchemeModified") { 31 | it.group = LAVENDER 32 | it.description = "Check the schemes modified might trigger compile failure" 33 | } 34 | } 35 | project.tasks.register( 36 | "checkSchemeModifiedFor${variant.nameCapitalize()}", 37 | CheckSchemeModifiedTask::class.java 38 | ) { 39 | it.variant = variant 40 | it.manifests.set(variant.getArtifactCollection(AndroidArtifacts.ArtifactType.MANIFEST)) 41 | it.mergedManifest.set(variant.artifacts.get(SingleArtifact.MERGED_MANIFEST)) 42 | it.config = project.extensions.findByType(CheckSchemeModifiedExtension::class.java) 43 | ?: project.extensions.create( 44 | "checkSchemeModifiedConfig", 45 | CheckSchemeModifiedExtension::class.java 46 | ) 47 | it.group = LAVENDER 48 | it.description = 49 | "Check the schemes modified might trigger compile failure for ${variant.name}." 50 | it.outputs.upToDateWhen { false } 51 | }.also { 52 | task.dependsOn(it) 53 | } 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/CheckServiceTypeVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.Variant 5 | import com.android.build.gradle.internal.publishing.AndroidArtifacts 6 | import com.android.build.gradle.internal.tasks.factory.dependsOn 7 | import com.omooo.plugin.spi.VariantProcessor 8 | import com.google.auto.service.AutoService 9 | import com.omooo.plugin.bean.LAVENDER 10 | import com.omooo.plugin.util.getArtifactCollection 11 | import com.omooo.plugin.util.nameCapitalize 12 | import com.omooo.plugin.util.project 13 | import com.omooo.plugin.util.variantImpl 14 | import org.gradle.api.UnknownTaskException 15 | 16 | /** 17 | * Author: Omooo 18 | * Date: 2023/08/25 19 | * Desc: 注册 [CheckServiceTypeTask] 20 | */ 21 | @AutoService(VariantProcessor::class) 22 | class CheckServiceTypeVariantProcessor : VariantProcessor { 23 | 24 | @Suppress("SwallowedException") 25 | override fun process(variant: Variant) { 26 | val project = variant.project 27 | val checkServiceTypeTask = try { 28 | project.tasks.named("checkServiceType") 29 | } catch (e: UnknownTaskException) { 30 | project.tasks.register("checkServiceType") { 31 | it.group = LAVENDER 32 | it.description = "Check set foreground service type attribute in Manifest." 33 | } 34 | } 35 | project.tasks.register("checkServiceTypeFor${variant.nameCapitalize()}", CheckServiceTypeTask::class.java) { 36 | it.variant = variant 37 | it.manifests.set(variant.getArtifactCollection(AndroidArtifacts.ArtifactType.MANIFEST)) 38 | it.mergedManifest.set(variant.artifacts.get(SingleArtifact.MERGED_MANIFEST)) 39 | it.mainManifest.set(variant.variantImpl.sources.manifestFile) 40 | it.group = LAVENDER 41 | it.description = "Check set foreground service type attribute in Manifest for ${variant.nameCapitalize()}" 42 | it.outputs.upToDateWhen { false } 43 | }.also { 44 | checkServiceTypeTask.dependsOn(it) 45 | } 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/DetectTranslucentActivityVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.Variant 5 | import com.android.build.gradle.internal.publishing.AndroidArtifacts 6 | import com.android.build.gradle.internal.tasks.factory.dependsOn 7 | import com.google.auto.service.AutoService 8 | import com.omooo.plugin.bean.LAVENDER 9 | import com.omooo.plugin.spi.VariantProcessor 10 | import com.omooo.plugin.util.getArtifactCollection 11 | import com.omooo.plugin.util.project 12 | import com.omooo.plugin.util.variantImpl 13 | import org.gradle.api.model.ObjectFactory 14 | 15 | /** 16 | * Author: Omooo 17 | * Date: 2023/3/22 18 | * Desc: 注册 [DetectTranslucentActivityTask] 19 | */ 20 | @AutoService(VariantProcessor::class) 21 | class DetectTranslucentActivityVariantProcessor : VariantProcessor { 22 | 23 | override fun process(variant: Variant) { 24 | val project = variant.project 25 | if (project.tasks.findByName("detectTranslucentActivity") != null) { 26 | return 27 | } 28 | project.tasks.register( 29 | "detectTranslucentActivity", 30 | DetectTranslucentActivityTask::class.java 31 | ) { 32 | it.variant = variant 33 | it.manifests.set(variant.getArtifactCollection(AndroidArtifacts.ArtifactType.MANIFEST)) 34 | it.apkFileCollection = project.files(variant.artifacts.get(SingleArtifact.APK)) 35 | it.group = LAVENDER 36 | it.description = "Detect the translucent activity from app project" 37 | it.outputs.upToDateWhen { false } 38 | } 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/FragmentNonConstructCheckVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.Variant 5 | import com.google.auto.service.AutoService 6 | import com.omooo.plugin.bean.LAVENDER 7 | import com.omooo.plugin.spi.VariantProcessor 8 | import com.omooo.plugin.util.project 9 | 10 | /** 11 | * Author: Omooo 12 | * Date: 2023/11/8 13 | * Desc: 注册 [FragmentNonConstructCheckTask] 14 | */ 15 | @AutoService(VariantProcessor::class) 16 | class FragmentNonConstructCheckVariantProcessor : VariantProcessor { 17 | 18 | override fun process(variant: Variant) { 19 | val project = variant.project 20 | if (project.tasks.findByName("checkFragmentNonConstruct") != null) { 21 | return 22 | } 23 | project.tasks.register("checkFragmentNonConstruct", FragmentNonConstructCheckTask::class.java) { 24 | it.variant = variant 25 | it.apkFileCollection = project.files(variant.artifacts.get(SingleArtifact.APK)) 26 | it.group = LAVENDER 27 | it.description = "Check Fragment non construct method in app project" 28 | } 29 | 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListAarSizeTask.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.android.build.gradle.internal.publishing.AndroidArtifacts 5 | import com.omooo.plugin.util.getArtifactName 6 | import com.omooo.plugin.util.getGroupIdFromAarName 7 | import com.omooo.plugin.util.variantImpl 8 | import com.omooo.plugin.util.writeToJson 9 | import org.gradle.api.DefaultTask 10 | import org.gradle.api.tasks.Internal 11 | import org.gradle.api.tasks.TaskAction 12 | import java.io.File 13 | 14 | /** 15 | * Author: Omooo 16 | * Date: 2022/11/13 17 | * Desc: 统计依赖的 AAR 大小 18 | * Use: ./gradlew listAarSize 19 | * Output: projectDir/listAarSize.json 20 | */ 21 | internal abstract class ListAarSizeTask : DefaultTask() { 22 | @get:Internal 23 | lateinit var variant: Variant 24 | 25 | @TaskAction 26 | fun doAction() { 27 | println( 28 | """ 29 | ********************************************* 30 | ********** -- ListAarSizeTask -- ************ 31 | ***** -- projectDir/listAarSize.json -- ***** 32 | ********************************************* 33 | """.trimIndent() 34 | ) 35 | var resultMap = mutableMapOf() 36 | variant.variantImpl.variantDependencies.getArtifactCollection( 37 | AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH, 38 | AndroidArtifacts.ArtifactScope.ALL, 39 | AndroidArtifacts.ArtifactType.AAR_OR_JAR 40 | ).artifacts.forEach { artifact -> 41 | val size = File(artifact.file.absolutePath).length() / 1024 42 | resultMap[artifact.getArtifactName()] = size 43 | } 44 | 45 | // 配置了 group-by 参数 46 | if (project.hasProperty("sortByGroupId")) { 47 | resultMap = resultMap 48 | .toList() 49 | .groupBy({ it.first.getGroupIdFromAarName() }) { it.second } 50 | .mapValues { (_, values) -> values.sum() } 51 | .toMutableMap() 52 | } 53 | 54 | resultMap.toList().asSequence().filter { 55 | if (project.hasProperty("groupIdPrefix")) { 56 | it.first.getGroupIdFromAarName().startsWith(project.properties["groupIdPrefix"].toString()) 57 | } else { 58 | true 59 | } 60 | }.sortedByDescending { (_, value) -> 61 | value 62 | }.toMap().mapValues { 63 | "${it.value}kb" 64 | }.also { 65 | it.writeToJson("${project.parent?.projectDir}/listAarSize.json") 66 | } 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListAarSizeVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.omooo.plugin.spi.VariantProcessor 5 | import com.google.auto.service.AutoService 6 | import com.omooo.plugin.bean.LAVENDER 7 | import com.omooo.plugin.util.project 8 | 9 | /** 10 | * Author: Omooo 11 | * Date: 2022/11/13 12 | * Version: v0.1.0 13 | * Desc: 注册 [ListAarSizeTask] 14 | */ 15 | @AutoService(VariantProcessor::class) 16 | class ListAarSizeVariantProcessor : VariantProcessor { 17 | 18 | override fun process(variant: Variant) { 19 | val project = variant.project 20 | if (project.tasks.findByName("listAarSize") != null) { 21 | return 22 | } 23 | project.tasks.register("listAarSize", ListAarSizeTask::class.java) { 24 | it.variant = variant 25 | it.group = LAVENDER 26 | it.description = "List the aar size in app project" 27 | } 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListAssetsTask.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.android.build.gradle.internal.publishing.AndroidArtifacts 5 | import com.omooo.plugin.util.getAllChildren 6 | import com.omooo.plugin.util.getArtifactName 7 | import com.omooo.plugin.util.variantImpl 8 | import com.omooo.plugin.util.writeToJson 9 | import org.gradle.api.DefaultTask 10 | import org.gradle.api.tasks.Internal 11 | import org.gradle.api.tasks.TaskAction 12 | 13 | /** 14 | * Author: Omooo 15 | * Date: 2022/11/17 16 | * Desc: 输出所有的 Assets 资源 17 | * Use: ./gradlew listAssets 18 | * Output: projectDir/assets.json 19 | */ 20 | internal abstract class ListAssetsTask : DefaultTask() { 21 | @get:Internal 22 | lateinit var variant: Variant 23 | 24 | @TaskAction 25 | fun doAction() { 26 | println( 27 | """ 28 | ********************************************* 29 | ********** -- ListAssetsTask -- ************* 30 | ******* -- projectDir/assets.json -- ******** 31 | ********************************************* 32 | """.trimIndent() 33 | ) 34 | getTotalAssets().writeToJson("${project.parent?.projectDir}/assets.json") 35 | } 36 | 37 | /** 38 | * 获取所有的 Assets 文件列表 39 | * 40 | * @return Map 41 | */ 42 | private fun getTotalAssets(): Map> { 43 | return variant.variantImpl.variantDependencies.getArtifactCollection( 44 | AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH, 45 | AndroidArtifacts.ArtifactScope.ALL, 46 | AndroidArtifacts.ArtifactType.ASSETS 47 | ).artifacts.associate { artifact -> 48 | artifact.getArtifactName() to artifact.file.getAllChildren() 49 | .sortedByDescending { file -> 50 | file.length() 51 | }.map { 52 | AssetFile(it.absolutePath.substringAfterLast("out/"), it.length()) 53 | } 54 | }.toMutableMap().also { map -> 55 | project.projectDir.resolve("src/main/assets").takeIf { 56 | it.isDirectory 57 | }?.getAllChildren()?.sortedByDescending { file -> 58 | file.length() 59 | }?.also { 60 | map[project.name] = it.map { file -> 61 | AssetFile(file.absolutePath.substringAfterLast("assets/"), file.length()) 62 | } 63 | } 64 | } 65 | } 66 | 67 | /** 68 | * Asset 文件数据类 69 | */ 70 | internal data class AssetFile( 71 | /** 文件名 */ 72 | val fileName: String, 73 | /** 大小,单位 byte */ 74 | val size: Long, 75 | ) 76 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListAssetsVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.omooo.plugin.spi.VariantProcessor 5 | import com.google.auto.service.AutoService 6 | import com.omooo.plugin.bean.LAVENDER 7 | import com.omooo.plugin.util.project 8 | 9 | /** 10 | * Author: Omooo 11 | * Date: 2022/11/17 12 | * Version: v0.0.1 13 | * Desc: 注册 [ListAssetsTask] 14 | */ 15 | @AutoService(VariantProcessor::class) 16 | class ListAssetsVariantProcessor : VariantProcessor { 17 | 18 | override fun process(variant: Variant) { 19 | val project = variant.project 20 | if (project.tasks.findByName("listAssets") != null) { 21 | return 22 | } 23 | project.tasks.register("listAssets", ListAssetsTask::class.java) { 24 | it.variant = variant 25 | it.group = LAVENDER 26 | it.description = "List all asset files in app project" 27 | } 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListClassOwnerMapTask.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.omooo.plugin.util.* 5 | import com.omooo.plugin.util.getOwnerShip 6 | import com.omooo.plugin.util.writeToJson 7 | import org.gradle.api.DefaultTask 8 | import org.gradle.api.file.FileCollection 9 | import org.gradle.api.tasks.InputFiles 10 | import org.gradle.api.tasks.Internal 11 | import org.gradle.api.tasks.TaskAction 12 | 13 | /** 14 | * Author: Omooo 15 | * Date: 2023/3/15 16 | * Desc: 输出类的归属者映射 17 | * Use: ./gradlew listClassOwnerMap 18 | * Output: projectDir/classOwnerMap.json 19 | */ 20 | internal abstract class ListClassOwnerMapTask : DefaultTask() { 21 | 22 | @get:Internal 23 | lateinit var variant: Variant 24 | @get:InputFiles 25 | abstract var apkFileCollection: FileCollection 26 | 27 | @TaskAction 28 | fun run() { 29 | println( 30 | """ 31 | ********************************************* 32 | ******* -- ListClassOwnerMapTask -- ********* 33 | **** -- projectDir/classOwnerMap.json -- **** 34 | ********************************************* 35 | """.trimIndent() 36 | ) 37 | 38 | val ownerShip = project.getOwnerShip() 39 | variant.getArtifactClassMap().mapValues { 40 | ownerShip.getOrDefault(it.value.first.getArtifactIdFromAarName(), "unknown") 41 | }.filterValues { 42 | it != "unknown" 43 | }.writeToJson("${project.parent?.projectDir}/classOwnerMap.json") 44 | 45 | } 46 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListClassOwnerMapVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.Variant 5 | import com.google.auto.service.AutoService 6 | import com.omooo.plugin.bean.LAVENDER 7 | import com.omooo.plugin.spi.VariantProcessor 8 | import com.omooo.plugin.util.project 9 | 10 | /** 11 | * Author: Omooo 12 | * Date: 2023/3/15 13 | * Desc: 注册 [ListClassOwnerMapTask] 14 | */ 15 | @AutoService(VariantProcessor::class) 16 | class ListClassOwnerMapVariantProcessor : VariantProcessor { 17 | 18 | override fun process(variant: Variant) { 19 | val project = variant.project 20 | if (project.tasks.findByName("listClassOwnerMap") != null) { 21 | return 22 | } 23 | project.tasks.register("listClassOwnerMap", ListClassOwnerMapTask::class.java) { 24 | it.variant = variant 25 | it.apkFileCollection = project.files(variant.artifacts.get(SingleArtifact.APK)) 26 | it.group = LAVENDER 27 | it.description = "List classes owner map in app project" 28 | } 29 | 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListImageTask.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.android.build.gradle.internal.publishing.AndroidArtifacts 5 | import com.omooo.plugin.reporter.AppReporter 6 | import com.omooo.plugin.reporter.HtmlReporter 7 | import com.omooo.plugin.reporter.Insight 8 | import com.omooo.plugin.reporter.common.AarFile 9 | import com.omooo.plugin.reporter.common.AppFile 10 | import com.omooo.plugin.reporter.common.totalSize 11 | import com.omooo.plugin.util.getArtifactName 12 | import com.omooo.plugin.util.getArtifactIdFromAarName 13 | import com.omooo.plugin.util.isImageFile 14 | import com.omooo.plugin.util.getAllChildren 15 | import com.omooo.plugin.util.getOwnerShip 16 | import com.omooo.plugin.util.project 17 | import com.omooo.plugin.util.variantImpl 18 | import com.omooo.plugin.util.versionName 19 | import com.omooo.plugin.util.writeToJson 20 | import org.gradle.api.DefaultTask 21 | import org.gradle.api.tasks.Internal 22 | import org.gradle.api.tasks.TaskAction 23 | 24 | /** 25 | * Author: Omooo 26 | * Date: 2023/05/25 27 | * Desc: 输出所有的图片资源 28 | * Use: ./gradlew listImage 29 | * Output: projectDir/imageList.json 30 | */ 31 | internal open class ListImageTask : DefaultTask() { 32 | @get:Internal 33 | lateinit var variant: Variant 34 | 35 | @TaskAction 36 | fun doAction() { 37 | println( 38 | """ 39 | ********************************************* 40 | ********** -- ListImageTask -- ************** 41 | ****** -- projectDir/imageList.json -- ****** 42 | ********************************************* 43 | """.trimIndent() 44 | ) 45 | AppReporter( 46 | desc = Insight.Title.LIST_IMAGE, 47 | documentLink = Insight.DocumentLink.LIST_IMAGE, 48 | versionName = variant.versionName, 49 | variantName = variant.name, 50 | aarList = getTotalImage(), 51 | ).apply { 52 | writeToJson("${project.parent?.projectDir}/imageList.json") 53 | HtmlReporter().generateReport(this, "${project.parent?.projectDir}/imageList.html") 54 | } 55 | } 56 | 57 | /** 58 | * 获取所有的图片 59 | */ 60 | private fun getTotalImage(): List { 61 | val ownership = project.getOwnerShip() 62 | return variant.variantImpl.variantDependencies.getArtifactCollection( 63 | AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH, 64 | AndroidArtifacts.ArtifactScope.ALL, 65 | AndroidArtifacts.ArtifactType.ANDROID_RES 66 | ).artifacts.associate { artifact -> 67 | // 处理 App 的所有依赖 68 | artifact.getArtifactName() to artifact.file.getAllChildren() 69 | }.plus( 70 | // 加上 App 工程的 71 | project.name to project.projectDir.resolve("src/main/res").getAllChildren() 72 | ).mapValues { entry -> 73 | entry.value.filter { 74 | it.isImageFile() 75 | }.sortedByDescending { 76 | it.length() 77 | }.map { 78 | AppFile(it.name, it.length()) 79 | } 80 | }.filterValues { 81 | it.isNotEmpty() 82 | }.map { 83 | val owner = ownership.getOrDefault(it.key.getArtifactIdFromAarName(), "unknown") 84 | AarFile(it.key, it.value.totalSize(), owner, it.value.toMutableList()) 85 | }.sortedByDescending { it.size } 86 | } 87 | 88 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListImageVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.android.build.gradle.internal.tasks.factory.dependsOn 5 | import com.google.auto.service.AutoService 6 | import com.omooo.plugin.bean.LAVENDER 7 | import com.omooo.plugin.spi.VariantProcessor 8 | import com.omooo.plugin.util.nameCapitalize 9 | import com.omooo.plugin.util.project 10 | import org.gradle.api.UnknownTaskException 11 | 12 | /** 13 | * Author: Omooo 14 | * Date: 2023/5/25 15 | * Desc: 注册 [ListImageTask] 16 | */ 17 | @Suppress("SwallowedException") 18 | @AutoService(VariantProcessor::class) 19 | class ListImageVariantProcessor : VariantProcessor { 20 | 21 | override fun process(variant: Variant) { 22 | val project = variant.project 23 | val listImageTask = try { 24 | project.tasks.named("listImage") 25 | } catch (e: UnknownTaskException) { 26 | project.tasks.register("listImage") { 27 | it.group = LAVENDER 28 | it.description = "List image in app project." 29 | } 30 | } 31 | project.tasks.register("listImageFor${variant.nameCapitalize()}", ListImageTask::class.java) { 32 | it.variant = variant 33 | it.group = LAVENDER 34 | it.description = "List image for ${variant.name}." 35 | it.outputs.upToDateWhen { false } 36 | }.also { 37 | listImageTask.dependsOn(it) 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListPackageNameTask.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.omooo.plugin.util.getArtifactClassMap 5 | import org.gradle.api.DefaultTask 6 | import org.gradle.api.file.FileCollection 7 | import org.gradle.api.tasks.InputFiles 8 | import org.gradle.api.tasks.Internal 9 | import org.gradle.api.tasks.TaskAction 10 | import java.io.File 11 | 12 | /** 13 | * Author: Omooo 14 | * Date: 2023/5/24 15 | * Desc: 输出业务模块的包名列表(用于 Robust 配置) 16 | * Use: ./gradlew listPackageName 17 | * Output: projectDir/packageNameList.xml 18 | */ 19 | internal abstract class ListPackageNameTask : DefaultTask() { 20 | 21 | @get:Internal 22 | lateinit var variant: Variant 23 | @get:InputFiles 24 | abstract var apkFileCollection: FileCollection 25 | 26 | @TaskAction 27 | fun run() { 28 | println( 29 | """ 30 | ********************************************* 31 | ********* -- ListPackageNameTask -- ********* 32 | **** -- projectDir/packageNameList.xml -- *** 33 | ********************************************* 34 | """.trimIndent() 35 | ) 36 | variant.getArtifactClassMap().keys.map { 37 | it.getPackageNameFromClassName() 38 | }.toSet().writeXml("${project.parent?.projectDir}/packageNameList.xml") 39 | } 40 | 41 | /** 42 | * 从类名中获取指定段数的包名(默认三段) 43 | * 44 | * "androidx.core.graphics.PaintKt" 45 | * n=3: "androidx.core.graphics" 46 | * n=2: "androidx.core" 47 | */ 48 | private fun String.getPackageNameFromClassName(): String { 49 | val l = if (project.hasProperty("length")) 50 | project.properties["length"].toString().toInt() else DEFAULT_PACKAGE_LENGTH 51 | val segments = this.split(".") 52 | val endIndex = segments.size.coerceAtMost(l) 53 | return segments.subList(0, endIndex).joinToString(".") 54 | } 55 | 56 | 57 | /** 58 | * 写入 xml 59 | * 60 | * @param filePath 文件路径 61 | */ 62 | private fun Set.writeXml(filePath: String) { 63 | val text = buildString { 64 | appendLine("""""") 65 | appendLine("") 66 | this@writeXml.forEach { s -> 67 | appendLine(" $s") 68 | } 69 | 70 | append("") 71 | } 72 | File(filePath).apply { 73 | if (exists()) { 74 | delete() 75 | } 76 | createNewFile() 77 | writeText(text) 78 | } 79 | } 80 | 81 | } 82 | 83 | private const val DEFAULT_PACKAGE_LENGTH = 3 -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListPackageNameVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.Variant 5 | import com.android.build.gradle.internal.tasks.factory.dependsOn 6 | import com.google.auto.service.AutoService 7 | import com.omooo.plugin.bean.LAVENDER 8 | import com.omooo.plugin.spi.VariantProcessor 9 | import com.omooo.plugin.util.nameCapitalize 10 | import com.omooo.plugin.util.project 11 | import org.gradle.api.UnknownTaskException 12 | 13 | /** 14 | * Author: Omooo 15 | * Date: 2023/5/24 16 | * Desc: 注册 [ListPackageNameTask] 17 | */ 18 | @Suppress("SwallowedException") 19 | @AutoService(VariantProcessor::class) 20 | class ListPackageNameVariantProcessor : VariantProcessor { 21 | 22 | override fun process(variant: Variant) { 23 | val listPackageNameTask = try { 24 | variant.project.tasks.named("listPackageName") 25 | } catch (e: UnknownTaskException) { 26 | variant.project.tasks.register("listPackageName") { 27 | it.group = LAVENDER 28 | it.description = "List package name in app project." 29 | } 30 | } 31 | variant.project.tasks.register("listPackageNameFor${variant.nameCapitalize()}", ListPackageNameTask::class.java) { 32 | it.variant = variant 33 | it.apkFileCollection = variant.project.files(variant.artifacts.get(SingleArtifact.APK)) 34 | it.group = LAVENDER 35 | it.description = "List package name for ${variant.name}." 36 | it.outputs.upToDateWhen { false } 37 | }.also { 38 | listPackageNameTask.dependsOn(it) 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListPermissionTask.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.android.build.gradle.internal.publishing.AndroidArtifacts 5 | import com.omooo.plugin.util.getArtifactName 6 | import com.omooo.plugin.util.variantImpl 7 | import com.omooo.plugin.util.writeToJson 8 | import org.gradle.api.DefaultTask 9 | import org.gradle.api.tasks.Internal 10 | import org.gradle.api.tasks.TaskAction 11 | import java.util.regex.Pattern 12 | 13 | /** 14 | * Author: Omooo 15 | * Date: 2019/9/27 16 | * Version: v0.1.0 17 | * Desc: 输出 app 及其依赖的 aar 权限信息 18 | * Use: ./gradlew listPermissions 19 | * Output: projectDir/permissions.json 20 | */ 21 | internal abstract class ListPermissionTask : DefaultTask() { 22 | 23 | @get:Internal 24 | lateinit var variant: Variant 25 | 26 | @TaskAction 27 | fun doAction() { 28 | println( 29 | """ 30 | ********************************************* 31 | ********* -- ListPermissionTask -- ********** 32 | ***** -- projectDir/permissions.json -- ***** 33 | ********************************************* 34 | """.trimIndent() 35 | ) 36 | 37 | val resultMap = HashMap>() 38 | // 获取 app 模块的权限 39 | getAppModulePermission(resultMap) 40 | // 获取 app 依赖的 aar 权限 41 | variant.variantImpl.variantDependencies.getArtifactCollection( 42 | AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH, 43 | AndroidArtifacts.ArtifactScope.ALL, 44 | AndroidArtifacts.ArtifactType.MANIFEST 45 | ).artifacts.asSequence().filter { artifact -> 46 | !resultMap.containsKey(artifact.getArtifactName()) 47 | && matchPermission(artifact.file.readText()).isNotEmpty() 48 | }.forEach { 49 | resultMap[it.getArtifactName()] = matchPermission(it.file.readText()) 50 | }.also { 51 | resultMap.writeToJson("${project.parent?.projectDir}/permissions.json") 52 | } 53 | if (project.hasProperty("simpleStyle")) { 54 | resultMap.values.flatten().toSet().sorted().apply { 55 | writeToJson("${project.parent?.projectDir}/permissionSet.json") 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * 获取 app 模块的权限信息 62 | */ 63 | private fun getAppModulePermission(map: HashMap>) { 64 | val file = project.projectDir.resolve("src/main/AndroidManifest.xml") 65 | if (file.exists()) { 66 | map["app"] = matchPermission(file.readText()) 67 | } else { 68 | println("App manifest is missing for path ${file.absolutePath}") 69 | } 70 | } 71 | 72 | /** 73 | * 根据 Manifest 文件匹配权限信息 74 | */ 75 | private fun matchPermission(text: String): List { 76 | val list = ArrayList() 77 | val pattern = Pattern.compile("") 78 | val matcher = pattern.matcher(text) 79 | while (matcher.find()) { 80 | list.add(matcher.group()) 81 | } 82 | return list 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListPermissionVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.android.build.gradle.internal.tasks.factory.dependsOn 5 | import com.omooo.plugin.spi.VariantProcessor 6 | import com.google.auto.service.AutoService 7 | import com.omooo.plugin.bean.LAVENDER 8 | import com.omooo.plugin.util.nameCapitalize 9 | import com.omooo.plugin.util.processManifestTaskProvider 10 | import com.omooo.plugin.util.project 11 | import org.gradle.api.UnknownTaskException 12 | 13 | /** 14 | * Author: Omooo 15 | * Date: 2019/9/27 16 | * Version: v0.1.0 17 | * Desc: 注册 ListPermissionTask 18 | * @see ListPermissionTask 19 | */ 20 | @AutoService(VariantProcessor::class) 21 | class ListPermissionVariantProcessor : VariantProcessor { 22 | 23 | override fun process(variant: Variant) { 24 | val project = variant.project 25 | val listPermissionsTask = try { 26 | project.tasks.named("listPermissions") 27 | } catch (e: UnknownTaskException) { 28 | project.tasks.register("listPermissions") { 29 | it.group = LAVENDER 30 | it.description = "List the permission declared in AndroidManifest.xml" 31 | } 32 | } 33 | project.tasks.register("listPermissionsFor${variant.nameCapitalize()}", ListPermissionTask::class.java) { 34 | it.variant = variant 35 | it.group = LAVENDER 36 | it.description = "List the permission declared in AndroidManifest.xml for ${variant.name}." 37 | it.outputs.upToDateWhen { false } 38 | }.also { 39 | it.dependsOn(variant.processManifestTaskProvider) 40 | listPermissionsTask.dependsOn(it) 41 | } 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListSchemeTask.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.android.build.gradle.internal.publishing.AndroidArtifacts 5 | import com.omooo.plugin.util.* 6 | import com.omooo.plugin.util.writeToJson 7 | import org.gradle.api.DefaultTask 8 | import org.gradle.api.file.FileCollection 9 | import org.gradle.api.tasks.InputFiles 10 | import org.gradle.api.tasks.Internal 11 | import org.gradle.api.tasks.TaskAction 12 | 13 | /** 14 | * Author: Omooo 15 | * Date: 2023/3/16 16 | * Desc: 输出 Manifest 里定义的 scheme 集合 17 | * Use: ./gradlew listSchemes 18 | * Output: projectDir/schemes.json 19 | */ 20 | internal abstract class ListSchemeTask : DefaultTask() { 21 | 22 | @get:Internal 23 | lateinit var variant: Variant 24 | @get:InputFiles 25 | abstract var apkFileCollection: FileCollection 26 | 27 | @TaskAction 28 | fun run() { 29 | println( 30 | """ 31 | ********************************************* 32 | ********** -- ListSchemeTask -- ************* 33 | ******* -- projectDir/schemes.json -- ******* 34 | ********************************************* 35 | """.trimIndent() 36 | ) 37 | 38 | val startTime = System.currentTimeMillis() 39 | val ownerShip = project.getOwnerShip() 40 | val classOwnerMap = variant.getArtifactClassMap().mapValues { 41 | ownerShip.getOwner(it.value.first) 42 | } 43 | variant.getArtifactFiles(AndroidArtifacts.ArtifactType.MANIFEST) 44 | .map { 45 | it.parseSchemesFromManifest() 46 | }.filter { 47 | it.isNotEmpty() 48 | }.flatMap { 49 | it.entries 50 | }.associate { 51 | it.toPair() 52 | }.mapValues { 53 | Pair(classOwnerMap.getOrDefault(it.key, "unknown"), it.value) 54 | }.toSortedMap().writeToJson("${project.parent?.projectDir}/schemes.json") 55 | println( 56 | green("ListSchemeTask execute success in ${System.currentTimeMillis() - startTime}ms") 57 | ) 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListSchemeVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.Variant 5 | import com.android.build.gradle.internal.tasks.factory.dependsOn 6 | import com.google.auto.service.AutoService 7 | import com.omooo.plugin.bean.LAVENDER 8 | import com.omooo.plugin.spi.VariantProcessor 9 | import com.omooo.plugin.util.nameCapitalize 10 | import com.omooo.plugin.util.processManifestTaskProvider 11 | import com.omooo.plugin.util.project 12 | import org.gradle.api.UnknownTaskException 13 | 14 | /** 15 | * Author: Omooo 16 | * Date: 2023/3/16 17 | * Desc: 注册 [ListSchemeTask] 18 | */ 19 | @AutoService(VariantProcessor::class) 20 | class ListSchemeVariantProcessor : VariantProcessor { 21 | 22 | @Suppress("SwallowedException") 23 | override fun process(variant: Variant) { 24 | val project = variant.project 25 | val listPermissionsTask = try { 26 | project.tasks.named("listSchemes") 27 | } catch (e: UnknownTaskException) { 28 | project.tasks.register("listSchemes") { 29 | it.group = LAVENDER 30 | it.description = "List the schemes declared in AndroidManifest.xml" 31 | } 32 | } 33 | project.tasks.register("listSchemesFor${variant.nameCapitalize()}", ListSchemeTask::class.java) { 34 | it.variant = variant 35 | it.apkFileCollection = project.files(variant.artifacts.get(SingleArtifact.APK)) 36 | it.group = LAVENDER 37 | it.description = "List the schemes declared in AndroidManifest.xml for ${variant.name}." 38 | it.outputs.upToDateWhen { false } 39 | }.also { 40 | // 因为要归属是负责该 scheme,所以需要依赖 JarTask 41 | it.dependsOn(variant.processManifestTaskProvider) 42 | listPermissionsTask.dependsOn(it) 43 | } 44 | 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListUnusedAssetsVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.Variant 5 | import com.android.build.gradle.internal.tasks.factory.dependsOn 6 | import com.omooo.plugin.spi.VariantProcessor 7 | import com.google.auto.service.AutoService 8 | import com.omooo.plugin.bean.LAVENDER 9 | import com.omooo.plugin.util.project 10 | 11 | /** 12 | * Author: Omooo 13 | * Date: 2022/01/11 14 | * Desc: 注册 [ListUnusedAssetsTask] 15 | */ 16 | @AutoService(VariantProcessor::class) 17 | class ListUnusedAssetsVariantProcessor : VariantProcessor { 18 | 19 | override fun process(variant: Variant) { 20 | val project = variant.project 21 | if (variant.name.lowercase().contains("debug")) { 22 | return 23 | } 24 | if (project.tasks.findByName("listUnusedAssets") != null) { 25 | return 26 | } 27 | project.tasks.register("listUnusedAssets", ListUnusedAssetsTask::class.java) { 28 | it.variant = variant 29 | it.apkFileCollection = project.files(variant.artifacts.get(SingleArtifact.APK)) 30 | it.group = LAVENDER 31 | it.description = "List unused assets in app project" 32 | }.also { 33 | // it.dependsOn(project.tasks.named("assembleRelease")) 34 | // it.get().mustRunAfter(project.tasks.named("assembleRelease").get()) 35 | } 36 | 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListUnusedClassVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.Variant 5 | import com.google.auto.service.AutoService 6 | import com.omooo.plugin.bean.LAVENDER 7 | import com.omooo.plugin.spi.VariantProcessor 8 | import com.omooo.plugin.util.nameCapitalize 9 | import com.omooo.plugin.util.project 10 | 11 | /** 12 | * Author: Omooo 13 | * Date: 2023/3/8 14 | * Desc: 注册 [ListUnusedClassTask] 15 | */ 16 | @AutoService(VariantProcessor::class) 17 | class ListUnusedClassVariantProcessor : VariantProcessor { 18 | 19 | override fun process(variant: Variant) { 20 | if (variant.name.contains("debug", true)){ 21 | return 22 | } 23 | val project = variant.project 24 | project.tasks.register("listUnusedClassFor${variant.nameCapitalize()}", ListUnusedClassTask::class.java) { 25 | it.variant = variant 26 | it.apkFileCollection = variant.project.files(variant.artifacts.get(SingleArtifact.APK)) 27 | it.group = LAVENDER 28 | it.description = "List unused class declared in application project for ${variant.name}." 29 | it.outputs.upToDateWhen { false } 30 | } 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/ListUnusedResVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.Variant 5 | import com.android.build.gradle.internal.tasks.factory.dependsOn 6 | import com.omooo.plugin.spi.VariantProcessor 7 | import com.google.auto.service.AutoService 8 | import com.omooo.plugin.bean.LAVENDER 9 | import com.omooo.plugin.util.nameCapitalize 10 | import com.omooo.plugin.util.project 11 | import org.gradle.api.DefaultTask 12 | import org.gradle.api.UnknownTaskException 13 | import org.gradle.api.tasks.Internal 14 | import org.gradle.api.tasks.TaskAction 15 | import java.io.File 16 | 17 | /** 18 | * Author: Omooo 19 | * Date: 2022/12/14 20 | * Desc: 注册 [ListUnusedResTask] 21 | */ 22 | @AutoService(VariantProcessor::class) 23 | class ListUnusedResVariantProcessor : VariantProcessor { 24 | 25 | override fun process(variant: Variant) { 26 | if (variant.name.contains("debug", true)) { 27 | return 28 | } 29 | val project = variant.project 30 | val listUnusedResTask = try { 31 | project.tasks.named("listUnusedRes") 32 | } catch (e: UnknownTaskException) { 33 | project.tasks.register("listUnusedRes") { 34 | it.group = LAVENDER 35 | it.description = "List unused res in app project" 36 | } 37 | } 38 | project.tasks.register("listUnusedResFor${variant.nameCapitalize()}", ListUnusedResTask::class.java) { 39 | it.variant = variant 40 | it.apkFileCollection = project.files(variant.artifacts.get(SingleArtifact.APK)) 41 | it.group = LAVENDER 42 | it.description = "List unused res for ${variant.name} in app project" 43 | }.also { 44 | listUnusedResTask.dependsOn(it) 45 | } 46 | 47 | if (project.properties.containsKey("strictMode")) { 48 | project.tasks.register("strictTaskInternalFor${variant.nameCapitalize()}", StrictTask::class.java) { 49 | it.variant = variant 50 | }.also { 51 | // project.tasks.named("shrink${variant.nameCapitalize()}Res").apply { 52 | // this.dependsOn(it) 53 | // this.get().mustRunAfter(it) 54 | // } 55 | } 56 | } 57 | 58 | } 59 | 60 | } 61 | 62 | /** 63 | * 设置 shrinkResources 严格模式 Task 64 | */ 65 | internal open class StrictTask : DefaultTask() { 66 | 67 | @get:Internal 68 | lateinit var variant: Variant 69 | 70 | @TaskAction 71 | fun run() { 72 | val resDir = File("${project.buildDir.absolutePath}/intermediates/merged-not-compiled-resources/${variant.flavorName}/${variant.buildType}") 73 | if (!resDir.exists() && !resDir.isDirectory) { 74 | println("merged-not-compiled-resources/${variant.flavorName}/${variant.buildType} dir is not exists.") 75 | return 76 | } 77 | File("$resDir/xml").apply { 78 | if (!exists()) { 79 | mkdir() 80 | } 81 | File(this, "lavender-keep-${System.currentTimeMillis()}.xml") 82 | .writeText(KEEP_STRICT_RES_CONTENT) 83 | } 84 | } 85 | 86 | } 87 | 88 | private const val KEEP_STRICT_RES_CONTENT = """ 89 | 92 | """ -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/RepeatResDetectorTask.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.ANDROID_RES 5 | import com.omooo.plugin.util.encode 6 | import com.omooo.plugin.util.getArtifactFiles 7 | import com.omooo.plugin.util.writeToJson 8 | import org.gradle.api.DefaultTask 9 | import org.gradle.api.tasks.Internal 10 | import org.gradle.api.tasks.TaskAction 11 | import java.io.File 12 | 13 | /** 14 | * Author: Omooo 15 | * Date: 2019/9/27 16 | * Version: v0.1.0 17 | * Desc: 重复资源监测 18 | * Use: ./gradlew detectRepeatRes 19 | * Output: projectDir/repeatRes.json 20 | */ 21 | internal abstract class RepeatResDetectorTask : DefaultTask() { 22 | 23 | @get:Internal 24 | lateinit var variant: Variant 25 | 26 | @TaskAction 27 | fun run() { 28 | println( 29 | """ 30 | ********************************************* 31 | ******** -- RepeatResDetectorTask -- ******** 32 | ****** -- projectDir/repeatRes.json -- ****** 33 | ********************************************* 34 | """.trimIndent() 35 | ) 36 | 37 | val resultMap = HashMap>() 38 | val prefix = if (project.properties["all"] != "true") "drawable-" else "drawable" 39 | 40 | variant.getArtifactFiles(ANDROID_RES).plus(project.projectDir.resolve("src/main/res")).forEach { resDir -> 41 | resDir.listFiles()?.filter { 42 | it.isDirectory && it.name.startsWith(prefix) 43 | }?.forEach { drawableDir -> 44 | drawableDir.listFiles()?.filter { 45 | !it.isDirectory 46 | }?.forEach { file -> 47 | resultMap.getOrDefault(file.readBytes().encode(), arrayListOf()).apply { 48 | add(file.absolutePath) 49 | }.also { 50 | resultMap[file.readBytes().encode()] = it 51 | } 52 | } 53 | } 54 | } 55 | 56 | var totalSize: Long = 0 57 | resultMap.filterValues { values -> 58 | values.size > 1 59 | }.apply { 60 | this.values.forEach { 61 | totalSize += File(it[0]).length() 62 | } 63 | 64 | println("Repeat Res count: ${keys.size}, total size: ${totalSize / 1000}kb") 65 | this.writeToJson("${project.parent?.projectDir}/repeatRes.json") 66 | } 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/task/RepeatResDetectorVariantProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.task 2 | 3 | import com.android.build.api.variant.Variant 4 | import com.omooo.plugin.spi.VariantProcessor 5 | import com.google.auto.service.AutoService 6 | import com.omooo.plugin.bean.LAVENDER 7 | import com.omooo.plugin.util.project 8 | 9 | /** 10 | * Author: Omooo 11 | * Date: 2019/9/27 12 | * Version: v0.1.0 13 | * Desc: 注册 RepeatResDetectorTask 14 | * @see RepeatResDetectorTask 15 | */ 16 | @AutoService(VariantProcessor::class) 17 | class RepeatResDetectorVariantProcessor : VariantProcessor { 18 | override fun process(variant: Variant) { 19 | val project = variant.project 20 | if (project.tasks.findByName("repeatRes") != null) { 21 | return 22 | } 23 | project.tasks.register("repeatRes", RepeatResDetectorTask::class.java) { 24 | it.variant = variant 25 | it.group = LAVENDER 26 | it.description = "Check repeat resources in app project" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/transform/BaseClassNode.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.transform 2 | 3 | import com.omooo.plugin.bean.ASM_VERSION 4 | import org.objectweb.asm.ClassVisitor 5 | import org.objectweb.asm.tree.ClassNode 6 | 7 | /** 8 | * Author: Omooo 9 | * Date: 2022/11/6 10 | * Desc: 使用 [ClassNode] 操作字节码基础类 11 | */ 12 | internal abstract class BaseClassNode(private val classVisitor: ClassVisitor) : 13 | ClassNode(ASM_VERSION) { 14 | 15 | override fun visitEnd() { 16 | super.visitEnd() 17 | accept(classVisitor) 18 | transform() 19 | } 20 | 21 | abstract fun transform() 22 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/transform/BaseClassVisitor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.transform 2 | 3 | import com.omooo.plugin.bean.ASM_VERSION 4 | import org.objectweb.asm.ClassVisitor 5 | 6 | /** 7 | * Author: Omooo 8 | * Date: 2022/11/6 9 | * Desc: 使用 [ClassVisitor] 操作字节码基础类 10 | */ 11 | internal abstract class BaseClassVisitor(classVisitor: ClassVisitor) : 12 | ClassVisitor(ASM_VERSION, classVisitor) { 13 | 14 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/transform/invoke/InvokeCheckClassNode.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.transform.invoke 2 | 3 | import com.omooo.plugin.transform.BaseClassNode 4 | import com.omooo.plugin.util.TransformReporter 5 | import org.objectweb.asm.ClassVisitor 6 | import org.objectweb.asm.tree.FieldInsnNode 7 | import org.objectweb.asm.tree.LdcInsnNode 8 | import org.objectweb.asm.tree.MethodInsnNode 9 | import org.objectweb.asm.tree.MethodNode 10 | 11 | /** 12 | * Author: Omooo 13 | * Date: 2022/11/6 14 | * Desc: 检测方法调用 15 | */ 16 | internal class InvokeCheckClassNode( 17 | classVisitor: ClassVisitor, 18 | private val params: InvokeCheckParams 19 | ) : BaseClassNode(classVisitor) { 20 | 21 | override fun transform() { 22 | methods.forEach { methodNode -> 23 | methodNode.instructions.filterIsInstance().forEach { insnNode -> 24 | params.packageList.filter { 25 | !name.startsWith(it) && insnNode.owner.startsWith(it) 26 | }.forEach { 27 | report(it.replace("/", "."), methodNode.getMethodPlainText()) 28 | } 29 | params.methodList.filter { 30 | // owner 是必须要有的,name 和 desc 可有可无 31 | insnNode.owner == it.first 32 | && (if (it.second.isNotEmpty()) insnNode.name == it.second else true) 33 | && (if (it.third.isNotEmpty()) insnNode.desc == it.third else true) 34 | }.map { 35 | if (it.second.isNotEmpty()) "${it.first.replace("/", ".")}#${it.second}${it.third}" 36 | else it.first.replace("/", ".") 37 | }.forEach { 38 | report(it, methodNode.getMethodPlainText()) 39 | } 40 | } 41 | 42 | // 常量检测 43 | if (params.constantsList.isNotEmpty()) { 44 | methodNode.instructions.filterIsInstance().filter { 45 | params.constantsList.contains(it.cst) 46 | }.forEach { 47 | report(it.cst.toString(), methodNode.getMethodPlainText()) 48 | } 49 | } 50 | 51 | // 字段检测 52 | if (params.fieldList.isNotEmpty()) { 53 | methodNode.instructions.filterIsInstance().forEach { fieldNode -> 54 | params.fieldList.filter { 55 | it.first == fieldNode.owner && it.second == fieldNode.name && it.third == fieldNode.desc 56 | }.map { 57 | "${it.first.replace("/", ".")}.${it.second}:${it.third}" 58 | }.forEach { 59 | report(it, methodNode.getMethodPlainText()) 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * 获取方法调用的文本描述 68 | * 69 | * ag: android.widget.Toast#show()V 70 | */ 71 | private fun MethodNode.getMethodPlainText(): String { 72 | return "${this@InvokeCheckClassNode.name.replace("/", ".")}#${name}${desc}" 73 | } 74 | 75 | /** 76 | * 输出报告 77 | * 78 | * @param key 待检测的 method/package 79 | * @param text 调用方 80 | */ 81 | private fun report(key: String, text: String) { 82 | TransformReporter.writeJsonLineByLine(REPORTER_FILE_NAME, key, text) 83 | } 84 | 85 | } 86 | 87 | /** 输出报告的文件名 */ 88 | private const val REPORTER_FILE_NAME = "invokeCheckReport.json" -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/transform/invoke/InvokeCheckCvFactory.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.transform.invoke 2 | 3 | import com.android.build.api.instrumentation.* 4 | import com.omooo.plugin.util.isRClass 5 | import com.omooo.plugin.util.isSystemClass 6 | import org.objectweb.asm.ClassVisitor 7 | 8 | /** 9 | * Author: Omooo 10 | * Date: 2022/11/5 11 | * Desc: 通用 ClassVisitorFactory 12 | */ 13 | abstract class InvokeCheckCvFactory : AsmClassVisitorFactory { 14 | 15 | override fun createClassVisitor( 16 | classContext: ClassContext, 17 | nextClassVisitor: ClassVisitor 18 | ): ClassVisitor { 19 | if (classContext.currentClassData.isRClass() 20 | || classContext.currentClassData.isSystemClass() 21 | ) { 22 | return nextClassVisitor 23 | } 24 | return InvokeCheckClassNode(nextClassVisitor, parameters.get()) 25 | } 26 | 27 | override fun isInstrumentable(classData: ClassData): Boolean { 28 | return true 29 | } 30 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/transform/invoke/InvokeCheckParams.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.transform.invoke 2 | 3 | import com.android.build.api.instrumentation.InstrumentationParameters 4 | import org.gradle.api.tasks.Input 5 | 6 | /** 7 | * Author: Omooo 8 | * Date: 2022/11/6 9 | * Desc: 检测方法、常量等调用的参数 10 | */ 11 | internal interface InvokeCheckParams : InstrumentationParameters { 12 | 13 | @get:Input 14 | var methodList: List> 15 | 16 | @get:Input 17 | var packageList: List 18 | 19 | @get:Input 20 | var constantsList: List 21 | 22 | @get:Input 23 | var fieldList: List> 24 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/transform/shrinkres/IdentifierCheckClassNode.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.transform.shrinkres 2 | 3 | import com.omooo.plugin.transform.BaseClassNode 4 | import com.omooo.plugin.util.TransformReporter 5 | import org.objectweb.asm.ClassVisitor 6 | import org.objectweb.asm.Opcodes 7 | import org.objectweb.asm.tree.AbstractInsnNode 8 | import org.objectweb.asm.tree.LdcInsnNode 9 | import org.objectweb.asm.tree.MethodInsnNode 10 | import org.objectweb.asm.tree.MethodNode 11 | 12 | /** 13 | * Author: Omooo 14 | * Date: 2022/12/9 15 | * Desc: resources.getIdentifier 调用检查 16 | */ 17 | internal class IdentifierCheckClassNode(classVisitor: ClassVisitor) : BaseClassNode(classVisitor) { 18 | 19 | override fun transform() { 20 | methods.filter { 21 | it.name == "onCreate" 22 | }.forEach { methodNode -> 23 | methodNode.instructions.forEachIndexed { index, insnNode -> 24 | // 从 Resources#getIdentifier 调用点开始往前找到第一个 Idc 指令 25 | // 直到遇到 getResources() 调用点结束 26 | if (insnNode.isInvokeGetIdentifier()) { 27 | report(methodNode, "") 28 | for (i in index - 1 downTo 0) { 29 | val insn = methodNode.instructions[i] 30 | if (insn is LdcInsnNode && insn.cst is String) { 31 | report(methodNode, insn.cst.toString()) 32 | } 33 | // if (insn.isInvokeGetResources()) { 34 | // break 35 | // } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * 上报 44 | * 45 | * @param methodNode [MethodNode] 调用点 46 | * @param resName assets 文件名 47 | */ 48 | private fun report(methodNode: MethodNode, resName: String) { 49 | TransformReporter.writeJsonLineByLine("resIdentifier.json", "$name#${methodNode.name}${methodNode.desc}", resName) 50 | } 51 | 52 | private fun AbstractInsnNode.isInvokeGetIdentifier(): Boolean { 53 | return this is MethodInsnNode 54 | && opcode == Opcodes.INVOKEVIRTUAL 55 | && owner == OWNER_RESOURCES 56 | && name == METHOD_NAME_GET_IDENTIFIER 57 | && desc == METHOD_DESC_GET_IDENTIFIER 58 | } 59 | 60 | private fun AbstractInsnNode.isInvokeGetResources(): Boolean { 61 | return this is MethodInsnNode 62 | && opcode == Opcodes.INVOKEVIRTUAL 63 | && name == METHOD_NAME_GET_RESOURCES 64 | && desc == METHOD_DESC_GET_RESOURCES 65 | } 66 | } 67 | 68 | private const val OWNER_RESOURCES = "android/content/res/Resources" 69 | private const val METHOD_NAME_GET_IDENTIFIER = "getIdentifier" 70 | private const val METHOD_DESC_GET_IDENTIFIER = 71 | "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I" 72 | 73 | private const val METHOD_NAME_GET_RESOURCES = "getResources" 74 | private const val METHOD_DESC_GET_RESOURCES = "()Landroid/content/res/Resources;" 75 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/transform/shrinkres/IdentifierCheckCvFactory.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.transform.shrinkres 2 | 3 | import com.android.build.api.instrumentation.AsmClassVisitorFactory 4 | import com.android.build.api.instrumentation.ClassContext 5 | import com.android.build.api.instrumentation.ClassData 6 | import com.android.build.api.instrumentation.InstrumentationParameters 7 | import com.omooo.plugin.util.isRClass 8 | import com.omooo.plugin.util.isSystemClass 9 | import org.objectweb.asm.ClassVisitor 10 | 11 | /** 12 | * Author: Omooo 13 | * Date: 2022/12/09 14 | * Desc: resources.getIdentifier 调用检查 15 | */ 16 | abstract class IdentifierCheckCvFactory : 17 | AsmClassVisitorFactory { 18 | 19 | override fun createClassVisitor( 20 | classContext: ClassContext, 21 | nextClassVisitor: ClassVisitor 22 | ): ClassVisitor { 23 | if (classContext.currentClassData.isRClass() 24 | || classContext.currentClassData.isSystemClass() 25 | ) { 26 | return nextClassVisitor 27 | } 28 | return IdentifierCheckClassNode(nextClassVisitor) 29 | } 30 | 31 | override fun isInstrumentable(classData: ClassData): Boolean { 32 | return true 33 | } 34 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/transform/systrace/SystraceClassVisitor.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.transform.systrace 2 | 3 | import com.omooo.plugin.bean.ASM_VERSION 4 | import com.omooo.plugin.transform.BaseClassVisitor 5 | import org.objectweb.asm.ClassVisitor 6 | import org.objectweb.asm.MethodVisitor 7 | import org.objectweb.asm.Opcodes 8 | import org.objectweb.asm.commons.AdviceAdapter 9 | import java.lang.reflect.Modifier 10 | 11 | /** 12 | * Author: Omooo 13 | * Date: 2022/12/28 14 | * Desc: 15 | */ 16 | internal class SystraceClassVisitor(classVisitor: ClassVisitor) : BaseClassVisitor(classVisitor) { 17 | 18 | var traceClassFlag = false 19 | 20 | var className = "" 21 | 22 | override fun visit( 23 | version: Int, 24 | access: Int, 25 | name: String, 26 | signature: String?, 27 | superName: String?, 28 | interfaces: Array? 29 | ) { 30 | traceClassFlag = !Modifier.isAbstract(access) && !Modifier.isInterface(access) 31 | && !Modifier.isNative(access) && 0 == (access and Opcodes.ACC_ANNOTATION) 32 | className = name 33 | super.visit(version, access, name, signature, superName, interfaces) 34 | } 35 | 36 | override fun visitMethod( 37 | access: Int, 38 | name: String, 39 | descriptor: String, 40 | signature: String?, 41 | exceptions: Array? 42 | ): MethodVisitor { 43 | if (!traceClassFlag || Modifier.isAbstract(access) || Modifier.isInterface(access) 44 | || Modifier.isNative(access) 45 | || name == "" || name == "" 46 | ) { 47 | return super.visitMethod(access, name, descriptor, signature, exceptions) 48 | } 49 | val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions) 50 | return InternalMethodVisitor(className, methodVisitor, access, name, descriptor) 51 | } 52 | 53 | class InternalMethodVisitor( 54 | private val className: String, 55 | methodVisitor: MethodVisitor, 56 | access: Int, 57 | private val methodName: String, 58 | descriptor: String 59 | ) : 60 | AdviceAdapter(ASM_VERSION, methodVisitor, access, methodName, descriptor) { 61 | 62 | override fun onMethodEnter() { 63 | println("植入成功_: $className#$methodName") 64 | val sectionName = "$className#$methodName".let { 65 | if (it.length > 127) "${it.substring(0, 124)}..." else it 66 | } 67 | mv.visitLdcInsn(sectionName) 68 | mv.visitMethodInsn( 69 | Opcodes.INVOKESTATIC, 70 | "android/os/Trace", 71 | "beginSection", 72 | "(Ljava/lang/String;)V", 73 | false 74 | ) 75 | super.onMethodEnter() 76 | } 77 | 78 | override fun onMethodExit(opcode: Int) { 79 | mv.visitMethodInsn( 80 | Opcodes.INVOKESTATIC, 81 | "android/os/Trace", 82 | "endSection", 83 | "()V", 84 | false 85 | ) 86 | super.onMethodExit(opcode) 87 | } 88 | 89 | } 90 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/transform/systrace/SystraceCvFactory.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.transform.systrace 2 | 3 | import com.android.build.api.instrumentation.AsmClassVisitorFactory 4 | import com.android.build.api.instrumentation.ClassContext 5 | import com.android.build.api.instrumentation.ClassData 6 | import com.android.build.api.instrumentation.InstrumentationParameters 7 | import com.omooo.plugin.bean.ASM_VERSION 8 | import com.omooo.plugin.util.* 9 | import com.omooo.plugin.util.isInstanceMethod 10 | import com.omooo.plugin.util.isMethodReturn 11 | import com.omooo.plugin.util.isRClass 12 | import org.objectweb.asm.ClassVisitor 13 | import org.objectweb.asm.Opcodes 14 | import org.objectweb.asm.tree.* 15 | import java.lang.reflect.Modifier 16 | 17 | /** 18 | * Author: Omooo 19 | * Date: 2022/12/28 20 | * Desc: Systrace 自定义插桩 21 | */ 22 | abstract class SystraceCvFactory : 23 | AsmClassVisitorFactory { 24 | 25 | override fun createClassVisitor( 26 | classContext: ClassContext, 27 | nextClassVisitor: ClassVisitor 28 | ): ClassVisitor { 29 | if (classContext.currentClassData.isRClass() 30 | || classContext.currentClassData.isSystemClass() 31 | || classContext.currentClassData.className.startsWith("org.bouncycastle") 32 | ) { 33 | return nextClassVisitor 34 | } 35 | return SystraceClassVisitor(nextClassVisitor) 36 | } 37 | 38 | override fun isInstrumentable(classData: ClassData): Boolean { 39 | return true 40 | } 41 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/ClassDataExt.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | import com.android.build.api.instrumentation.ClassData 4 | 5 | /** 6 | * 是否是 R 文件 7 | * 8 | * @return true: R 文件 9 | */ 10 | internal fun ClassData.isRClass(): Boolean { 11 | return className.split(".").lastOrNull()?.let { 12 | it == "R" || it.startsWith("R$") 13 | } ?: false 14 | } 15 | 16 | /** 17 | * 是否是系统类 18 | * 19 | * @return true: 系统类 20 | */ 21 | internal fun ClassData.isSystemClass(): Boolean { 22 | val filterList = arrayOf( 23 | "kotlin.", "org.intellij.", "androidx.", 24 | "com.google.", "org.jetbrains.", "android.", 25 | ) 26 | return filterList.find { 27 | className.startsWith(it) 28 | } != null 29 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/ClassNodeExt.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | import org.objectweb.asm.Opcodes 4 | import org.objectweb.asm.tree.ClassNode 5 | 6 | /** 7 | * Author: Omooo 8 | * Date: 2022/12/28 9 | * Desc: ClassNode 扩展方法 10 | */ 11 | 12 | val ClassNode.className: String 13 | get() = name.replace('/', '.') 14 | 15 | val ClassNode.isAnnotation: Boolean 16 | get() = 0 != (access and Opcodes.ACC_ANNOTATION) 17 | 18 | val ClassNode.isInterface: Boolean 19 | get() = 0 != (access and Opcodes.ACC_INTERFACE) 20 | 21 | val ClassNode.isAbstract: Boolean 22 | get() = 0 != (access and Opcodes.ACC_ABSTRACT) 23 | 24 | val ClassNode.isPublic: Boolean 25 | get() = 0 != (access and Opcodes.ACC_PUBLIC) 26 | 27 | val ClassNode.isProtected: Boolean 28 | get() = 0 != (access and Opcodes.ACC_PROTECTED) 29 | 30 | val ClassNode.isPrivate: Boolean 31 | get() = 0 != (access and Opcodes.ACC_PRIVATE) 32 | 33 | val ClassNode.isStatic: Boolean 34 | get() = 0 != (access and Opcodes.ACC_STATIC) 35 | 36 | val ClassNode.isFinal: Boolean 37 | get() = 0 != (access and Opcodes.ACC_FINAL) -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/CollectionsExt.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | import org.json.JSONArray 4 | import org.json.JSONObject 5 | 6 | /** 7 | * Author: Omooo 8 | * Date: 2022/11/16 9 | * Desc: 集合相关扩展方法 10 | */ 11 | 12 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/ConsoleExt.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2023/3/12 6 | * Desc: 日志输出文本颜色设置 7 | */ 8 | 9 | private const val ESC = '\u001B' 10 | private const val CSI_RESET = "$ESC[0m" 11 | private const val CSI_RED = "$ESC[31m" 12 | private const val CSI_GREEN = "$ESC[32m" 13 | private const val CSI_YELLOW = "$ESC[33m" 14 | 15 | internal fun red(s: Any) = "${CSI_RED}${s}${CSI_RESET}" 16 | internal fun green(s: Any) = "${CSI_GREEN}${s}${CSI_RESET}" 17 | internal fun yellow(s: Any) = "${CSI_YELLOW}${s}${CSI_RESET}" 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/InsnNodeExt.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | import org.objectweb.asm.tree.MethodInsnNode 4 | 5 | /** 6 | * [MethodInsnNode] 转文本 7 | */ 8 | internal fun MethodInsnNode.toPlainText(): String { 9 | return "$owner#$name$desc" 10 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/ManifestFileExt.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | import org.w3c.dom.Element 4 | import java.io.File 5 | import javax.xml.parsers.DocumentBuilderFactory 6 | 7 | /** 8 | * Author: Omooo 9 | * Date: 2023/8/21 10 | * Desc: Manifest 文件解析相关扩展方法 11 | */ 12 | 13 | /** 14 | * 解析 Manifest 文件 15 | * 16 | * @return { "com.xxx.SampleActivity": "scheme://home/mall, scheme://home/mine" } 17 | */ 18 | @Suppress("NestedBlockDepth") 19 | internal fun File.parseSchemesFromManifest(): Map { 20 | val result = mutableMapOf() 21 | DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(this).apply { 22 | val manifestNode = getElementsByTagName("manifest").item(0) as Element 23 | val packageName = manifestNode.getAttribute("package") 24 | 25 | val activityNodes = getElementsByTagName("activity") 26 | for (i in 0 until activityNodes.length) { 27 | val activityNode = activityNodes.item(i) as Element 28 | val activityName = activityNode.getAttribute("android:name").let { 29 | if (it.startsWith(packageName) || !it.startsWith(".")) { 30 | it 31 | } else { 32 | "$packageName$it" 33 | } 34 | } 35 | 36 | val dataNodes = activityNode.getElementsByTagName("data") 37 | if (dataNodes.length > 0) { 38 | var scheme = "" 39 | for (j in 0 until dataNodes.length) { 40 | val dataNode = dataNodes.item(j) as Element 41 | val dataValue = dataNode.getAttribute("android:scheme") + "://" + 42 | dataNode.getAttribute("android:host") + dataNode.getAttribute("android:path") 43 | scheme = if (scheme.isEmpty()) dataValue else "$scheme, $dataValue" 44 | } 45 | result[activityName] = scheme 46 | } 47 | } 48 | } 49 | return result 50 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/OpcodesExt.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | import org.objectweb.asm.Opcodes 4 | import java.lang.reflect.Modifier 5 | 6 | /** 7 | * Author: Omooo 8 | * Date: 2022/12/28 9 | * Desc: Opcodes 扩展函数 10 | */ 11 | 12 | internal fun Int.isMethodReturn(): Boolean { 13 | return (this >= Opcodes.IRETURN && this <= Opcodes.RETURN) 14 | || this == Opcodes.ATHROW 15 | } 16 | 17 | /** 18 | * 是否是实例方法 19 | */ 20 | internal fun Int.isInstanceMethod(): Boolean { 21 | return !Modifier.isNative(this) && !Modifier.isInterface(this) 22 | && !Modifier.isAbstract(this) 23 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/ProjectExt.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | import com.android.build.api.variant.Variant 4 | import org.gradle.api.Project 5 | import org.gradle.api.Task 6 | import org.gradle.api.tasks.TaskProvider 7 | import org.yaml.snakeyaml.Yaml 8 | 9 | /** 10 | * Author: Omooo 11 | * Date: 2023/2/10 12 | * Desc: [Project] 相关扩展函数 13 | */ 14 | 15 | /** 16 | * 获取归属人映射关系 17 | * 18 | * @return Mapx 19 | */ 20 | internal fun Project.getOwnerShip(): Map { 21 | val ownershipFile = parent?.projectDir?.resolve("$DIR_PLUGIN_FILES/$FILE_OWNERSHIP") 22 | if (ownershipFile?.exists() == true) { 23 | return Yaml().load>>(ownershipFile.readText()).entries.flatMap { entry -> 24 | entry.value.map { 25 | it to entry.key 26 | } 27 | }.toMap() 28 | } 29 | return emptyMap() 30 | } 31 | 32 | /** 33 | * 获取归属人映射关系 34 | * 35 | * @return Map>x 36 | */ 37 | internal fun Project.getOwnerMap(): Map> { 38 | val ownershipFile = parent?.projectDir?.resolve("$DIR_PLUGIN_FILES/$FILE_OWNERSHIP") 39 | if (ownershipFile?.exists() == true) { 40 | return Yaml().load(ownershipFile.readText()) 41 | } 42 | return emptyMap() 43 | } 44 | 45 | /** 46 | * 根据 AAR 的 groupId 前缀匹配出 owner 47 | */ 48 | internal fun Map.getOwner(aarName: String): String { 49 | return this.entries.find { 50 | aarName.startsWith(it.key) 51 | }?.value ?: "unknown" 52 | } 53 | 54 | /** 55 | * 是否是内部组件 56 | */ 57 | internal fun Project.isInternalComponent(aarName: String): Boolean { 58 | return getOwnerMap().flatMap { it.value }.find { 59 | aarName.startsWith(it) 60 | } != null 61 | } 62 | 63 | internal fun Project.getJarTaskProviders(variant: Variant): List> { 64 | return emptyList() 65 | } 66 | 67 | internal val Project.isAndroid: Boolean 68 | get() = plugins.hasPlugin("com.android.application") 69 | || plugins.hasPlugin("com.android.dynamic-feature") 70 | || plugins.hasPlugin("com.android.library") 71 | 72 | internal val Project.isJava: Boolean 73 | get() = plugins.hasPlugin("java") || isJavaLibrary 74 | 75 | internal val Project.isJavaLibrary: Boolean 76 | get() = plugins.hasPlugin("java-library") 77 | 78 | /** 是否需要输出报告 */ 79 | internal val Project.needPrintReporter: Boolean 80 | get() = hasProperty("printReporter") 81 | 82 | 83 | private const val DIR_PLUGIN_FILES = "lavender-plugin" 84 | private const val FILE_OWNERSHIP = "ownership.yaml" -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/StringExt.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | /** 4 | * Author: Omooo 5 | * Date: 2023/2/4 6 | * Desc: String 相关扩展函数 7 | */ 8 | 9 | /** 10 | * 从 AAR 全限定名中获取 group id 11 | */ 12 | internal fun String.getGroupIdFromAarName(): String { 13 | return substringBefore(":") 14 | } 15 | 16 | /** 17 | * 从 AAR 全限定名中移除版本信息 18 | * 19 | * ag: com.google.android.material:material:1.0.0 -> com.google.android.material:material 20 | */ 21 | internal fun String.removeVersionFromAarName(): String { 22 | return substringBeforeLast(":") 23 | } 24 | 25 | /** 26 | * 从 AAR 全限定名中获取 artifact id 27 | */ 28 | internal fun String.getArtifactIdFromAarName(): String { 29 | return substringAfter(":").substringBeforeLast(":") 30 | } 31 | 32 | /** 33 | * The following classes exclude from lint 34 | * 35 | * - `android.**` 36 | * - `androidx.**` 37 | * - `com.android.**` 38 | * - `com.google.android.**` 39 | * - `com.google.gson.**` 40 | * - `**.R` 41 | * - `**.R$*` 42 | * - `BuildConfig` 43 | */ 44 | private const val DOLLAR = '$' 45 | 46 | internal val EXCLUDES = Regex("^(((android[x]?)|(com/(((google/)?android)|(google/gson))))/.+)|(.+/((R[2]?(${DOLLAR}[a-z]+)?)|(BuildConfig)))$") 47 | 48 | internal fun String.formatDollar(): String { 49 | return replace("$", "${'$'}") 50 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/TransformReporter.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | import groovy.json.JsonOutput 4 | import org.json.JSONArray 5 | import org.json.JSONObject 6 | import java.io.File 7 | import java.io.FileWriter 8 | import java.io.PrintWriter 9 | import java.nio.charset.Charset 10 | 11 | /** 12 | * Author: Omooo 13 | * Date: 2023/1/8 14 | * Desc: Transform 阶段输出报告 15 | */ 16 | internal object TransformReporter { 17 | 18 | private const val DIR_REPORTER = "./reporter" 19 | 20 | /** 21 | * 删除 Transform 报告文件夹 22 | */ 23 | fun deleteTransformReporterDir() { 24 | File(DIR_REPORTER).takeIf { 25 | it.exists() 26 | }?.deleteRecursively() 27 | } 28 | 29 | /** 30 | * 逐行写入 Json 31 | * 32 | * @param fileName 文件名 33 | * @param key Json 的 key; 34 | * @param value Json 的 value;类型为 JSONArray 35 | */ 36 | @Synchronized 37 | fun writeJsonLineByLine(fileName: String, key: String, value: String) { 38 | runCatching { 39 | val file = File(File(DIR_REPORTER).apply { 40 | takeIf { !it.exists() }?.mkdirs() 41 | }, fileName).apply { 42 | takeIf { !exists() }?.createNewFile() 43 | if (readText().isEmpty()) { 44 | writeText("{}") 45 | } 46 | } 47 | JSONObject(file.readText()).let { 48 | if (it.optJSONArray(key) == null) { 49 | it.put(key, JSONArray().apply { 50 | put(value) 51 | }) 52 | } else if (!it.getJSONArray(key).contains(value)) { 53 | it.getJSONArray(key).put(value) 54 | } 55 | Pair(file, it.toString()) 56 | } 57 | }.onSuccess { (file, text) -> 58 | PrintWriter(FileWriter(file, Charset.defaultCharset())) 59 | .use { it.write(JsonOutput.prettyPrint(text)) } 60 | }.onFailure { 61 | println("TransformReporter#writeJsonLineByLine throw Exception: ${it.message}") 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | import groovy.json.JsonOutput 4 | import org.json.JSONObject 5 | import java.io.File 6 | import java.security.MessageDigest 7 | 8 | /** 9 | * Author: Omooo 10 | * Date: 2019/10/8 11 | * Version: v0.1.1 12 | * Desc: 一系列 Kotlin 扩展函数 13 | */ 14 | 15 | /** 16 | * 生成 ByteArray 的 MD5 值 17 | */ 18 | fun ByteArray.encode(): String { 19 | val instance: MessageDigest = MessageDigest.getInstance("MD5") 20 | val digest: ByteArray = instance.digest(this) 21 | val sb = StringBuffer() 22 | for (b in digest) { 23 | val i: Int = b.toInt() and 0xff 24 | var hexString = Integer.toHexString(i) 25 | if (hexString.length < 2) hexString = "0$hexString" 26 | sb.append(hexString) 27 | } 28 | return sb.toString() 29 | } 30 | 31 | /** 32 | * 将 Map 以 Json 文件输出 33 | */ 34 | fun Map.writeToJson(path: String) { 35 | val jsonFile = File(path) 36 | if (jsonFile.exists()) { 37 | jsonFile.delete() 38 | } 39 | jsonFile.createNewFile() 40 | val json = JsonOutput.toJson(this) 41 | jsonFile.writeText(JsonOutput.prettyPrint(json), Charsets.UTF_8) 42 | println("Reporter: ${jsonFile.toPath().toUri()}") 43 | } 44 | 45 | /** 46 | * 将 List 以 Json 文件输出 47 | */ 48 | fun List.writeToJson(path: String) { 49 | val jsonFile = File(path) 50 | if (jsonFile.exists()) { 51 | jsonFile.delete() 52 | } 53 | jsonFile.createNewFile() 54 | val json = JsonOutput.toJson(this) 55 | jsonFile.writeText(JsonOutput.prettyPrint(json), Charsets.UTF_8) 56 | println("Reporter: ${jsonFile.toPath().toUri()}") 57 | } 58 | 59 | /** 60 | * 将 List 以 Json 文件输出 61 | */ 62 | fun JSONObject.writeToJson(path: String) { 63 | val jsonFile = File(path) 64 | if (jsonFile.exists()) { 65 | jsonFile.delete() 66 | } 67 | jsonFile.createNewFile() 68 | jsonFile.writeText(JsonOutput.prettyPrint(this.toString()), Charsets.UTF_8) 69 | println("Reporter: ${jsonFile.toPath().toUri()}") 70 | } 71 | 72 | /** 73 | * 将 Json string text 写入 json 文件 74 | */ 75 | internal fun String.writeToJson(filePath: String) { 76 | val jsonFile = File(filePath) 77 | if (jsonFile.exists()) { 78 | jsonFile.delete() 79 | } 80 | jsonFile.createNewFile() 81 | jsonFile.writeText(JsonOutput.prettyPrint(this), Charsets.UTF_8) 82 | println("Reporter: ${jsonFile.toPath().toUri()}") 83 | } 84 | 85 | /** 86 | * 将对象以 Json 文件输出 87 | */ 88 | internal fun Any.writeToJson(path: String) { 89 | File(path).apply { 90 | if (exists()) { 91 | delete() 92 | } 93 | createNewFile() 94 | val json = JsonOutput.toJson(this@writeToJson) 95 | writeText(JsonOutput.prettyPrint(json), Charsets.UTF_8) 96 | println("Reporter: ${toPath().toUri()}") 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/VariantExt.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | import com.android.build.api.variant.ApplicationVariant 4 | import com.android.build.api.variant.Variant 5 | import com.android.build.gradle.BaseExtension 6 | import org.gradle.api.Project 7 | import org.gradle.api.Task 8 | import org.gradle.api.UnknownTaskException 9 | import org.gradle.api.tasks.TaskProvider 10 | import java.util.* 11 | import kotlin.reflect.full.declaredMemberProperties 12 | import kotlin.reflect.jvm.isAccessible 13 | 14 | /** 15 | * Author: Omooo 16 | * Date: 2023/3/8 17 | * Desc: [Variant] 相关扩张函数 18 | */ 19 | 20 | internal val Variant.project: Project 21 | get() { 22 | return this.variantImpl.variantDependencies.javaClass.kotlin.declaredMemberProperties.first { 23 | it.name == "project" 24 | }.apply { 25 | isAccessible = true 26 | }.get(this.variantImpl.variantDependencies) as Project 27 | } 28 | 29 | internal fun Variant.nameCapitalize(): String { 30 | return name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } 31 | } 32 | 33 | internal val Variant.versionName: String 34 | get() { 35 | return (this as? ApplicationVariant)?.outputs?.first()?.versionName?.get() ?: "-" 36 | } 37 | 38 | /** 39 | * 处理资源任务 40 | * 41 | * @from LinkApplicationAndroidResourcesTask 42 | */ 43 | internal val Variant.processResTaskProvider: TaskProvider? 44 | get() = try { 45 | project.tasks.named(getTaskName("process", "Resources")) 46 | } catch (_: UnknownTaskException) { 47 | println(red("processResourcesTaskProvider not found.")) 48 | null 49 | } 50 | 51 | /** 52 | * 优化资源任务 53 | * 54 | * @from OptimizeResourcesTask 55 | */ 56 | internal val Variant.optimizeResourcesTaskProvider: TaskProvider? 57 | get() = try { 58 | project.tasks.named(getTaskName("optimize", "Resources")) 59 | } catch (_: UnknownTaskException) { 60 | println(red("optimizeResourcesTaskProvider not found.")) 61 | null 62 | } 63 | 64 | internal val Variant.processManifestTaskProvider: TaskProvider? 65 | get() = try { 66 | this.variantImpl.taskContainer.processManifestTask 67 | } catch (e: Exception) { 68 | println(red("processManifestTaskProvider not found, e: ${e.message}")) 69 | null 70 | } 71 | 72 | internal fun Variant.getTaskName(prefix: String, suffix: String = ""): String { 73 | return variantImpl.computeTaskName(prefix, suffix) 74 | } 75 | 76 | internal inline fun Project.getAndroid(): T = extensions.getByName("android") as T -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/WebpToolUtil.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | import com.omooo.plugin.bean.WebpToolBean 4 | 5 | /** 6 | * Created by Omooo 7 | * Date: 2020-02-13 8 | * Desc: 9 | */ 10 | class WebpToolUtil { 11 | 12 | companion object { 13 | 14 | /** 15 | * 执行 cwebp 命令压缩图片 16 | */ 17 | fun cmd(cmd: String, params: String) { 18 | val cmdStr = when { 19 | isWindows() -> 20 | "${WebpToolBean.getToolsDirPath()}/windows/$cmd $params" 21 | isMac() -> 22 | "${WebpToolBean.getToolsDirPath()}/mac/$cmd $params" 23 | isLinux() -> 24 | "${WebpToolBean.getToolsDirPath()}/linux/$cmd $params" 25 | else -> "" 26 | } 27 | if (cmd == "") { 28 | println("Cwebp can't support this system.") 29 | return 30 | } 31 | Runtime.getRuntime().exec(cmdStr).waitFor() 32 | } 33 | 34 | private fun isLinux(): Boolean { 35 | return System.getProperty("os.name").startsWith("Linux") 36 | } 37 | 38 | private fun isMac(): Boolean { 39 | return System.getProperty("os.name").startsWith("Mac OS") 40 | } 41 | 42 | private fun isWindows(): Boolean { 43 | return System.getProperty("os.name").contains("win", true) 44 | } 45 | 46 | } 47 | } -------------------------------------------------------------------------------- /plugin/src/main/kotlin/com/omooo/plugin/util/XmlParseExt.kt: -------------------------------------------------------------------------------- 1 | package com.omooo.plugin.util 2 | 3 | import org.w3c.dom.Node 4 | import org.w3c.dom.NodeList 5 | 6 | /** 7 | * Author: Omooo 8 | * Date: 2022/12/20 9 | * Desc: Xml 解析相关扩展函数 10 | */ 11 | 12 | /** 13 | * NodeList 转 List 14 | */ 15 | internal fun NodeList.toList(): List { 16 | val list = mutableListOf() 17 | for (i in 0 until this.length) { 18 | list.add(item(i)) 19 | } 20 | return list 21 | } 22 | 23 | /** 24 | * Node 属性转 Map 25 | */ 26 | internal fun Node.attributeMap(): Map { 27 | val map = mutableMapOf() 28 | for (i in 0 until this.attributes.length) { 29 | map[attributes.item(i).nodeName] = attributes.item(i).nodeValue 30 | } 31 | return map 32 | } -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/gradle-plugins/com.omooo.lavender.properties: -------------------------------------------------------------------------------- 1 | implementation-class=com.omooo.plugin.Lavender -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':plugin', ':annotation' 2 | include ':library' 3 | include ':frontend' 4 | -------------------------------------------------------------------------------- /tools/cwebp/linux/cwebp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/tools/cwebp/linux/cwebp -------------------------------------------------------------------------------- /tools/cwebp/mac/cwebp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/tools/cwebp/mac/cwebp -------------------------------------------------------------------------------- /tools/cwebp/windows/cwebp.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Omooo/Lavender/0a300ad745f7cb6fa6ee9094dc1e0ffdaaa7db72/tools/cwebp/windows/cwebp.exe -------------------------------------------------------------------------------- /wiki/包体积优化/APK 增量分析.md: -------------------------------------------------------------------------------- 1 | --- 2 | APK 增量分析 3 | --- 4 | 5 | | 功能 | 使用 | 输出 | 主要实现类 | 6 | | ------------ | -------------------- | -------------------------------- | -------------- | 7 | | APK 增量分析 | ./gradlew apkAnalyse | *{projectDir}/*apkAnalyse*.*html | ApkAnalyseTask | 8 | 9 | #### 一、背景 10 | 11 | APK 每个版本都有近 10M 的增量,但是无法知道这些增量来源于哪里?不利于 App 包体积良性增长。 12 | 13 | 如果在某个业务中引用了较大的资源文件,如何能够及时发现呢? 14 | 15 | 那么,这正是这个任务所要解决的问题。 16 | 17 | #### 二、如何使用 18 | 19 | 在接入 Lavender 的 Application 工程中,直接运行: 20 | 21 | ``` 22 | ./gradlew apkAnalyse 23 | ``` 24 | 25 | 该任务会在终端输出: 26 | 27 | ```kotlin 28 | > Task :app-startup:apkAnalyseForDevRelease 29 | ********************************************* 30 | *********** -- ApkAnalyseTask -- ************ 31 | ***** -- projectDir/apkAnalyse.json -- ****** 32 | ********************************************* 33 | Reporter: file:///Users/xxx/projectRootDir/apkAnalyse.json 34 | Reporter: file:///Users/xxx/projectRootDir/apkAnalyse.html 35 | Spend time: 32990ms 36 | ``` 37 | 38 | 同时会在项目的根目录输出 apkAnalyse.html 报告。 39 | 40 | #### 三、实现原理 41 | 42 | ##### 1. 解析 APK 43 | 44 | 这一步主要的目标是,解析 Apk 生成文件列表。它涉及解析依赖、解 dex 文件、解混淆。 45 | 46 | 其中比较麻烦的是资源的混淆问题。 47 | 48 | 不同于类的反混淆,资源混淆是没有默认生成的 mapping 文件的。那如何去解决这个问题呢? 49 | 50 | 我们首先想到的是使用 md5 对比 resources-release.ap_ 和 resources-release-optimize.ap_ 文件,这可以解决绝大部分的资源混淆问题。因为 [OptimizeResourcesTask](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/tasks/OptimizeResourcesTask.kt) 默认只做 "--shorten-resource-paths" 处理,即缩短资源路径,不会对文件内容处理,所以可以通过对比混淆前后资源文件的 md5 值,得到混淆前后资源名的映射。但有一种情况例外,即资源未被使用,在 ShrinkResourcesTask 时被重写成了空文件。 51 | 52 | 这种情况下,就无法使用 md5 对比了。那还有什么办法呢? 53 | 54 | 其实 AAPT2 提供了生成资源 mapping 文件的命令行参数,见:[AAPT2 - 优化选项](https://developer.android.com/studio/command-line/aapt2?hl=zh-cn#optimize_options),但是该参数,没法通过 gradle.properties 或 aaptOptions 来指定,所以我们最终解决方案就是,使用 AAPT2 对 resources-release.ap_ 文件进行再处理一次,获取到 mapping 文件即可。混淆规则可见:[ResourcePathShortener](https://android.googlesource.com/platform/frameworks/base/+/master/tools/aapt2/optimize/ResourcePathShortener.cpp)。 55 | 56 | ##### 2. 增量分析 57 | 58 | 增量分析这一步的目标是,对比上一个版本的文件列表,输出差异。那上一个版本的文件列表是如何存储的呢? 59 | 60 | 其实是存储在了 {projectDir}/lavender-plugin/apk/previous.json 下,在第一次运行该任务时,就会把当前版本的 Apk 文件列表存储至此。 61 | 62 | ##### 3. 可视化 63 | 64 | 使用一个独立的 Kotlin JS 工程,来渲染 json 生成 html。 65 | 66 | 不过在 Lavender 中,是直接内置了模版 html。 -------------------------------------------------------------------------------- /wiki/包体积优化/删除无用 Assets 资源.md: -------------------------------------------------------------------------------- 1 | --- 2 | 删除无用 Assets 资源 3 | --- 4 | 5 | -------------------------------------------------------------------------------- /wiki/包体积优化/无用 Assets 资源检测.md: -------------------------------------------------------------------------------- 1 | --- 2 | 无用 Assets 资源检测 3 | --- 4 | 5 | 无用 Assets 资源检测 6 | 7 | | 功能 | 使用 | 输出 | 主要实现类 | 8 | | -------------------- | -------------------------- | ---------------------------- | -------------------- | 9 | | 无用 Assets 资源检测 | ./gradlew listUnusedAssets | projectDir/unusedAssets.json | ListUnusedAssetsTask | 10 | 11 | #### 一、如何使用 12 | 13 | 在接入 Lavender 的 Application 工程中,直接运行: 14 | 15 | ```tex 16 | ./gradlew listUnusedAssets 17 | ``` 18 | 19 | 该任务会输出: 20 | 21 | ```tex 22 | > Task :app-startup:listUnusedAssetsForNioRelease 23 | ********************************************* 24 | ********* -- ListUnusedAssetsTask -- ******** 25 | ***** -- projectDir/unusedAssets.json -- **** 26 | ********************************************* 27 | Total reduce size: 14251141 bytes. 28 | Reporter: file:///xxx/nio/rootProjectDir/unusedAssets.json 29 | ``` 30 | 31 | 并且会在项目的根目录输出 unusedAssets.json 报告,示例如下: 32 | 33 | ``` 34 | { 35 | "xxx:xx:5.12.1": [ // AAR 名称 36 | "mockhome.json" // 该 AAR 下的无用 assets 资源 37 | ], 38 | "xxx:xx:2.6.5": [ 39 | "checkbox_ok.json" 40 | ], 41 | // ... 42 | } 43 | ``` 44 | 45 | 1. ##### 白名单配置 46 | 47 | 如果想配置白名单,则可以在项目的根目录下新增 {projectDir}/lavender-plugin/whitelist/assets.json,例如: 48 | 49 | ``` 50 | [ 51 | "lottie/xxx.json", // assets 资源名称 52 | "rule_action_layout.json", 53 | // ... 54 | ] 55 | ``` 56 | 57 | 1. ##### 资源归属 58 | 59 | 如何归属 AAR 是属于谁负责呢,可以同样在项目的根目录下 {projectDir}/lavender-plugin/ownership.yaml,例如: 60 | 61 | ```YAML 62 | omooo@yourcompany.com: 63 | - aar-name-sdk 64 | - xx-carinspect-sdk 65 | - xx-carwash-sdk 66 | - xx-common-lib 67 | 68 | xxx@yourcompany.com: 69 | - xxx-shadow-secret 70 | ``` 71 | 72 | #### 二、实现原理 73 | 74 | 实现原理类似于 [Matrix#UnusedAssetsTask](https://github.com/Tencent/matrix/blob/master/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnusedAssetsTask.java),该方案的是:搜索 smali 文件中引用字符串常量的指令,判断引用的字符串常量是否某个 assets 文件的名称。 75 | 76 | 而我们的做法是: 77 | 78 | 1. 收集所有的 assets 资源名称 79 | 2. 在 assembleRelease 后解析 resources.txt 文件,匹配出所有的引用字符串 80 | 3. 如果引用字符串中未包含的 assets 资源名称,即判定为未使用的 assets 资源 81 | 82 | 所以 该任务是依赖于 assembleReleaseTask。 83 | 84 | 其实思路都是来源于 AGP 的 [ResourceUsageAnalyzer.java](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/ResourceUsageAnalyzer.java) 实现。 -------------------------------------------------------------------------------- /wiki/包体积优化/无用资源监测.md: -------------------------------------------------------------------------------- 1 | --- 2 | 无用资源监测 3 | --- 4 | 5 | 无用资源监测 6 | 7 | | 功能 | 使用 | 输出 | 主要实现类 | 8 | | ------------ | ----------------------- | ------------------------- | ----------------- | 9 | | 无用资源监测 | ./gradlew listUnusedRes | projectDir/unusedRes.json | ListUnusedResTask | 10 | 11 | #### 一、背景及收益 12 | 13 | 优化包体积大小的一个重要手段,就是移除无用的资源。移除无用资源可以给我们带来两方面的收益: 14 | 15 | 1. 减少 res 和 resources.asrc 的大小,预计可减少总计 6.6M 的大小 16 | 2. 减少打包耗时(暂未衡量出来具体减少时间) 17 | 18 | #### 二、如何使用 19 | 20 | 在接入 Lavender 的 Application 工程中,直接运行: 21 | 22 | ``` 23 | ./gradlew listUnusedRes 24 | ``` 25 | 26 | 该任务会在输出: 27 | 28 | ``` 29 | > Task :app-startup:listUnusedResForDevRelease 30 | ********************************************* 31 | ********** -- ListUnusedResTask -- ********** 32 | ****** -- projectDir/unusedRes.json -- ****** 33 | ********************************************* 34 | Total unused resources count: 3185 35 | ``` 36 | 37 | 该 Task 会在项目的根目录输出一个 unusedRes.json 文件,类似以下: 38 | 39 | ``` 40 | { 41 | "app-startup": [ // AAR 名称 42 | "res/raw/keep.xml" // 该 AAR 下的无用资源 43 | ], 44 | "appcompat-1.3.1": [ 45 | "res/anim/abc_fade_in.xml", 46 | "res/anim/abc_fade_out.xml", 47 | // ... 48 | ], 49 | // ... 50 | } 51 | ``` 52 | 53 | 以上输出是在 shrinkResources 并未失效的情况下的输出,而当前我们工程 shrinkResources 失效了,则会输出: 54 | 55 | ``` 56 | > Task :app-startup:listUnusedResForDevRelease 57 | ********************************************* 58 | ********** -- ListUnusedResTask -- ********** 59 | ****** -- projectDir/unusedRes.json -- ****** 60 | ********************************************* 61 | Unused resource is empty. 62 | May be resource shrinking did not work. 63 | Please try use './gradlew listUnusedRes -PstrictMode' instead. 64 | ``` 65 | 66 | 所以请使用: 67 | 68 | ``` 69 | ./gradlew listUnusedRes -PstrictMode 70 | ``` 71 | 72 | #### 三、实现原理 73 | 74 | 该 Task 依赖于 assembleRelease,并且是一定会在 assembleRelease 执行完成之后执行。 75 | 76 | 这时候就可以拿到 buildDir/outputs/mapping 下的 resources.txt 文件,通过正则匹配即可得到以下内容: 77 | 78 | ``` 79 | Skipped unused resource res/xml/preference_nfc.xml: 1580 bytes (replaced with small dummy file of size 104 bytes) 80 | Skipped unused resource res/xml/preference_nfc_debug.xml: 1908 bytes (replaced with small dummy file of size 104 bytes) 81 | // ... 82 | ``` 83 | 84 | 其中 res/xml/preference_nfc.xml、res/xml/preference_nfc_debug.xml 就是我们要找的无用资源名称。 85 | 86 | 那怎么知道该无用资源是来源于哪个 AAR 呢? 87 | 88 | 这个比较好办,通过 'allRawAndroidResources' 即可获取到所有资源及其所属的 AAR 名称。 89 | 90 | 如果 shrinkResources 失效了,我们就需要在 Application 工程里 [启用严格引用检查](https://developer.android.com/studio/build/shrink-code?hl=zh-cn#strict-reference-checks),即在 app-startup 工程里的 res/raw下新增 keep.xml,内容如下: 91 | 92 | ```xml 93 | 94 | 96 | ``` 97 | 98 | 但是,如何简化使用呢?这就需要在打包阶段,插件会自动新增一个 keep.xml。 99 | 100 | 实现原理可见 ToolsAttributeUsageRecorder 源码: 101 | 102 | ![](https://s2.loli.net/2023/07/07/uHb2GaT56SfAXFo.png) 103 | 104 | 也就是在 shrinkResources 任务之前,在 rawResourcesPath 里写一个 keep.xml,这里我命名为了 lavender-keep-{System.currentTimeMillis()}.xml 的文件。 105 | 106 | #### 四、遗留问题 107 | 108 | 1. 如何在子模块自动删除这些无用资源? 109 | 2. 如何找出 getIdentifier 所引用的资源,以避免误删导致线上问题? -------------------------------------------------------------------------------- /wiki/包体积优化/输出 App 依赖 AAR 下的 assets 资源.md: -------------------------------------------------------------------------------- 1 | --- 2 | 输出 App 依赖 AAR 下的 assets 资源 3 | --- 4 | 5 | | 功能 | 使用 | 输出 | 主要实现类 | 6 | | ---------------------- | -------------------- | ---------------------- | -------------- | 7 | | 列出所有的 assets 资源 | ./gradlew listAssets | projectDir/assets.json | ListAssetsTask | 8 | 9 | #### 一、如何使用 10 | 11 | 在接入 Lavender 的 Application 工程中,直接运行: 12 | 13 | ``` 14 | ./gradlew listAssets 15 | ``` 16 | 17 | 该任务会在终端输出: 18 | 19 | ``` 20 | > Task :app-startup:listAssets 21 | ********************************************* 22 | ********** -- ListAssetsTask -- ************* 23 | ******* -- projectDir/assets.json -- ******** 24 | ********************************************* 25 | Total assets size: 15839513 bytes. 26 | ``` 27 | 28 | 同时会在项目的根目录输出 assets.json 报告,示例如下: 29 | 30 | ``` 31 | { 32 | "xxx:xxx:5.13.1-SNAPSHOT": [ // AAR 名称 33 | { 34 | "fileName": "/.../assets/mockhome.json", // assets 资源名 35 | "size": 18909 // assets 大小,单位字节 36 | }, 37 | // ... 38 | ], 39 | // ... 40 | } 41 | ``` -------------------------------------------------------------------------------- /wiki/包体积优化/输出 App 依赖的 AAR 大小.md: -------------------------------------------------------------------------------- 1 | --- 2 | 输出 App 依赖的 AAR 大小 3 | --- 4 | 5 | | 功能 | 使用 | 输出 | 主要实现类 | 6 | | ------------- | --------------------- | ----------------------- | --------------- | 7 | | 输出 AAR 大小 | ./gradlew listAarSize | projectDir/aarSize.json | ListAarSizeTask | 8 | 9 | #### 一、如何使用 10 | 11 | 在接入 Lavender 的 Application 工程中,直接运行: 12 | 13 | ``` 14 | // 这样会输出所有的 AAR 15 | ./gradlew listAarSize 16 | 17 | // 当然你也可以过滤指定 groupId 前缀的 AAR,即: 18 | ./gradlew listAarSize -PgroupIdPrefix="com.androidx.core" 19 | ``` 20 | 21 | 该任务会在项目的 根目录输出一个 aarSize.json 文件(默认按照 AAR 大小倒序排序),类似如下: 22 | 23 | ``` 24 | { 25 | // key: AAR 名称;value: AAR 大小,单位 kb 26 | "com.xxx.1:5.11.0-SNAPSHOT": "24524kb", 27 | "com.xxx.2:1.0.14": "23769kb", 28 | "com.xxx.3:8.7.10102": "22101kb", 29 | // ... 30 | } 31 | ``` -------------------------------------------------------------------------------- /wiki/包体积优化/输出图片列表.md: -------------------------------------------------------------------------------- 1 | --- 2 | 输出图片列表 3 | --- 4 | 5 | | 功能 | 使用 | 输出 | 主要实现类 | 6 | | ------------ | ------------------- | ------------------------- | ------------- | 7 | | 输出图片列表 | ./gradlew listImage | projectDir/imageList.json | ListImageTask | 8 | 9 | #### 一、如何使用 10 | 11 | 在接入 Lavender 的 Application 工程中,直接运行: 12 | 13 | ``` 14 | ./gradlew listImage 15 | ``` 16 | 17 | 该任务会在项目的根目录生成 repeatRes.json 和 imageList.html 报告,repeatRes.json 报告类似如下: 18 | 19 | ```JSON 20 | { 21 | "aarList": [ 22 | { 23 | "owner": "xxx@yourcompany.com", 24 | "fileList": [ 25 | { 26 | "fileType": "OTHER", 27 | "size": 159522, 28 | "name": "picture_empty_shopping.webp", 29 | "desc": "" 30 | }, 31 | { 32 | "fileType": "OTHER", 33 | "size": 132354, 34 | "name": "picture_empty_community.webp", 35 | "desc": "" 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | ``` 42 | 43 | #### 二、实现原理 44 | 45 | 遍历所有资源文件,过滤出图片类型,以文件大小倒序输出。 -------------------------------------------------------------------------------- /wiki/包体积优化/重复资源检测.md: -------------------------------------------------------------------------------- 1 | --- 2 | 重复资源检测 3 | --- 4 | 5 | 重复资源检测 6 | 7 | | 功能 | 使用 | 输出 | 主要实现类 | 8 | | ------------ | ------------------- | ------------------------- | --------------------- | 9 | | 重复资源监测 | ./gradlew repeatRes | projectDir/repeatRes.json | RepeatResDetectorTask | 10 | 11 | #### 一、如何使用 12 | 13 | 在接入 Lavender 的 Application 工程中,直接运行: 14 | 15 | ``` 16 | ./gradlew repeatRes 17 | ``` 18 | 19 | 该任务会输出: 20 | 21 | ``` 22 | > Task :app-startup:repeatRes 23 | ********************************************* 24 | ******** -- RepeatResDetectorTask -- ******** 25 | ****** -- projectDir/repeatRes.json -- ****** 26 | ********************************************* 27 | Repeat Res count: 224, total size: 449kb 28 | ``` 29 | 30 | 并在项目的根目录生成 repeatRes.json 报告,类似如下: 31 | 32 | ``` 33 | { 34 | "8963235980624699639": [ // 文件的 MD5 值 35 | "/Users/xxx/.gradle/caches/transforms-3/4b9bdd49d27b23cdf8a854a3f0d55341/transformed/jetified-exoplayer-ui-2.14.0/res/drawable-xhdpi-v4/exo_ic_fullscreen_enter.png", 36 | "/Users/xxx/.gradle/caches/transforms-3/4b9bdd49d27b23cdf8a854a3f0d55341/transformed/jetified-exoplayer-ui-2.14.0/res/drawable-mdpi-v4/exo_icon_fullscreen_enter.png", 37 | "/Users/xxx/.gradle/caches/transforms-3/85cb133791149d8edbeafdb04f19ac2a/transformed/jetified-sdk-commonwidget-1.180.0/res/drawable-xhdpi-v4/quantum_ic_fullscreen_white_24.png" 38 | ], 39 | // ... 40 | } 41 | ``` 42 | 43 | #### 二、实现原理 44 | 45 | 实现原理相对简单,遍历所有的资源文件,以文件的 MD5 值作为是否是重复资源的判断依据。 -------------------------------------------------------------------------------- /wiki/静态分析/exported 属性检测.md: -------------------------------------------------------------------------------- 1 | --- 2 | exported 属性检测 3 | --- 4 | 5 | | 功能 | 使用 | 输出 | 主要实现类 | 6 | | ---------------------------------------------------- | ------------------------------------ | ----------------------------- | ----------------- | 7 | | 检测 Manifest 注册组件是否声明 android:exported 属性 | ./gradlew check{variantName}Exported | projectDir/checkExported.json | CheckExportedTask | 8 | 9 | #### 一、背景 10 | 11 | 进行了 Android 12 的适配时,有一条规则是: 12 | 13 | > 在 Android 12 中包含 \ 的 activity、 service 或 receiver 必须为这些应用组件显示声明 android:exported 属性,否则 App 将无法安装。 14 | 15 | 所以,需要我们找出哪些模块,它的 Manifest 中声明的组件包含了 intent-filter 但未声明 exported 属性。 16 | 17 | #### 二、如何使用 18 | 19 | 引入了 Lavender 插件的工程,可以直接运行: 20 | 21 | ``` 22 | ./gradlew checkExported 23 | ``` 24 | 25 | 该 Task 会在项目的根目录输出一个 checkExported.json 文件,类似以下: 26 | 27 | ``` 28 | { 29 | "xxx:comweb-sdk:5.11.0": [ // AAR 名称 30 | "xxx.comweb.CommWebViewActivity" // 声明的组件名称 31 | ], 32 | "app-startup": [ 33 | "xxx.ui.activity.SplashActivity" 34 | ], 35 | // ... 36 | } 37 | ``` 38 | 39 | #### 三、实现原理 40 | 41 | 解析所有的 AndroidManifest.xml 文件,找出声明的组件包含了 intent-filter 但未声明 exported 属性的组件名称。 -------------------------------------------------------------------------------- /wiki/静态分析/scheme 变更检查.md: -------------------------------------------------------------------------------- 1 | --- 2 | exported 属性检测 3 | --- 4 | 5 | | 功能 | 使用 | 输出 | 主要实现类 | 6 | | --------------- | ----------------------------- | ----------------------- | ----------------------- | 7 | | Scheme 变更检查 | ./gradlew checkSchemeModified | projectDir/schemes.json | CheckSchemeModifiedTask | 8 | 9 | #### 一、背景 10 | 11 | 代码下线时可能会删除一些 Activity,如果这些 Activity 配置了 scheme 路由跳转,可能会导致一些线上问题。 12 | 13 | 所以需要在构建时,检查 scheme 相对于基线是否存在变更,**如果存在则触发编译失败**。 14 | 15 | #### 二、如何使用 16 | 17 | 引入了 Lavender 插件的工程,可以直接运行: 18 | 19 | ``` 20 | ./gradlew checkSchemeModified 21 | ``` 22 | 23 | #### 三、如何解决 24 | 25 | ##### 本地编译 26 | 27 | 1. 【方法一】本地编译话,可以直接新增一个命令行参数 "-PskipCheck" 跳过该检查,例如: 28 | 29 | ``` 30 | ./gradlew clean assembleDevDebug -PskipCheck 31 | ``` 32 | 33 | 2. 拿生成的 schemes.json 文件覆盖 app-startup/runtime/devRelease/runtimeSchems.json 文件 34 | 35 | schemes.json 文件位于项目根目录,当触发编译失败时,会自动生成该文件。 36 | 37 | ##### 远程编译(jenkins 打包) 38 | 39 | 这种情况下,得先根据输出的错误信息,找到对应的人,询问清楚是否 scheme 已经下线了,然后按照上述的「本地编译」的【方法二】处理,将该变更提交上去。 40 | 41 | #### 四、实现原理 42 | 43 | 实现步骤分为两步: 44 | 45 | 1. 首先在 Application 工程的 build.gradle 配置基线文件: 46 | 47 | ``` 48 | checkSchemeModifiedConfig { 49 | enable = true 50 | baselineSchemeFile = file("runtime/devRelease/runtimeSchemes.json") 51 | } 52 | ``` 53 | 54 | 这个基线文件可以通过 "./gradlew listSchemes" 任务来生成。 55 | 56 | 57 | 2. 对比基线文件,如果发现发生变更则会输出以下提示信息,并且触发 Task 执行失败 58 | 59 | ``` 60 | > Task :app:checkSchemeModifiedForDebug FAILED 61 | ********************************************* 62 | ****** -- CheckSchemeModifiedTask -- ******** 63 | ********************************************* 64 | Reporter: file:///Users/x xx/AndroidStudioProjects/Lavender/schemes.json 65 | 66 | FAILURE: Build failed with an exception. 67 | 68 | * What went wrong: 69 | Execution failed for task ':app:checkSchemeModifiedForDebug'. 70 | > ---------------------------------- Lavender - Check Scheme Modified ---------------------------------- 71 | 发现下列 scheme 定义存在变更: 72 | --- 73 | Class: app.ui.activity.DemoActivity 74 | Owner: owner@demo.com 75 | SchemeList: [scheme://host/path] 76 | --- 77 | 如何解决,请参考文档: 78 | 本地编译跳过该任务检查: 增加 "skipCheck" 参数即可,例如: ./gradlew clean assembleDebug -PskipCheck 79 | ------------------------------------------------------------------------------------------------------ 80 | ``` 81 | 82 | 一旦该任务执行失败,就会在项目的根目录(schemes.json)输出一份当前版本的 scheme 列表,你可以拿该文件的内容覆盖基线文件。 -------------------------------------------------------------------------------- /wiki/静态分析/依赖权限检测.md: -------------------------------------------------------------------------------- 1 | --- 2 | 依赖权限检测 3 | --- 4 | 5 | | 功能 | 使用 | 输出 | 主要实现类 | 6 | | ---------------------------- | ------------------------- | --------------------------- | ------------------ | 7 | | 输出 App 及其依赖的 AAR 权限 | ./gradlew listPermissions | projectDir/permissions.json | ListPermissionTask | 8 | 9 | #### 一、如何使用 10 | 11 | 在接入 Lavender 的 Application 工程中,直接运行: 12 | 13 | ``` 14 | ./gradlew listPermissions 15 | ``` 16 | 17 | 该任务会在项目的 根目录输出一个 permissions.json 文件,类似如下: 18 | 19 | ``` 20 | { 21 | "xxxx:gallery-sdk:5.12.0": [ 22 | "", 23 | "", 24 | "" 25 | ], 26 | "xxx:tiled-widget:2.10.0-SNAPSHOT": [ 27 | "" 28 | ], 29 | // ... 30 | } 31 | ``` 32 | 33 | #### 二、实现原理 34 | 35 | 解析所有的 AndroidManifest.xml 文件,通过正则表达式匹配出权限信息。 --------------------------------------------------------------------------------