├── .gitignore ├── .idea ├── Zucker.iml ├── encodings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── CONTRIBUTING.rst ├── ChangeLog.txt ├── LICENSE ├── README.md ├── README_EN.md ├── Sample ├── .gitignore ├── .idea │ ├── codeStyles │ │ └── Project.xml │ ├── gradle.xml │ ├── inspectionProfiles │ │ └── Project_Default.xml │ └── runConfigurations.xml ├── README.md ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── wuba │ │ │ └── zucker │ │ │ └── sample │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── wuba │ │ │ │ └── zucker │ │ │ │ └── sample │ │ │ │ └── MainActivity.java │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ └── 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 │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── wuba │ │ └── zucker │ │ └── sample │ │ └── ExampleUnitTest.java ├── app2 │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── wuba │ │ │ └── zucker │ │ │ └── app2 │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── wuba │ │ │ │ └── zucker │ │ │ │ └── app2 │ │ │ │ └── MainActivity.java │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ └── 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 │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── wuba │ │ └── zucker │ │ └── app2 │ │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── imgs │ ├── s1.png │ ├── s2.png │ ├── s3.png │ └── s4.png ├── module1 │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── wuba │ │ │ └── zucker │ │ │ └── module1 │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── wuba │ │ └── zucker │ │ └── module1 │ │ └── ExampleUnitTest.java ├── module2 │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── wuba │ │ │ └── zucker │ │ │ └── module2 │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── wuba │ │ └── zucker │ │ └── module2 │ │ └── ExampleUnitTest.java └── settings.gradle ├── docs ├── Python使用规范.md ├── imgs │ ├── analysis.png │ ├── app-size.png │ ├── clone.png │ ├── node-flow.png │ ├── node-tree.png │ ├── node.png │ ├── reflect.png │ ├── zucker-arc.png │ └── zucker_contact.png └── 介绍文档.md ├── imgs ├── sample_aar.png ├── sample_aar_size.png └── sample_clone.png └── zucker.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | encodings.txt 3 | misc.xml 4 | modules.xml 5 | vcs.xml 6 | Zucker.iml 7 | /.idea/ 8 | -------------------------------------------------------------------------------- /.idea/Zucker.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | 2 | 【Zucker】 3 | =========================== 4 | 5 | 6 | How to contribute to Zucker 7 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 8 | 9 | Thank you for considering contributing to Zucker ! 10 | 11 | 12 | Start coding 13 | ^^^^^^^^^^^^^ 14 | 15 | * Create a branch to identify the issue you would like to work on. If you're submitting a bug or documentation fix, branch off of the latest ".x" branch 16 | 17 | :: 18 | 19 | $ git checkout -b your-branch-name origin/remote-branch-name 20 | 21 | * Add your code and push it to the remote branch **'origin/zucker-dev'** 22 | 23 | :: 24 | 25 | $ git push origin zucker-dev 26 | 27 | * Comment your change and issue 28 | 29 | A Simple Example 30 | ^^^^^^^^^^^^^^^^ 31 | The python script usage as follows. 32 | :: 33 | 34 | $ python3 xxx/xxx/zucker.py xxx/xxx/targetProject 35 | 36 | -------------------------------------------------------------------------------- /ChangeLog.txt: -------------------------------------------------------------------------------- 1 | # zucker.py changelog 2 | 3 | ## 2019-12-31 0.1 4 | 5 | * First public version. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (C) 2005-present, 58.com. All rights reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | License for the specific language governing permissions and limitations 14 | under the License. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android APP模块化大小自动分析统计工具-Zucker 2 | 3 | 基于APP模块的,一个简单无侵害计算AAR独有大小的工具(An easier way to automatically calculate the size of AAR in apk based on APP module) 4 | 5 | [介绍文档](/docs/介绍文档.md) 6 | 7 | [英文文档](README_EN.md) 8 | 9 | > AAR独立大小和被引入到工程后打包后占用的大小是不一样的,这个有经验的开发者都应该了解。AAR独立大小一目了然,但是怎么计算AAR在APK中的独立占有大小(独有大小)呢?Zucker就此开源给出了一份答案。 10 | 11 | ## 依赖环境 12 | - Python 3.0+ 13 | - Android编译环境 14 | 15 | ## 开始使用 16 | ### Demo工程测试 17 | 1. 克隆本工程 18 | 2. 终端cd到本工程下的src目录 19 | 3. 执行python脚本:python3 zucker.py Sample 20 | 4. 根据终端列出的AAR列表,选择一个目标AAR输入得到结果 21 | 22 | ### 项目工程测试 23 | 1. 将zucker.py脚本放置在需要测试工程的同级目录 24 | 2. 同[Demo工程测试]步骤2 25 | 3. 同[Demo工程测试]步骤3:python3 zucker.py [targetProjectName](Android工程名) 26 | 4. 同[Demo工程测试]步骤4 27 | 28 | ### 注意事项 29 | 1. 确保目标工程在不依赖Zucker脚本的前提下可以正常编译 30 | 2. 编译时使用`gradlew`命令,以保证采用了项目的`gradle`配置 31 | 3. 首次运行时间较长,请耐心等待... 32 | 33 | ``` 34 | ./gradlew build 35 | 36 | ``` 37 | ``` 38 | -> 〜python3 /Users/iann/EnterpriseProject/Zucker/src/zucker.py /Users/iann/EnterpriseProject/Zucker/Sample 39 | cloneBaseProject DONE 40 | findBaseAppDirs DONE 41 | clearBaseFlavors DONE 42 | insertBaseScript DONE 43 | 44 | > Configure project :app 45 | WARNING: API 'variantOutput.getPackageApplication()' is obsolete and has been re placed with 'variant.getPackageApplicationProvider()'. 46 | It will be removed at the end of 2019. 47 | For more information, see https://d.android.com/r/tools/task-configuration-avoid ance. 48 | To determine what is calling variantOutput.getPackageApplication(), use -Pandroi d.debug.obsoleteApi=true on the command line to display more information. 49 | 50 | BUILD SUCCESSFUL in 2s 51 | 5 actionable tasks: 4 executed, 1 up-to-date 52 | ``` 53 | 54 | 脚本会自动执行,获取项目中的依赖关系并输出一级节点,可以选择目标节点进行AAR大小计算。 55 | 56 | ``` 57 | ['app', 'app2'] 58 | com.github.moduth:blockcanary-android:1.2.1 59 | com.squareup.okhttp3:okhttp:4.2.1 60 | com.airbnb.android:lottie:2.5.6 61 | 输入AAR名称及版本,格式xxx.xxx:xxx:xxx:com.github.moduth:blockcanary-android:1.2.1 62 | 输出AAR------ 63 | ['com.github.moduth:blockcanary-android:1.2.1', 'com.github.moduth:blockcanary-core:1.2.1'] 64 | /Users/iann/.gradle/caches/modules-2/files-2.1/com.github.moduth/blockcanary-android/1.2.l/78f65b7622338d512e79a26fe76e7bb9f7614190/blockcanary-android-l.2.1.aar 65 | /Users/iann/.gradle/caches/modules-2/files-2.1/com.github.moduth/blockcanary-android/1.2.l/78f65b7622338d512e79a26fe76e7bb9f7614190/blockcanary-android-l.2.1.aar 66 | 替换 build.gradle 67 | ``` 68 | 69 | 最后经过打包后,AAR大小就会显示在终端上。 70 | 71 | ``` 72 | Deprecated Gradle features were used in this build, making it incompatible with Gradle 6.0. 73 | Use f--warning-mode all1 to show the individual deprecation warnings. 74 | See https://docs.gradle.org/5.4.l/userguide/command_line_interface.html#sec:comm ancLline一warnings 75 | 76 | BUILD SUCCESSFUL in 9s 77 | 137 actionable tasks: 133 executed, 4 up-to-date 78 | compile DONE 79 | basePackSize: 2908419 80 | aarPackSize: 2873610 81 | aarSize: 34809 82 | ``` 83 | 84 | > 建议先在本项目的`sample工程`进行测试,具体流程见工程[README](Sample/README.md) 85 | 86 | ## 联系我们 87 | 88 | - 想了解更多开源项目信息? 89 | - 与项目成员零距离交流? 90 | - 扫码加小秘书微信 91 | - 一切应有尽有 92 | ![jishu-58](docs/imgs/zucker_contact.png) 93 | - 微信号 : jishu-58 94 | - 添加小秘书微信后由小秘书拉您进项目交流群 95 | 96 | ## 常见问题处理 97 | - 暂不支持工程依赖类型的测量 `implementation project(':xxx')` 98 | - 在build过程中发生报错:Could not get resource 'https://jcenter.bintray.com/com/google/guava/guava/27.0.1-jre/guava-27.0.1-jre.jar' 99 | 100 | 解决办法:使用阿里云镜像,重新进行下载;修改build.gradle中的buildscript和allprojects的jcenter,添加url 'https://maven.aliyun.com/repository/jcenter'即可。 101 | 102 | ## 贡献代码 103 | 详见 [CONTRIBUTING](CONTRIBUTING.rst) 104 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # Zucker: An easier way to automatically calculate the size of AAR in apk. 2 | 3 | 4 | [中文文档](README_CN.md) 5 | [Python使用规范](./docs/Python使用规范.md) 6 | 7 | > As we all know,that when the project was introduced with AAR, it was not accurate to calculate only the aAR file size. Because aar is unzipped during packaging, the resource files are merged and then compressed, the actual apk footprint may be smaller than the current aar file size. Zucker was born to calculate the size of the target AAR at apk. 8 | 9 | 10 | ## Requirements 11 | 12 | - Python 3.7+ 13 | - Android Dev 14 | - Gradle 2.0+ 15 | - *unx/Windows 16 | 17 | ## Get Start 18 | 19 | The first time you use Zucker for AAR size calculations, it is recommended to compile once on the command line. 20 | 21 | Compile-time recommended `gradlew` command to ensure the use of the project's 'gradle' configuration 22 | Perform the following command: 23 | ``` 24 | ./gradlew build 25 | ``` 26 | Execute the following command in the terminal: 27 | ``` 28 | python3 xxx/zucker.py xxx/targetProjectName(Android project) 29 | ``` 30 | ![execute_python_command](imgs/sample_clone.png) 31 | 32 | The script is executed automatically, obtaining the dependencies in the project and outputing a level-level node, which can select the target node for AAR size calculation. 33 | 34 | ![dependency_aar_list](imgs/sample_aar.png) 35 | 36 | Finally, after packaging, the AAR size is displayed on the terminal. 37 | 38 | ![mock_aar_size](imgs/sample_aar_size.png) 39 | 40 | > It is recommended to test the project's 'sample project' first, as detailed in the project [README](Sample/README.md) 41 | 42 | ## Q&A 43 | - a dependency on local library module (implementation project(':xxx')) is not support. 44 | 45 | ## Contribute 46 | 47 | See [CONTRIBUTING](CONTRIBUTING.rst) 48 | 49 | 50 | ## Licence 51 | 52 | 53 | Copyright (C) 2005-present, 58.com. All rights reserved. 54 | 55 | Licensed under the Apache License, Version 2.0 (the "License"); you may 56 | not use this file except in compliance with the License. You may obtain 57 | a copy of the License at 58 | http://www.apache.org/licenses/LICENSE-2.0 59 | 60 | Unless required by applicable law or agreed to in writing, software 61 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 62 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 63 | License for the specific language governing permissions and limitations 64 | under the License. -------------------------------------------------------------------------------- /Sample/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /Sample/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /Sample/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | -------------------------------------------------------------------------------- /Sample/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 36 | -------------------------------------------------------------------------------- /Sample/.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /Sample/README.md: -------------------------------------------------------------------------------- 1 | # sample 2 | 3 | ## 步骤 4 | 1. 将sample工程和`zucker.py`脚本放置到空文件夹中 5 | 2. 终端cd到该目录下执行`python zucker.py Sample` 6 | 3. 看到输出一级节点,选择一个非support包和jetbean相关的AAR名称。输入规则:`xxx.xxx:xxx:xxx` 7 | 4. 等待计算完成即可 8 | 9 | 10 | ### 执行脚本 11 | ![](imgs/s1.png) 12 | 13 | ### 选择要计算的AAR 14 | ![](imgs/s3.png) 15 | 16 | ### 计算结果 17 | ![](imgs/s4.png) -------------------------------------------------------------------------------- /Sample/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /Sample/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 29 5 | buildToolsVersion "29.0.2" 6 | defaultConfig { 7 | applicationId "com.wuba.zucker.sample" 8 | minSdkVersion 21 9 | targetSdkVersion 29 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | } 14 | compileOptions { 15 | sourceCompatibility JavaVersion.VERSION_1_8 16 | targetCompatibility JavaVersion.VERSION_1_8 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: 'libs', include: ['*.jar']) 29 | implementation 'androidx.appcompat:appcompat:1.0.2' 30 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 31 | testImplementation 'junit:junit:4.12' 32 | androidTestImplementation 'androidx.test:runner:1.1.1' 33 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 34 | implementation 'com.github.moduth:blockcanary-android:1.2.1' 35 | implementation("com.squareup.okhttp3:okhttp:4.2.1") 36 | implementation 'com.airbnb.android:lottie:2.5.6' 37 | implementation project(path: ':module1') 38 | implementation project(path: ':module2') 39 | } 40 | -------------------------------------------------------------------------------- /Sample/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 | -------------------------------------------------------------------------------- /Sample/app/src/androidTest/java/com/wuba/zucker/sample/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.wuba.zucker.sample; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | 25 | assertEquals("com.wuba.zucker.sample", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sample/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sample/app/src/main/java/com/wuba/zucker/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.wuba.zucker.sample; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | 5 | import android.os.Bundle; 6 | 7 | public class MainActivity extends AppCompatActivity { 8 | 9 | @Override 10 | protected void onCreate(Bundle savedInstanceState) { 11 | super.onCreate(savedInstanceState); 12 | setContentView(R.layout.activity_main); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Sample 3 | 4 | -------------------------------------------------------------------------------- /Sample/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Sample/app/src/test/java/com/wuba/zucker/sample/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.wuba.zucker.sample; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /Sample/app2/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /Sample/app2/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 29 5 | buildToolsVersion "29.0.2" 6 | 7 | 8 | defaultConfig { 9 | applicationId "com.wuba.zucker.app2" 10 | minSdkVersion 21 11 | targetSdkVersion 29 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: 'libs', include: ['*.jar']) 29 | 30 | implementation 'androidx.appcompat:appcompat:1.0.2' 31 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 32 | testImplementation 'junit:junit:4.12' 33 | androidTestImplementation 'androidx.test:runner:1.1.1' 34 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 35 | implementation project(path: ':module2') 36 | implementation project(path: ':module1') 37 | } 38 | -------------------------------------------------------------------------------- /Sample/app2/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 | -------------------------------------------------------------------------------- /Sample/app2/src/androidTest/java/com/wuba/zucker/app2/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.wuba.zucker.app2; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | 25 | assertEquals("com.wuba.zucker.app2", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sample/app2/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sample/app2/src/main/java/com/wuba/zucker/app2/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.wuba.zucker.app2; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | 5 | import android.os.Bundle; 6 | 7 | public class MainActivity extends AppCompatActivity { 8 | 9 | @Override 10 | protected void onCreate(Bundle savedInstanceState) { 11 | super.onCreate(savedInstanceState); 12 | setContentView(R.layout.activity_main); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sample/app2/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /Sample/app2/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /Sample/app2/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /Sample/app2/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Sample/app2/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Sample/app2/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app2/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app2/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app2/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app2/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app2/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app2/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app2/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app2/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app2/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app2/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app2/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app2/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app2/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app2/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app2/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app2/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app2/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /Sample/app2/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/app2/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /Sample/app2/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /Sample/app2/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | app2 3 | 4 | -------------------------------------------------------------------------------- /Sample/app2/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Sample/app2/src/test/java/com/wuba/zucker/app2/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.wuba.zucker.app2; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /Sample/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | jcenter(){ 7 | url 'https://maven.aliyun.com/repository/jcenter' 8 | } 9 | 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.5.0' 13 | 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | jcenter(){ 23 | url 'https://maven.aliyun.com/repository/jcenter' 24 | } 25 | } 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /Sample/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=-Xmx1536m 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 | 21 | -------------------------------------------------------------------------------- /Sample/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /Sample/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jan 02 11:32:46 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /Sample/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /Sample/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 | -------------------------------------------------------------------------------- /Sample/imgs/s1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/imgs/s1.png -------------------------------------------------------------------------------- /Sample/imgs/s2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/imgs/s2.png -------------------------------------------------------------------------------- /Sample/imgs/s3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/imgs/s3.png -------------------------------------------------------------------------------- /Sample/imgs/s4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/imgs/s4.png -------------------------------------------------------------------------------- /Sample/module1/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /Sample/module1/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 29 5 | buildToolsVersion "29.0.2" 6 | 7 | 8 | defaultConfig { 9 | minSdkVersion 21 10 | targetSdkVersion 29 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles 'consumer-rules.pro' 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: 'libs', include: ['*.jar']) 29 | 30 | implementation 'androidx.appcompat:appcompat:1.0.2' 31 | testImplementation 'junit:junit:4.12' 32 | androidTestImplementation 'androidx.test:runner:1.1.1' 33 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 34 | implementation("com.squareup.okhttp3:okhttp:4.2.1") 35 | } 36 | -------------------------------------------------------------------------------- /Sample/module1/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/module1/consumer-rules.pro -------------------------------------------------------------------------------- /Sample/module1/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 | -------------------------------------------------------------------------------- /Sample/module1/src/androidTest/java/com/wuba/zucker/module1/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.wuba.zucker.module1; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | 25 | assertEquals("com.wuba.zucker.module1.test", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sample/module1/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /Sample/module1/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | module1 3 | 4 | -------------------------------------------------------------------------------- /Sample/module1/src/test/java/com/wuba/zucker/module1/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.wuba.zucker.module1; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /Sample/module2/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /Sample/module2/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 29 5 | buildToolsVersion "29.0.2" 6 | 7 | 8 | defaultConfig { 9 | minSdkVersion 21 10 | targetSdkVersion 29 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles 'consumer-rules.pro' 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: 'libs', include: ['*.jar']) 29 | 30 | implementation 'androidx.appcompat:appcompat:1.0.2' 31 | testImplementation 'junit:junit:4.12' 32 | androidTestImplementation 'androidx.test:runner:1.1.1' 33 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 34 | } 35 | -------------------------------------------------------------------------------- /Sample/module2/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/Sample/module2/consumer-rules.pro -------------------------------------------------------------------------------- /Sample/module2/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 | -------------------------------------------------------------------------------- /Sample/module2/src/androidTest/java/com/wuba/zucker/module2/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.wuba.zucker.module2; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | 25 | assertEquals("com.wuba.zucker.module2.test", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sample/module2/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /Sample/module2/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | module2 3 | 4 | -------------------------------------------------------------------------------- /Sample/module2/src/test/java/com/wuba/zucker/module2/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.wuba.zucker.module2; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /Sample/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':app2', ':module1', ':module2' 2 | rootProject.name='Sample' 3 | -------------------------------------------------------------------------------- /docs/Python使用规范.md: -------------------------------------------------------------------------------- 1 | #Python使用规范 2 | 3 | 为了更好地地迭代扎克,在合并代码前,应该进行提交代码的检查校验,其规则如下: 4 | 5 | ##编码 6 | - 统一使用 UTF-8 编码; 7 | - 导入的包在代码的最前面; 8 | - 每行代码不宜过长,不要超过120字符; 9 | - 使用面向对象思想编程,按照功能划分类,按照职责划分方法; 10 | 11 | ##关于注释 12 | - 单行文档注释可以使用#标注; 13 | - 多行可以使用三个双引号"""..."""; 14 | - 关键方法要使用注释,方便维护; 15 | 16 | ##类和方法 17 | - 对于一个类,里面有多个方法可以使用_init_()方法来初始化变量,以便该类中各个方法使用;假如接入该类只有一个方法,那么可以忽略_init()方法,直接自定义方法; 18 | - 方法之间可以不空行,类与类之间要空行; 19 | - 类的名字使用驼峰命名法,首字母大写; 20 | - 所有方法,全部使用小写; 21 | - 私有方法,使用_下划线开头; 22 | - 常量使用大写; 23 | - 每个语句执行完可以不加";",但是要注意使用换行 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/imgs/analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/docs/imgs/analysis.png -------------------------------------------------------------------------------- /docs/imgs/app-size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/docs/imgs/app-size.png -------------------------------------------------------------------------------- /docs/imgs/clone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/docs/imgs/clone.png -------------------------------------------------------------------------------- /docs/imgs/node-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/docs/imgs/node-flow.png -------------------------------------------------------------------------------- /docs/imgs/node-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/docs/imgs/node-tree.png -------------------------------------------------------------------------------- /docs/imgs/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/docs/imgs/node.png -------------------------------------------------------------------------------- /docs/imgs/reflect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/docs/imgs/reflect.png -------------------------------------------------------------------------------- /docs/imgs/zucker-arc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/docs/imgs/zucker-arc.png -------------------------------------------------------------------------------- /docs/imgs/zucker_contact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/docs/imgs/zucker_contact.png -------------------------------------------------------------------------------- /docs/介绍文档.md: -------------------------------------------------------------------------------- 1 | ## Zucker介绍 2 | 3 | ### 导语 4 | 本文介绍了在实际场景中,随着App模块功能增多包体积不断增大,如何通过脚本自动化分析模块体积大小,无侵入且快速差值计算,具有一定实践意义,希望本文的介绍对大家有所启发。 5 | 6 | ### 背景 7 | 随着业务功能迭代、包体积大小呈曲线快速上升,如何做到业务功能增加映射为包体积大小增加。在手动统计将近20个版本(如下图一:各版本大小比对),从统计各版本APK包体积大小到统计当前版本APK中某需求增加多大体积,再到自动化统计APK中各模块所占大小,经历了从手动到自动化、脚本化的完整过程。 8 | 9 | ![](imgs/app-size.png) 10 | 图一:各版本大小比对 11 | 12 | ### Zucker 13 | 正是基于这样的背景,我们隆重推出了开源工程Zucker,一个自动化无侵入计算模块化大小的工具,帮助我们深入了解每个模块在工程打包生成安装包APK中的体积大小占比,这样就可对其进行迭代监控及优化。例如,App包大小不可能是完全的增量,从功能层面监控其大小方便日后对其功能进行删减;此外,一个AAR被引入后导致APK体积增大多少,针对增大的数量级别我们可以选择是否接入或者考虑其他的实现方式。 14 | 15 | #### 核心功能 16 | Zucker模块化统计实现是通过Python脚本利用Gradle缓存将目标AAR替换为模拟AAR,结合Gradle打包命令,分别打出基础包和模拟AAR后的安装包,最后通过差值计算两个APK体积大小得出AAR大小。如下图二:Zucker统计流程: 17 | 18 | ![](imgs/zucker-arc.png) 19 | 20 | 图二 - Zucker统计流程 21 | 22 | Zucker特点: 23 | - 无侵入:无需添加或修改项目代码,只需将Zucker放置于待统计工程的目录下,运行Zucker脚本。 24 | - 自动化:自动化寻路App入口、自动化分析依赖关系以及完成打包流程、自动化输出结果。 25 | - 操作简单:脚本运行后输入统计列表中目标AAR名称,待打包完成即可得出结果。 26 | 27 | ### 具体实现 28 | #### 自动化打包统计 29 | 30 | 对于一个给定的目标工程首先避免产生侵害和改动,需要对源码工程做拷贝处理。则第一步把源码工程克隆到一个输出目录里。然后编译克隆后的工程,产出一个基础的APK并计算基础包大小。接着,脚本会根据用户指定要统计的AAR名称及版本信息,分析工程中AAR的依赖关系,自动产生一个空AAR来模拟目标AAR,并保持原工程中依赖关系。此时对模拟并替换了目标AAR后的克隆工程进行打包,计算出模拟AAR的APK大小。最后,通过打出的两个包的大小差值,可以得到目标AAR的大小。 31 | 32 | #### 克隆工程 33 | 34 | 为了实现无侵害计算,需要修改工程的gradle脚本来实现自动模拟AAR以及计算包大小,将原有工程进行克隆再分析模拟AAR及打包统计。 35 | Zucker的Python脚本文件需要放置目标工程的同级目录,运行脚本在同级目录产生一个output目录,统一存放拷贝的工程文件。 36 | 37 | ![](imgs/clone.png) 38 | 39 | 图三 - 工程及AAR克隆 40 | 41 | #### 编译过程 42 | 43 | 整个编译过程包括以下步骤:初始化工程→自动寻路查找工程入口→清除flavors→插入计算脚本→执行打包命令。通过此流程,可完整实现自动化寻路APP入口并修改配置来实现基础包打包和计算包体积大小任务。 44 | 45 | ![](imgs/analysis.png) 46 | 47 | 图四 - 编译打包流程 48 | 49 | #### 依赖分析 50 | 51 | 在上文中提到获取用户输入的AAR,分析工程中各模块依赖关系。获取AAR的相互依赖关系,对于计算AAR大小起到关键作用。首先明确两个依赖关系概念。 52 | • 独有依赖:对于某一AAR,内部引用库仅被当前AAR所依赖,再无其他依赖关系,则这个库被称为AAR的独有依赖。 53 | • 公有依赖库:不同于独有依赖,一个库可能被多个AAR所引用,则这个库被称为公有依赖。 54 | 回到上面说的,若要统计AAR在App包大小中的占比,除了它自己还不够,还要分析该AAR的独有依赖才能正确计算引入该AAR后的大小。 55 | 56 | #### 依赖树转化 57 | 58 | 在项目的Gradle文件中可以找到该项目的依赖引用,项目执行`./gradlew dependencies`命令后会获取当前项目的依赖树结构,查看gradle命令输出的依赖树可知,依赖树有且比较严格的格式标准,特殊字符+,\,|表示依赖关系起始,空格缩进表示上下级引用关系。部分依赖关系如下: 59 | 60 | ``` xml 61 | +--- com.android.support:design:28.0.0 62 | | +--- com.android.support:support-annotations:28.0.0 63 | | +--- com.android.support:support-compat:28.0.0 (*) 64 | | +--- com.android.support:support-core-ui:28.0.0 (*) 65 | | +--- com.android.support:support-core-utils:28.0.0 (*) 66 | | +--- com.android.support:support-fragment:28.0.0 (*) 67 | | +--- com.android.support:transition:28.0.0 68 | | | +--- com.android.support:support-annotations:28.0.0 69 | | | \--- com.android.support:support-compat:28.0.0 (*) 70 | | +--- com.android.support:appcompat-v7:28.0.0 (*) 71 | | +--- com.android.support:cardview-v7:28.0.0 72 | | | \--- com.android.support:support-annotations:28.0.0 73 | | \--- com.android.support:recyclerview-v7:28.0.0 74 | | +--- com.android.support:support-annotations:28.0.0 75 | | +--- com.android.support:support-compat:28.0.0 (*) 76 | | \--- com.android.support:support-core-ui:28.0.0 (*) 77 | ``` 78 | 79 | 我们可以将项目中的依赖库简化成A,B,C来表示,如图所示,A,A1,A2表示项目直接引用的AAR依赖。B,C是A的子依赖,同时C,D是A1的子依赖。根据上文所述,A的内部依赖B没有其他的被依赖关系,因此称B为A的独有依赖。另外的由于C同时被A和A1依赖,因此称C为公有依赖。 80 | 81 | ![](imgs/node-tree.png) 82 | 83 | 图五 - 节点依赖关系图 84 | 85 | 理解依赖树结构特点,可将依赖树形结构转化成列表来显示。由上述分析可知一个依赖库可能被多个库依赖,产生依赖关系。同时依赖关系是单向有序的,箭头指向表示A依赖B:A是B的父依赖,B是A的子依赖。现挑出A,A1,C作为例子分析,将A,A1作为一个父节点,节点内部同时维护两个列表,父节点列表(parents)和子节点列表(children)。依次就可以将依赖树中所有的依赖关系放置在节点类型的数据结构中。另外root节点是工程总节点,它是A,A1和A2的父节点。通过此步骤,得到依赖列表,记录工程中所有依赖节点: 86 | 87 | ![](imgs/node.png) 88 | 89 | 图六 - 节点依赖列表 90 | 91 | #### 独有依赖分析 92 | 93 | 统计目标AAR大小时,不仅要统计目标AAR还要包括它的独有依赖。通过上文步骤获取到了项目所有AAR列表,在输入目标依赖名称后,我们在列表中遍历找到该目标,检查其子依赖然后获取最后的独有依赖,检查流程如下: 94 | 1. 罗列目标依赖下的所有子依赖,并且遍历子依赖的子依赖,将它们记录到新列表中; 95 | 2. 采用深度便算法来逐一去判断新列表中的依赖,判断其父依赖是否仅在该列表中,如果父依赖全部都在该列表中则保留,否则从列表中可删除该依赖; 96 | 3. 经过步骤2后的筛选,剩下的节点则为目标依赖的独有依赖。 97 | 98 | 下面我们以上文图中的节点A1为例,将A1节点所有的子节点记录到列表。首先独有依赖包括它自身,故A1保留。C节点的父父节点有A和A1,A不在当前列表中,因此C不是独有依赖,将C移除。D的父节点仅有A1且在列表中,因此D是独有依赖。以此类推,在判断I节点时,由于C节点已被移除不在列表中,因此I也不是A1的独有依赖。如下图九:节点独有依赖寻找流程: 99 | 100 | ![](imgs/node-flow.png) 101 | 102 | 图七 - 节点独有依赖寻找流程 103 | 104 | #### 模拟目标AAR 105 | 106 | 在项目打包生成Apk过程中,会利用Gradle缓存特性,工程编译前,获取用户输入的目标AAR,脚本通过目标AAR名称在缓存目录下自动寻找。然后将本地的目标AAR文件进行模拟处理,打包时将该模拟后的AAR打入APK中。如下图八 :模拟替换目标AAR流程: 107 | 108 | ![](imgs/reflect.png) 109 | 110 | 图八 - 模拟替换目标AAR 111 | 112 | 我们知道AAR是二进制归档文件,也是压缩文件,只不过它是AAPT打包命令中的一个结果,通常会压缩:资源文件、类文件、系统文件等。所以找到该AAR后,我们进行“解剖”,步骤如下: 113 | 1. 将目标AAR在当前目录下备份一份; 114 | 2. 将AAR文件重命名变成.zip文件并进行解压缩; 115 | 3. 遍历解压缩文件目录,当目标是文件时,判断其文件类型是否为.xml或.9.png,是则跳过;否则,将其文件大小置为0KB; 116 | 4. 解压缩文件完全按照3步骤处理完成后,将其重新压缩为一个模拟的AAR文件,参与打包计算; 117 | 5. 打包完成后,为了不影响后续打包任务,删除模拟的AAR将备份的AAR文件恢复; 118 | 6. 修改build.gradle文件,使用Gradle的打包特性配置所有all*.exclude移除对应的group和module; 119 | -------------------------------------------------------------------------------- /imgs/sample_aar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/imgs/sample_aar.png -------------------------------------------------------------------------------- /imgs/sample_aar_size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/imgs/sample_aar_size.png -------------------------------------------------------------------------------- /imgs/sample_clone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuba/Zucker/35be5818c3c6461c3fdaf317472cb8393d1d1c8b/imgs/sample_clone.png -------------------------------------------------------------------------------- /zucker.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # 4 | # Copyright Zucker 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | import os 19 | import sys 20 | import subprocess 21 | from shutil import copyfile 22 | import shutil 23 | import re 24 | import zipfile 25 | import copy 26 | from pathlib import Path 27 | 28 | 29 | class CloneProject: 30 | target_project_name = "" 31 | 32 | def clone(self, current_path, output_path, target_project_name, output_project_name): 33 | self.target_project_name = target_project_name 34 | # 如果output目录不存在,则创建文件夹 35 | if not os.path.exists(output_path): 36 | os.mkdir(output_path) 37 | self.__copy_dir(current_path, output_path, output_project_name, 0) 38 | 39 | # 拷贝文件夹 40 | def __copy_dir(self, source_dir, target_dir, target, index): 41 | # 源文件夹 42 | dir = os.path.join(source_dir, target) 43 | if index == 0: 44 | dir = os.path.join(source_dir, self.target_project_name) 45 | else: 46 | dir = os.path.join(source_dir, target) 47 | # 源文件夹下的文件列表 48 | files = os.listdir(dir) 49 | # 目标文件夹 50 | target_dir = os.path.join(target_dir, target) 51 | if not os.path.exists(target_dir): 52 | os.mkdir(target_dir) 53 | if index == 0: 54 | self.fileSize = 0 55 | for dirpath, dirnames, filenames in os.walk(dir): 56 | for file in filenames: 57 | self.fileSize = self.fileSize + 1 58 | self.count = 0 59 | for f in files: 60 | sourcefile = os.path.join(dir, f) 61 | targetfile = os.path.join(target_dir, f) 62 | if os.path.isdir(sourcefile): 63 | index += 1 64 | self.__copy_dir(dir, target_dir, f, index) 65 | if os.path.isfile(sourcefile) and not os.path.exists(targetfile): 66 | copyfile(sourcefile, targetfile) 67 | self.count += 1 68 | print("Cloning: {0}%".format(round((self.count + 1) * 100 / self.fileSize)), end="\r") 69 | 70 | 71 | class TreeNode: 72 | children = set() 73 | parents = set() 74 | parent = None 75 | value = "" 76 | 77 | def __init__(self, value): 78 | self.value = value 79 | self.children = set() 80 | self.parents = set() 81 | self.parent = None 82 | 83 | def add_child(self, node): 84 | self.children.add(node) 85 | 86 | def add_parent(self, node): 87 | self.parent = node 88 | self.parents.add(node) 89 | 90 | def is_root(self): 91 | return len(self.parents) == 0 or self.parent is None 92 | 93 | def get_level(self): 94 | if self.is_root(): 95 | return 0 96 | return self.parent.get_level() + 1 97 | 98 | 99 | class Dependency: 100 | # gradle命令,获取release下依赖树,写入文件 101 | commend = "./gradlew -q dependencies :%s:dependencies --configuration releaseRuntimeClasspath>" 102 | # 去重set 103 | __node_set = set() 104 | # root节点 105 | rootNode = None 106 | # 记录当前的节点 107 | stack = [] 108 | # 所有依赖节点 109 | allNode = [] 110 | # 移除support包和Android原生依赖包 111 | exportArr = ["com.android.support", "android.arch.lifecycle", "com.google.android", 112 | "com.squareup.leakcanary:leakcanary-android", "android.arch.core", 113 | "org.jetbrains.kotlin:kotlin-stdlib-common", "org.jetbrains:annotations", 114 | "androidx.", "project :"] 115 | 116 | def __init__(self, output_project_path, app_dir): 117 | self.file_name = "dependency_" + app_dir + ".txt" 118 | self.projectPath = output_project_path 119 | self.appDir = app_dir 120 | 121 | def get_top_level_aars(self): 122 | self.rootNode = TreeNode(self.appDir) 123 | self.stack.append(self.rootNode) 124 | self.allNode.append(self.rootNode) 125 | # 执行gradle命令 126 | self.commend = (self.commend % self.appDir) + os.path.join(self.projectPath, self.file_name) 127 | # cd到工程目录下,才能正常的读取gradle命令 128 | self.commend = ("cd %s\nchmod +x gradlew\n" % self.projectPath) + self.commend 129 | subprocess.check_call(self.commend, shell=True) 130 | self.__check_dependency_file() 131 | 132 | nodes = [] 133 | for n in self.allNode: 134 | nodeName = n.value 135 | if len(n.parents) >= 1 and not self.__check_aar_in_export(nodeName): 136 | for p in n.parents: 137 | if p == self.rootNode: 138 | nodes.append(nodeName) 139 | continue 140 | return nodes 141 | 142 | def __check_dependency_file(self): 143 | dep_file = os.path.join(self.projectPath, self.file_name) 144 | # 逐行读取文件 145 | with open(dep_file) as f: 146 | line = f.readline() 147 | while line: 148 | line = line.rstrip("\n") 149 | if len(line) == 0 or ( 150 | not line.startswith("+") and (not line.startswith("|")) and (not line.startswith("\\"))): 151 | line = f.readline() 152 | continue 153 | line = line.replace("\\", "+").replace("+---", " ").replace("|", " ").replace(" ", "!") 154 | current_level = line.count("!") 155 | if current_level == 0: 156 | line = f.readline() 157 | continue 158 | last_parent = self.stack.pop() 159 | parent_level = last_parent.get_level() 160 | while not (current_level > parent_level): 161 | last_parent = self.stack.pop() 162 | parent_level = last_parent.get_level() 163 | line = line.replace("!", "").replace(" -> ", ":").replace(" (*)", "") 164 | buffer = line.split(":") 165 | tmp_length = len(buffer) 166 | if tmp_length > 2: 167 | line = "%s:%s:%s" % (buffer[0], buffer[1], buffer[-1]) 168 | if line in self.__node_set: 169 | for node in self.allNode: 170 | if node.value == line: 171 | self.__update_node(node, last_parent) 172 | break 173 | else: 174 | node = TreeNode(line) 175 | self.__update_node(node, last_parent) 176 | self.__node_set.add(node.value) 177 | self.allNode.append(node) 178 | 179 | node.add_parent(last_parent) 180 | last_parent.add_child(node) 181 | self.stack.append(last_parent) 182 | self.stack.append(node) 183 | line = f.readline() 184 | 185 | # 更新依赖关系 186 | @staticmethod 187 | def __update_node(node, parent): 188 | node.add_parent(parent) 189 | parent.add_child(node) 190 | 191 | """ 192 | # 获取输入的多个aar依赖:该方法是要统计系列依赖比如fresco,animated-gif,webpsupport,animated-webp 193 | 方法步骤: 194 | 遍历输入的节点,将该节点的依赖树记录到result set中 195 | copy一份des set 196 | 遍历该result set,检查每个节点的父节点是否仅在这个des set中 197 | 在-->保留该节点,否则将该节点不是独有依赖,从des set中移除 198 | 经过一次遍历后,该des set即为结果set 199 | """ 200 | 201 | def __get_array_node(self, array): 202 | input_set = set() 203 | result = set() 204 | # 遍历输入的aar列表 205 | for s in array: 206 | n = self.__find_node_by_name(s) 207 | if n is None: 208 | print("暂无该依赖节点%s" % s) 209 | continue 210 | input_set.add(n) 211 | result.add(n) 212 | self.__add_children_node(n, result) 213 | # copy依赖的节点,作为结果set 214 | des = result.copy() 215 | for rNode in result: 216 | if rNode in input_set or 1 == len(rNode.parents): 217 | continue 218 | # 移除support库 219 | for p in rNode.parents: 220 | if p not in des: 221 | des.remove(rNode) 222 | break 223 | return des 224 | 225 | def get_input_aar(self, target_aar): 226 | if self.__check_aar_in_export(target_aar): 227 | print("不支持【%s】该类型的库统计!" % target_aar) 228 | return [] 229 | array = [target_aar.lstrip(" ").rstrip(" ")] 230 | aars = self.__get_array_node(array) 231 | result = [] 232 | for aar in aars: 233 | if self.__check_aar_in_export(aar.value): 234 | continue 235 | result.append(aar.value) 236 | return result 237 | 238 | # 校验传入的aar是否在去除列表中 239 | def __check_aar_in_export(self, aar_name): 240 | result = False 241 | for s in self.exportArr: 242 | if aar_name.startswith(s): 243 | result = True 244 | break 245 | return result 246 | 247 | # 遍历所有节点,记录在set中 248 | def __add_children_node(self, node, node_set): 249 | if len(node.children) == 0: 250 | node_set.add(node) 251 | return 252 | for child in node.children: 253 | node_set.add(child) 254 | self.__add_children_node(child, node_set) 255 | 256 | # 根据名称获取依赖节点 257 | def __find_node_by_name(self, node_name): 258 | node = None 259 | for n in self.allNode: 260 | if n.value.find(node_name) != -1: 261 | node = n 262 | break 263 | return node 264 | 265 | 266 | class AarCache: 267 | # 记录传入的aar本地缓存路径 268 | targetAarPath = "" 269 | # gradle本地目录 270 | gradleUserHome = "" 271 | __envMap = os.environ 272 | 273 | def __init__(self): 274 | # 先检查是否将路径写在环境变量中 275 | gradle_home = self.__envMap.get("GRADLE_USER_HOME") 276 | if gradle_home is None or not os.path.exists(gradle_home): 277 | # 查看默认路径 ~/user/.gradle 278 | gradle_home = os.path.join(self.__envMap.get('HOME'), ".gradle") 279 | self.gradleUserHome = os.path.join(gradle_home, "caches", "modules-2", "files-2.1") 280 | 281 | # 获取率AAR 282 | def get_aar_file(self, aar_name): 283 | aar_info = aar_name.split(":") 284 | aar_path = os.path.join(self.gradleUserHome, aar_info[0], aar_info[1], aar_info[2]) 285 | # print(aarPath) 286 | if not os.path.exists(aar_path): 287 | print("aar 本地缓存不存在") 288 | return False 289 | aar_file = self.__get_aar_file_(aar_path) 290 | self.targetAarPath = aar_file 291 | return aar_file and os.path.exists(aar_file) 292 | 293 | @staticmethod 294 | def __get_aar_file_(file): 295 | for root, dirs, files in os.walk(file, topdown=False): 296 | for name in files: 297 | if name.endswith(".aar"): 298 | return os.path.join(root, name) 299 | 300 | 301 | class Compile: 302 | def __init__(self, output_project_path): 303 | self.outputProjectPath = output_project_path 304 | 305 | def new_module(self, app_dirs): 306 | compile_sdk_version = "" 307 | build_tools_version = "" 308 | # settings.gradle 309 | settings = os.path.join(self.outputProjectPath, "settings.gradle") 310 | with open(settings, 'a+') as f: 311 | f.write("\ninclude ':zucker'") 312 | # app build.gradle 313 | regex = re.compile(r'dependencies([\s]*){') 314 | STATEMENT = " implementation project(':zucker')\n" 315 | for dir in app_dirs: 316 | lines = [] 317 | gradle_file = os.path.join(self.outputProjectPath, dir, "build.gradle") 318 | with open(gradle_file, 'r') as f: 319 | has_found_dependencies = False 320 | bracket_count = 0 321 | for line in f.readlines(): 322 | lines.append(line) 323 | if line.lstrip().startswith("//"): 324 | pass 325 | if has_found_dependencies: 326 | for index, c in enumerate(line): 327 | if c == '{': 328 | bracket_count += 1 329 | elif c == '}': 330 | bracket_count -= 1 331 | if bracket_count < 0: 332 | lines.remove(STATEMENT) 333 | has_found_dependencies = False 334 | bracket_count = 0 335 | if "compileSdkVersion" in line: 336 | compile_sdk_version = line 337 | elif "buildToolsVersion" in line: 338 | build_tools_version = line 339 | elif regex.search(line): 340 | has_found_dependencies = True 341 | bracket_count += 1 342 | lines.append(STATEMENT) 343 | with open(gradle_file, "w") as f: 344 | f.writelines(lines) 345 | f.close() 346 | # zucker dir 347 | zucker = os.path.join(self.outputProjectPath, "zucker") 348 | if not Path(zucker).exists(): 349 | os.mkdir(zucker) 350 | # src dir 351 | src = os.path.join(zucker, "src") 352 | if not Path(src).exists(): 353 | os.mkdir(src) 354 | # zucker main 355 | main = os.path.join(src, "main") 356 | if not Path(main).exists(): 357 | os.mkdir(main) 358 | # AndroidManifest 359 | manifest = os.path.join(main, "AndroidManifest.xml") 360 | with open(manifest, 'w') as f: 361 | f.write("") 362 | # project build.gradle 363 | build = os.path.join(zucker, "build.gradle") 364 | with open(build, 'w') as f: 365 | f.write("apply plugin: 'com.android.library'\n\n") 366 | f.write("android {\n") 367 | f.write(compile_sdk_version + "\n") 368 | f.write(build_tools_version + "\n") 369 | f.write("}\n\n") 370 | f.write("dependencies {\n\n}") 371 | return main 372 | 373 | def clear_flavors(self, app_dirs): 374 | self.__clear_bucket_content('productFlavors', app_dirs) 375 | 376 | def insert_script(self, app_dirs): 377 | PATH = self.outputProjectPath 378 | for targetFile in app_dirs: 379 | package_size_path = os.path.join(PATH, targetFile, "zucker.txt") 380 | open(package_size_path, 'w') 381 | gradle_file = os.path.join(PATH, targetFile, 'build.gradle') 382 | with open(gradle_file, 'a+') as f: 383 | f.write("\n") 384 | f.write("android.applicationVariants.all { variant ->\n") 385 | f.write(" variant.outputs.all { output ->\n") 386 | f.write(" if (output.outputFileName.contains('debug.apk')) {\n") 387 | f.write(" Task assembleDebug = tasks.getByName('assembleDebug')\n") 388 | f.write(" File file = output.outputFile\n") 389 | f.write(" assembleDebug.doLast {\n") 390 | f.write(" def apkSize = file.length().toString()\n") 391 | f.write(" print('ApkSize: '+apkSize)\n") 392 | f.write(" def packageSizeFile = new File(\"" + package_size_path + "\")\n") 393 | f.write(" packageSizeFile.withWriter { writer ->\n") 394 | f.write(" writer.write(apkSize)\n") 395 | f.write(" }\n") 396 | f.write(" }\n") 397 | f.write(" }\n") 398 | f.write(" }\n") 399 | f.write("}\n\n") 400 | 401 | def find_app_dirs(self): 402 | app_dirs = [] 403 | PATH = self.outputProjectPath 404 | dir_list = [x for x in os.listdir(PATH) if 405 | os.path.isdir(os.path.join(PATH, x)) and not x.startswith('.') and not x == 'gradle'] 406 | for targetFile in dir_list: 407 | gradle_file = os.path.join(PATH, targetFile, 'build.gradle') 408 | if os.path.isfile(gradle_file): 409 | with open(gradle_file) as f: 410 | for index, line in enumerate(f.readlines()): 411 | if "apply plugin: 'com.android.application'" in line: 412 | app_dirs.append(targetFile) 413 | break 414 | return app_dirs 415 | 416 | def __clear_bucket_content(self, TAG, appDirs): 417 | PATH = self.outputProjectPath 418 | for targetFile in appDirs: 419 | gradle_file = os.path.join(PATH, targetFile, 'build.gradle') 420 | with open(gradle_file, 'r') as f: 421 | tag_lines = [] 422 | has_find_tag = False 423 | has_find_start_tag = False 424 | has_find_end_tag = False 425 | bracket_count = 0 426 | for line in f.readlines(): 427 | if line.lstrip().startswith("//"): 428 | tag_lines.append(line) 429 | continue 430 | if not has_find_tag: 431 | index = line.find(TAG) 432 | if index >= 0: 433 | has_find_tag = True 434 | start_index = 0 435 | end_index = len(line) 436 | for index, c in enumerate(line): 437 | if c == '{': 438 | if not has_find_start_tag: 439 | has_find_start_tag = True 440 | start_index = index + 1 441 | bracket_count += 1 442 | elif c == '}': 443 | bracket_count -= 1 444 | if has_find_start_tag and bracket_count == 0: 445 | has_find_end_tag = True 446 | end_index = index 447 | break 448 | if has_find_end_tag: 449 | tag_lines.append(line[0:start_index] + line[end_index:len(line)]) 450 | else: 451 | if has_find_start_tag: 452 | tag_lines.append(line[0:start_index] + "\n") 453 | else: 454 | tag_lines.append(line) 455 | if has_find_tag and not has_find_end_tag: 456 | start_index = -1 457 | end_index = len(line) 458 | for index, c in enumerate(line): 459 | if c == '{': 460 | if not has_find_start_tag: 461 | has_find_start_tag = True 462 | start_index = index + 1 463 | bracket_count += 1 464 | elif c == '}': 465 | bracket_count -= 1 466 | if has_find_start_tag and bracket_count == 0: 467 | has_find_end_tag = True 468 | end_index = index 469 | break 470 | if has_find_start_tag: 471 | linebreak = "" 472 | if start_index >= 0: 473 | linebreak = "\n" 474 | else: 475 | start_index = 0 476 | if has_find_end_tag: 477 | tag_lines.append(line[0:start_index] + linebreak + " " + line[end_index:len(line)]) 478 | else: 479 | tag_lines.append(line[0:start_index] + linebreak) 480 | else: 481 | tag_lines.append(line) 482 | if has_find_tag and has_find_end_tag: 483 | tag_lines.append(line) 484 | if not has_find_tag and not has_find_end_tag: 485 | tag_lines.append(line) 486 | if has_find_tag: 487 | fd = open(gradle_file, "w") 488 | fd.writelines(tag_lines) 489 | fd.close() 490 | 491 | def compile(self): 492 | command = "cd " + self.outputProjectPath + "\n" 493 | command += "chmod +x gradlew" + "\n" 494 | command += "./gradlew clean" + "\n" 495 | command += "./gradlew assembleDebug" 496 | subprocess.call(command, shell=True) 497 | 498 | 499 | class MockCache: 500 | zucker_res_size = 0 501 | 502 | def __init__(self, origin_aar_cache_path, target_main_src_path): 503 | # 复制文件 504 | self.originAarCachePath = origin_aar_cache_path 505 | self.targetMainSrcPath = target_main_src_path 506 | self.mockAarCachePath = origin_aar_cache_path.replace(".aar", "-origin.zip") 507 | self.mockAarOriginPath = origin_aar_cache_path.replace(".aar", "-origin.aar") 508 | 509 | def mock_cache(self): 510 | copyfile(self.originAarCachePath, self.mockAarCachePath) 511 | copyfile(self.originAarCachePath, self.mockAarOriginPath) 512 | 513 | # 解压 514 | unzip_file = os.path.dirname(self.originAarCachePath) + "/" + ( 515 | os.path.basename(self.originAarCachePath)).replace(".aar", "") 516 | file_zip = zipfile.ZipFile(self.mockAarCachePath, 'r') 517 | for file in file_zip.namelist(): 518 | file_zip.extract(file, unzip_file) 519 | file_zip.close() 520 | 521 | self._copy_mock_file(unzip_file, self.targetMainSrcPath) 522 | # 基础Mock 523 | for root, dirs, files in os.walk(os.path.dirname(self.targetMainSrcPath + "/res"), topdown=False): 524 | for name in files: 525 | if name.startswith('values') and name.endswith('.xml'): 526 | pass 527 | elif name in ('AndroidManifest.xml'): 528 | pass 529 | elif ('layout' in root) and name.endswith('.xml'): 530 | mypath = os.path.join(root, name) 531 | fd = open(mypath, "w") 532 | fd.write("") 533 | fd.write("") 534 | fd.close() 535 | elif ('drawable' in root) and name.endswith('.xml'): 536 | mypath = os.path.join(root, name) 537 | fd = open(mypath, "w") 538 | fd.write("") 539 | fd.write("") 540 | fd.close() 541 | elif ('drawable' in root) and name.endswith('.9.png'): 542 | pass 543 | elif ('mipmap' in root) and name.endswith('.9.png'): 544 | pass 545 | elif ('anim' in root) and name.endswith('.xml'): 546 | mypath = os.path.join(root, name) 547 | fd = open(mypath, "w") 548 | fd.write("") 549 | fd.write("") 550 | fd.close() 551 | elif ('color' in root) and name.endswith('.xml'): 552 | mypath = os.path.join(root, name) 553 | fd = open(mypath, "w") 554 | fd.write("") 555 | fd.write("") 556 | fd.close() 557 | elif ('xml' in root) and name.endswith('.xml'): 558 | mypath = os.path.join(root, name) 559 | fd = open(mypath, "w") 560 | fd.write("") 561 | fd.write("") 562 | fd.close() 563 | else: 564 | mypath = os.path.join(root, name) 565 | fd = open(mypath, "w") 566 | fd.writelines([]) 567 | fd.close() 568 | 569 | # 遍历文件,并删除 os.path.join(path, file) 570 | white_list = ["classes.jar"] 571 | dirs = os.listdir(unzip_file) 572 | for root, dirs, files in os.walk(unzip_file, topdown=False): 573 | for name in files: 574 | if name in white_list: 575 | pass 576 | else: 577 | os.remove(os.path.join(root, name)) 578 | # 删除原有AAR 579 | for root, dirs, files in os.walk(os.path.dirname(self.originAarCachePath), topdown=False): 580 | for name in files: 581 | if name in os.path.basename(self.originAarCachePath): 582 | os.remove(os.path.join(root, name)) 583 | 584 | # 计算zucker库里面res的文件大小 585 | self.zucker_res_size = self._get_dir_size(os.path.dirname(self.targetMainSrcPath + "/res")) 586 | # 压缩Mock的File 587 | self._zip_mock_file(unzip_file) 588 | 589 | @staticmethod 590 | def _get_dir_size(path): 591 | # 计算指定的路径下的所有文件的大小 592 | if os.path.isdir(path): 593 | file_size, dir_list = 0, [path] 594 | while dir_list: 595 | path = dir_list.pop() 596 | dirs = os.listdir(path) 597 | for name in dirs: 598 | file_path = os.path.join(path, name) 599 | if os.path.isfile(file_path): 600 | file_size += os.path.getsize(file_path) 601 | else: 602 | dir_list.append(file_path) 603 | return file_size 604 | elif os.path.isfile(path): 605 | return os.path.getsize(path) 606 | else: 607 | print('找不到%s文件' % path) 608 | 609 | def _copytree(self, src, dst, symlinks=False, ignore=None, copy_function=shutil.copy2): 610 | names = os.listdir(src) 611 | if ignore is not None: 612 | ignored_names = ignore(src, names) 613 | else: 614 | ignored_names = set() 615 | if not os.path.exists(dst): 616 | os.makedirs(dst) 617 | errors = [] 618 | for name in names: 619 | if name in ignored_names: 620 | continue 621 | srcname = os.path.join(src, name) 622 | dstname = os.path.join(dst, name) 623 | try: 624 | if os.path.islink(srcname): 625 | linkto = os.readlink(srcname) 626 | if symlinks: 627 | # We can't just leave it to `copy_function` because legacy 628 | # code with a custom `copy_function` may rely on copytree 629 | # doing the right thing. 630 | os.symlink(linkto, dstname) 631 | shutil.copystat(srcname, dstname, follow_symlinks=not symlinks) 632 | else: 633 | # ignore dangling symlink if the flag is on 634 | if not os.path.exists(linkto) and False: 635 | continue 636 | # otherwise let the copy occurs. copy2 will raise an error 637 | if os.path.isdir(srcname): 638 | self._copytree(srcname, dstname, symlinks, ignore, 639 | copy_function) 640 | else: 641 | copy_function(srcname, dstname) 642 | elif os.path.isdir(srcname): 643 | self._copytree(srcname, dstname, symlinks, ignore, copy_function) 644 | else: 645 | # Will raise a SpecialFileError for unsupported file types 646 | copy_function(srcname, dstname) 647 | # catch the Error from the recursive copytree so that we can 648 | # continue with other files 649 | except shutil.Error as err: 650 | errors.extend(err.args[0]) 651 | except OSError as why: 652 | errors.append((srcname, dstname, str(why))) 653 | try: 654 | shutil.copystat(src, dst) 655 | except OSError as why: 656 | # Copying file access times may fail on Windows 657 | if getattr(why, 'winerror', None) is None: 658 | errors.append((src, dst, str(why))) 659 | if errors: 660 | raise shutil.Error(errors) 661 | return dst 662 | 663 | def _copy_mock_file(self, origin_path, target_main_src_path): 664 | origin_path = origin_path + "/res" 665 | if os.path.exists(origin_path): 666 | self._copytree(origin_path, target_main_src_path + "/res") 667 | elif not os.path.exists(target_main_src_path + "/res"): 668 | os.makedirs(target_main_src_path + "/res") 669 | 670 | @staticmethod 671 | def _zip_mock_file(start_dir): 672 | start_dir = start_dir 673 | file_news = start_dir + '.aar' 674 | 675 | z = zipfile.ZipFile(file_news, 'w', zipfile.ZIP_DEFLATED) 676 | for dir_path, dir_names, file_names in os.walk(start_dir): 677 | f_path = dir_path.replace(start_dir, '') 678 | f_path = f_path and f_path + os.sep or '' 679 | for filename in file_names: 680 | z.write(os.path.join(dir_path, filename), f_path + filename) 681 | z.close() 682 | return file_news 683 | 684 | @staticmethod 685 | def add_configurations(aar, output_project_path, app_dir): 686 | aars = aar.split(":") 687 | name = aars[1] + "-" + aars[2] 688 | sub_path = os.path.join(output_project_path, app_dir) 689 | 690 | build_gradle = sub_path + "/build.gradle" 691 | if os.path.exists(build_gradle): 692 | # 先读文件 693 | configurations = open(build_gradle, 'r') 694 | content = configurations.read() 695 | post = content.find("configurations {") 696 | if post != -1: 697 | configurations = open(build_gradle, 'w') 698 | content = content[:post + len("configurations {")] + "\n" + "all*.exclude group: \'" + aars[ 699 | 0] + "\'" + " ,module: " + "\'" + aars[1] + "\'\n" + content[post + len("configurations {"):] 700 | configurations.write(content) 701 | else: 702 | configurations = open(build_gradle, 'a+') 703 | configurations.write("configurations {") 704 | configurations.write("\n") 705 | configurations.write("all*.exclude group: \'" + aars[0] + "\'" + " ,module: " + "\'" + aars[1] + "\'") 706 | configurations.write("\n") 707 | configurations.write("}") 708 | configurations.close() 709 | 710 | 711 | class RevertCache: 712 | # 回滚修改的Cache目标AAR 713 | def __init__(self, origin_aar_cache_path): 714 | self.originAarCachePath = origin_aar_cache_path 715 | 716 | def revert(self): 717 | file_name = os.path.dirname(self.originAarCachePath) 718 | dirs = os.listdir(file_name) 719 | for root, dirs, files in os.walk(file_name, topdown=False): 720 | for name in files: 721 | if name.endswith("-origin.aar"): 722 | pass 723 | else: 724 | os.remove(os.path.join(root, name)) 725 | for name in dirs: 726 | os.rmdir(os.path.join(root, name)) 727 | 728 | for root, dirs, files in os.walk(file_name, topdown=False): 729 | for name in files: 730 | if name.endswith("-origin.aar"): 731 | new_name = copy.deepcopy(name) 732 | new_name = new_name.replace("-origin.aar", ".aar") 733 | os.rename(root + "/" + name, root + "/" + new_name) 734 | 735 | 736 | class PackageSize: 737 | # 统计大小,并输出最终结果 738 | @staticmethod 739 | def get_result(base_output_project_path, aar_output_project_path, app_dir, zucker_res_size): 740 | base_pack_size = 0 741 | aar_pack_size = 0 742 | base_pack_size_path = os.path.join(base_output_project_path, app_dir, "zucker.txt") 743 | aar_pack_size_path = os.path.join(aar_output_project_path, app_dir, "zucker.txt") 744 | with open(base_pack_size_path) as f: 745 | for line in f.readlines(): 746 | base_pack_size = line 747 | print("基础包大小(basePackSize,单位Byte): " + line) 748 | break 749 | with open(aar_pack_size_path) as f: 750 | for line in f.readlines(): 751 | aar_pack_size = line 752 | aar_pack_size = int(aar_pack_size) - (int(zucker_res_size) * 2) 753 | print("替换后的APK大小(aarPackSize,单位Byte): " + str(aar_pack_size)) 754 | break 755 | aar_size = int(base_pack_size) - int(aar_pack_size) 756 | print("AAR大小(aarSize,单位Byte): " + str(aar_size)) 757 | 758 | 759 | if __name__ == '__main__': 760 | sys.argv.append("ZuckerDemo") 761 | # 工程文件夹名称 762 | projectName = sys.argv[1] 763 | # 当前目录 764 | currentPath = os.getcwd() 765 | # 输出目录 766 | outputPath = os.path.join(currentPath, "output") 767 | # 资源大小 768 | zuckerResSize = "" 769 | # 是否找到缓存 770 | isCacheExist = False 771 | 772 | # 基础包:克隆、打包流程====================================== 773 | # 基础包AAR工程目录 774 | baseOutputProjectPath = os.path.join(outputPath, projectName + "_BASE") 775 | if not os.path.exists(baseOutputProjectPath): 776 | # 克隆工程 777 | cloneBaseProject = CloneProject() 778 | cloneBaseProject.clone(currentPath, outputPath, projectName, projectName + "_BASE") 779 | print("cloneBaseProject DONE") 780 | # 编译工程 781 | baseCompile = Compile(baseOutputProjectPath) 782 | baseAppDirs = baseCompile.find_app_dirs() 783 | print("findBaseAppDirs DONE") 784 | baseCompile.clear_flavors(baseAppDirs) 785 | print("clearBaseFlavors DONE") 786 | baseCompile.insert_script(baseAppDirs) 787 | print("insertBaseScript DONE") 788 | baseCompile.compile() 789 | 790 | # AAR:克隆、依赖、mock、打包流程====================================== 791 | # AAR工程目录 792 | outputProjectPath = os.path.join(outputPath, projectName + "_AAR") 793 | # 如果已经存在,则删除 794 | if os.path.exists(outputProjectPath): 795 | shutil.rmtree(outputProjectPath, True) 796 | # 克隆工程 797 | cloneProject = CloneProject() 798 | cloneProject.clone(currentPath, outputPath, projectName, projectName + "_AAR") 799 | print("cloneAARProject DONE") 800 | # 编译工程 801 | compile = Compile(outputProjectPath) 802 | appDirs = compile.find_app_dirs() 803 | print("findAARAppDirs DONE") 804 | zuckerModuleMainDir = compile.new_module(appDirs) 805 | print("newModule DONE: " + zuckerModuleMainDir) 806 | compile.clear_flavors(appDirs) 807 | print("clearAARFlavors DONE") 808 | compile.insert_script(appDirs) 809 | print("insertAARScript DONE") 810 | 811 | # 遍历工程下依赖的所有AAR 812 | for appdir in appDirs: 813 | dependency = Dependency(outputProjectPath, appdir) 814 | aars = dependency.get_top_level_aars() 815 | for aar in aars: 816 | print(aar) 817 | target_aar = input("输入AAR名称及版本,格式xxx.xxx:xxx:xxx:") 818 | result_aars = dependency.get_input_aar(target_aar) 819 | print("输出AAR:") 820 | print(result_aars) 821 | 822 | targetAarArray = [] 823 | for aar in result_aars: 824 | aar_cache = AarCache() 825 | if aar_cache.get_aar_file(aar): 826 | print(aar_cache.targetAarPath) 827 | mockCache = MockCache(aar_cache.targetAarPath, zuckerModuleMainDir) 828 | mockCache.mock_cache() 829 | mockCache.add_configurations(aar, outputProjectPath, appdir) 830 | zuckerResSize = mockCache.zucker_res_size 831 | targetAarArray.append(aar_cache.targetAarPath) 832 | isCacheExist = True 833 | else: 834 | isCacheExist = False 835 | 836 | if isCacheExist: 837 | compile.compile() 838 | print("compile DONE") 839 | 840 | # 将修改的AAR进行回滚 841 | for path in targetAarArray: 842 | revertCache = RevertCache(path) 843 | revertCache.revert() 844 | 845 | # 统计大小并输出 846 | packSize = PackageSize() 847 | packSize.get_result(baseOutputProjectPath, compile.outputProjectPath, appdir, zuckerResSize) 848 | else: 849 | print("缓存aar未找到,请重新尝试") 850 | # break 851 | --------------------------------------------------------------------------------