Ignore proguard configuration of an external library - android

So, I want to add an external library to my project. The library itself is quite small, around 300 methods. But it is configured to be very liberal with it's proguard configuration. I ran a simple test with/without the library and with/without proguard on a barebones project and this is what I came up with
Proguard Lib Method Count
N N 15631
Y N 6370
N Y 15945
Y Y 15573
As you can see, with proguard enabled, the count is ~6000. But the moment I add the lib, count shoots up to ~15000 despite the library itself being only ~300 methods.
So my question is, how do I ignore the proguard configuration of this particular library?
UPDATE:
It is not possible with android gradle plugin now. I found android bug which doesn't have priority at all. Please avoid answers with mentioning "it is not possible" and keep question opened until a workaround or an official decision is possible. Otherwise, you will collect half of bounty without adding value. Thanks!

In this specific case you have a few options:
extract the classes.jar file from the aar and include it as normal jar dependency in your project (will not work when the aar includes resources)
change the aar and remove the consumer proguard rules from it
use DexGuard which allows you to filter out unwanted consumer rules
do a bit of gradle hacking, see below
Add the following to your build.gradle:
afterEvaluate {
// All proguard tasks shall depend on our filter task
def proguardTasks = tasks.findAll { task ->
task.name.startsWith('transformClassesAndResourcesWithProguardFor') }
proguardTasks.each { task -> task.dependsOn filterConsumerRules }
}
// Let's define our custom task that filters some unwanted
// consumer proguard rules
task(filterConsumerRules) << {
// Collect all consumer rules first
FileTree allConsumerRules = fileTree(dir: 'build/intermediates/exploded-aar',
include: '**/proguard.txt')
// Now filter the ones we want to exclude:
// Change it to fit your needs, replace library with
// the name of the aar you want to filter.
FileTree excludeRules = allConsumerRules.matching {
include '**/library/**'
}
// Print some info and delete the file, so ProGuard
// does not pick it up. We could also just rename it.
excludeRules.each { File file ->
println 'Deleting ProGuard consumer rule ' + file
file.delete()
}
}
When using DexGuard (7.2.02+), you can add the following snippet to your build.gradle:
dexguard {
// Replace library with the name of the aar you want to filter
// The ** at the end will include every other rule.
consumerRuleFilter '!**/library/**,**'
}
Mind that the logic is inverted to the ProGuard example above, the consumerRuleFilter will only include consumer rules that match the pattern.

In case you're using R8 (which replaced ProGuard since Android Gradle plugin 3.4.0) - you can filter-out specific consumer rule files by adding the following work-around to your module's build.gradle:
tasks.whenTaskAdded { Task task ->
// Once 'minifyEnabled' is set to 'true' for a certain build type/variant -
// a 'minify<variantName>WithR8' task will be created for each such variant
//
// - This task is implemented by com.android.build.gradle.internal.tasks.R8Task
// - R8Task extends from ProguardConfigurableTask
// - ProguardConfigurableTask exposes property 'configurationFiles'
// - configurationFiles contains all files that will be contributing R8 rules
// - configurationFiles is mutable (its type is ConfigurableFileCollection)
//
// Thus - we can overwrite the list of files and filter them out as we please
//
// More details: https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/internal/tasks/R8Task.kt
if (task.name.startsWith("minify") && task.name.endsWith("WithR8")) {
afterEvaluate {
def filteredList = task.configurationFiles.filter {
// Example paths in this collection:
// /Users/me/MyProject/myModule/proguard-rules.pro
// (for library dependencies) /Users/me/.gradle/caches/<...>/okhttp3.pro
// The below filter condition will, for example, exclude consumer ProGuard rules file from the AndroidX RecyclerView library
!it.path.contains("recyclerview-1.1.0")
}
task.configurationFiles.setFrom(filteredList.files)
}
}
}
The above work-around was confirmed to be working on Android Gradle plugin 4.2.2
If one decides to rely on such a work-around - it might be a good idea to also add some sort of automated checks and/or tests to make sure this filtering is working. Given that the solution is quite fragile and can break with future updates of Android Gradle plugin.

Inspired by Jonas' answer, modified for Kotlin DSL and confirmed working on Android Gradle plugin 7.2.1:
import com.android.build.gradle.internal.tasks.ProguardConfigurableTask
afterEvaluate {
// Get each ProguardConfigurableTask
tasks.withType(ProguardConfigurableTask::class.java).forEach { task ->
// Remove proguard rules from lifecycle-runtime library
val filteredConfigurationFiles = task.configurationFiles.filter { file ->
!file.path.contains("lifecycle-runtime")
}
task.configurationFiles.setFrom(filteredConfigurationFiles)
}
}

Related

How to generate OpenAPI sources from gradle when building Android app

What I'm trying to achieve
I'm trying to generate my REST API client for Android using OpenAPI Generator from the build.gradle script. That way, I wouldn't have to run the generator command line every time the specs change. Ideally, this would be generated when I build/assemble my app, and the sources would end up in the java (generated) folder, where generated sources are then accessible from the code (this is what happens with the BuildConfig.java file for example).
What I've tried so far
Following this link from their official GitHub, here's the build.gradle file I ended up with:
apply plugin: 'com.android.application'
apply plugin: 'org.openapi.generator'
...
openApiValidate {
inputSpec = "$rootDir/app/src/main/openapi/my-api.yaml"
recommend = true
}
openApiGenerate {
generatorName = "java"
inputSpec = "$rootDir/app/src/main/openapi/my-api.yaml"
outputDir = "$buildDir/generated/openapi"
groupId = "$project.group"
id = "$project.name-openapi"
version = "$project.version"
apiPackage = "com.example.mypackage.api"
invokerPackage = "com.example.mypackage.invoker"
modelPackage = "com.example.mypackage.model"
configOptions = [
java8 : "true",
dateLibrary : "java8",
library : "retrofit2"
]
}
...
First, I've never managed to get the API generated with the build/assemble task, even when I tried adding:
compileJava.dependsOn tasks.openApiGenerate
or
assemble.dependsOn tasks.openApiGenerate
The only way I could generate the sources was by manually triggering the openApiGenerate task:
Then, when I do generate my sources this way, they end up in the build folder but aren't accessible from my code, and aren't visible in the java (generated) folder:
I then have to manually copy/paste the generated source files to my project sources in order to use the API.
Even though I'm able to work around these issues by adding manual procedures, it would be way more maintainable if the whole process was simply automatic. I was able to achieve a similar result with another tool, Protobuf. Indeed, my gradle task gets triggered every time I build the app, and the sources end up in the java (generated) folder, so I don't have to do any additional work. The task is much simpler though, so I assume the main work that I'm not able to replicate with OpenAPI Generator is handled by the Protobuf plugin itself.
You have to specify path to the generated sources as a custom source set for your Gradle module, which is app in this case, as described here – https://developer.android.com/studio/build/build-variants#configure-sourcesets. That way Gradle will treat your sources as accessible from your code.
Something like this:
android {
...
sourceSets {
main {
java.srcDirs = ['build/generated/openapi/src/main/java']
}
}
...
}
I solved the issue you described like this, I'm using gradle.kts however.
See my build.gradle.kts
plugins {
// Your other plugins
id("org.openapi.generator") version "5.3.0"
}
openApiGenerate {
generatorName.set("kotlin")
inputSpec.set("$rootDir/app/src/main/openapi/my-api.yaml")
outputDir.set("$buildDir/generated/api")
// Your other specification
}
application {
// Your other code
sourceSets {
main {
java {
// TODO: Set this path according to what was generated for you
srcDir("$buildDir/generated/api/src/main/kotlin")
}
}
}
}
tasks.compileKotlin {
dependsOn(tasks.openApiGenerate)
}
You need to build the application at least once for the IDE to detect the library (at least this is the case for me in Intellij)
Your build should automatically generate the open api classes , to refer the generated classes in your java project you should add the generated class path to your source directory like it was mentioned in the other answers
https://developer.android.com/studio/build/build-variants#configure-sourcesets
As far as the task dependency goes , in android tasks are generated after configuration thus for gradle to recognize the task , wrap it inside afterEvaluate block like
afterEvaluate {
tasks.compileDebugJavaWithJavac.dependsOn(tasks.openApiGenerate)
}
I had this issue, and this answer https://stackoverflow.com/a/55646891/14111809 led me to a more informative error:
error: incompatible types: Object cannot be converted to Annotation
#java.lang.Object()
Taking a look at the generated files that were causing this error, noticed:
import com.squareup.moshi.Json;
After including a Moshi in the app build.gradle, the build succeeded and the generated code was accessible.
implementation("com.squareup.moshi:moshi-kotlin:1.13.0")

Gradle task for Sentry not compiling

I have to add the Analytics tool Sentry to our Android project. In order to make it work, one needs to create mappings for the obfuscated code (from Proguard/R8) and upload it later to Sentry.
On the website https://docs.sentry.io/platforms/android/ it is even described how to do that.
There it is written that one needs to create a gradle task looking like this:
gradle.projectsEvaluated {
android.applicationVariants.each { variant ->
def variantName = variant.name.capitalize();
def proguardTask = project.tasks.findByName(
"transformClassesAndResourcesWithProguardFor${variantName}")
def dexTask = project.tasks.findByName(
"transformClassesWithDexFor${variantName}")
def task = project.tasks.create(
name: "processSentryProguardFor${variantName}",
type: Exec) {
workingDir project.rootDir
commandLine *[
"sentry-cli",
"upload-proguard",
"--write-properties",
"${project.rootDir.toPath()}/app/build/intermediates/assets" +
"/${variant.dirName}/sentry-debug-meta.properties",
variant.getMappingFile(),
"--no-upload"
]
}
dexTask.dependsOn task
task.dependsOn proguardTask
}
}
This shall wait until Proguard is finished, than copy this properties file to the assets. However, when I add this to my Android gradle script I get the error:
Could not create task
':app:processSentryProguardForPlayStoreStagingDebug'.
No signature of method: java.util.ArrayList.multiply() is applicable for argument types: (ArrayList) values: [[sentry-cli, upload-proguard,
--write-properties, {Application-Path}/app/build/intermediates/assets/playStoreStaging/debug/sentry-debug-meta.properties,
...]] Possible solutions: multiply(java.lang.Number),
multiply(java.lang.Number)
I assume there is something wrong with the multiplication symbol * before the commandLine array. But when I remove it I get the error
Could not create task
':app:processSentryProguardForPlayStoreStagingDebug'.
Cannot cast object 'sentry-cli' with class 'java.lang.String' to class 'int'
So I tried to test this with only that line
commandLine "sentry-cli", ...
Which gave me another error
What went wrong: Cannot invoke method dependsOn() on null object
Thus I assume something went really wrong with that gradle script since it seems the dependend task can't be found. Does anyone have any idea how to fix this (or optionally have any other idea how to copy that sentry-debug-meta.properties file to my assets in another way, once Proguard/R8 is finished)?
Thanks!
-------- EDIT --------
I noticed something important.
The gradle tasks are defined in a different name than what was defined in the manual. Looking at my tasks I have them named
transformClassesAndResourcesWithR8For...
and
transformClassesWithDexBuilderFor...
However, I print the variantName then for checking but it seems my tasks are incomplete.
In my tasks list there exist
transformClassesAndResourcesWithR8ForPlayStoreStagingDebug
but not
transformClassesAndResourcesWithR8ForPlayStoreStagingRelease
and thus the task can't be found. I think that is the real problem here. So where are these gradle tasks defined?
------- EDIT 2 --------
Okay I noticed something strange here. Some variants don't have tasks. It makes sense that DEBUG tasks don't have R8 tasks but I found this here:
Variant: PlayStoreStagingRelease DexTask is null
Variant: PlayStorePreviewRelease DexTask is null
Variant: HockeyAppRelease DexTask is null
Variant: LocalServerRelease DexTask is null
Variant: PlayStoreProductionRelease DexTask is null
So how can this be?
I'd recommend using the Sentry Gradle integration (Gradle plugin) which is described here https://docs.sentry.io/platforms/android/#gradle-integration
The official Android Gradle plugin changed its task names over versions, Gradle version also affects those code snippets.
Google also replaced Proguard with R8 and it also affected those code snippets.
Is there a reason why not using the Sentry Gradle integration? if so, We'll be looking into updating them.
Thanks.
java.util.ArrayList.multiply() hints for that * in front of the [ ] list, which looks strange to me. Try removing the *[ ], only keeping List<String> (there's no ArrayList expected, to begin with):
commandLine "sentry-cli", "upload-proguard", "--write-properties", "${project.rootDir.toPath()}/app/build/intermediates/assets/${variant.dirName}/sentry-debug-meta.properties", variant.getMappingFile(), "--no-upload"
You'd have to look up how your tasks are actually being called, but it should be something alike:
def r8Task = project.tasks.findByName("transformClassesAndResourcesWithR8For${variantName}")
def d8Task = project.tasks.findByName("transformClassesWithDexBuilderFor${variantName}")
With a null check, because not every variant might have minifyEnabled true set:
if(r8Task != null) {
d8Task.dependsOn task
task.dependsOn r8Task
}
Maybe even a previous null check is required, because variant.getMappingFile() needs R8.
And that some flavors have no D8 task might be based upon the absence of code (nothing to do).
Here's a summary of the steps that I followed for integrating Sentry with my Android app. These steps are to ensure the sentry gradle plugin works as expected and automatically uploads the proguard mapping files, without you having to worry about uploading using cli. I assume you would have setup the Sentry SDK as described here:
https://docs.sentry.io/platforms/android/#integrating-the-sdk
Ensure you have Android Studio gradle plugin 3.5.0 (Not 3.6.x, that seems to break the sentry plugin. I observed that the sentry proguard or native symbol upload tasks are not configured or executed at all). This value should be in your root project's build.gradle in dependencies block
Provide a sentry.properties file the root folder of your project. The sentry.properties file should have the following values at minimum:
defaults.project=your_sentry_project_name
defaults.org=your_sentry_org_name
auth.token=sentry_project_auth_token
You can get info about generating auth tokens here: https://sentry.io/settings/account/api/auth-tokens/
(Optional: If you have build flavors) In my case, I have different flavors for my app. So, I had to put the sentry.properties inside my flavor specific folder in /app/src/ folder. Then, I wrote a gradle task to copy the flavor specific sentry.properties file into the project's root folder during gradle's configuration phase. Example:
task copySentryPropertiesTask {
if (getBuildFlavor() != null && !getBuildFlavor().isEmpty()) {
println("Copying Sentry properties file: ${getBuildFlavor()}")
copy {
from "src/${getBuildFlavor()}/"
include "sentry.properties"
into "../"
}
}
}
def getBuildFlavor() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
Pattern pattern;
if (tskReqStr.contains("assemble"))
pattern = Pattern.compile("assemble(\\w+)(Release|Debug)")
else
pattern = Pattern.compile("generate(\\w+)(Release|Debug)")
Matcher matcher = pattern.matcher(tskReqStr)
if (matcher.find())
return matcher.group(1)
else {
println "NO MATCH FOUND"
return ""
}
}
Note 1: You can place this task in your app/build.gradle anywhere (I had placed it at the end).
Note 2: If you followed step 3 for build flavors, you can also add the root folder's sentry.properties in .gitignore. Since, it will be copied everytime you create a build.
Sentry should now be able to upload the proguard files for any release builds (or if you set minifyEnabled=true for any buildType).

Firebase 2.0 - how to deal with multiple flavors (environments) of an android app?

I have multiple flavors of my app. How should I set this up server side? My package names are:
com.example.app (production)
com.example.app.staging (staging)
com.example.app.fake (fake)
Should this be 3 separate projects in the firebase console?
Largely it will depend on how you want your project to work. You can set all three up in the same console, or you can set up as two or more different projects. Either option is valid.
Benefits of same project:
Share the same billing, quotas, permissions, and services (database, storage, FCM etc).
Environment which is the same as production.
Benefits of different projects:
No risk of overwriting production data or affecting production users.
If using multiple projects, you can take advantage of the build types support which will allow you to have different google-services.json files for different versions. If using one project, the same google-services.json will work for all the varieties.
Note: as CodyMace says in the comments - remember to re-download the JSON file each time you add an app!
There are things you can do to minimise risks in either case (e.g. have dev/ stage/ prod/ keys in your Database, and have similar structures underneath), but what makes sense for you is largely about tradeoffs.
If you're just starting out, I would suggest starting with one project while you're in development, and once you've launched consider moving your development environment to a separate project. Staging could go either way.
Note I didn't fully try this yet, but documenting it here to not lose it until I get to it.
One actually isn't forced to use the gradle plugin, which enforces you to have a firebase project for all of your flavors and build types.
This is poorly documented, but one can find a hint at the top of the documentation for FirebaseApp and some more at https://firebase.google.com/docs/configure/
Alternatively initializeApp(Context, FirebaseOptions) initializes the default app instance. This method should be invoked from Application. This is also necessary if it is used outside of the application's main process.
So, fetch the google-services.json as usual and from it take mobilesdk_app_id and current_key (under api_key), that should be all that's needed for Google Analytics tracking at least. With that information run the following in your app's Application subclass for the variants where you need it:
FirebaseOptions options = new FirebaseOptions.Builder()
.setProjectId("<project_id>")
.setApplicationId("<mobilesdk_app_id>")
.setApiKey("<current_key>")
.build();
FirebaseApp.initializeApp(this, options);
EDIT
Obsolete solution
in case you are wondering to how to use google-services.json for different firebase backends.
first place all relevant json files in separate folder like:
...src/dev/google-services.json
...src/prod/google-services.json
you need to move file when a build task is running to the root as per your flavour in app level gradle file
like this:
android{
...
def flavourName = getCurrentFlavor()
delete 'google-services.json'
clean
if (flavourName == "development") {
copy {
from 'src/dev/'
include '*.json'
into '.'
}
} else if (flavourName == "stagging") {
copy {
from 'src/dev/'
include '*.json'
into '.'
}
} else if (flavourName == "production") {
copy {
from 'src/prod/'
include '*.json'
into '.'
}
} else {
println("NA")
}
}
here getCurrentFlavor() is defined as->
def getCurrentFlavor() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
Pattern pattern
println tskReqStr
if (tskReqStr.contains("assemble"))
pattern = Pattern.compile("assemble(\\w+)(Release|Debug)")
else
pattern = Pattern.compile("generate(\\w+)(Release|Debug)")
Matcher matcher = pattern.matcher(tskReqStr)
if (matcher.find())
return matcher.group(1).toLowerCase()
else {
println "NO MATCH FOUND"
return ""
}
}

Custom Class Loading in Dalvik with Gradle (Android New Build System)

As per the introduction of Custom Class Loading in Dalvik by Fred Chung on the Android Developers Blog:
The Dalvik VM provides facilities for developers to perform custom
class loading. Instead of loading Dalvik executable (“dex”) files from
the default location, an application can load them from alternative
locations such as internal storage or over the network.
However, not many developers have the need to do custom class loading. But those who do and follow the instructions on that blog post, might have some problems mimicking the same behavior with Gradle, the new build system for Android introduced in Google I/O 2013.
How exactly one can adapt the new build system to perform the same intermediary steps as in the old (Ant based) build system?
My team and I recently reached the 64K method references in our app, which is the maximum number of supported in a dex file. To get around this limitation, we need to partition part of the program into multiple secondary dex files, and load them at runtime.
We followed the blog post mentioned in the question for the old, Ant based, build system and everything was working just fine. But we recently felt the need to move to the new build system, based on Gradle.
This answer does not intend to replace the full blog post with a complete example. Instead, it will simply explain how to use Gradle to tweak the build process and achieve the same thing. Please note that this is probably just one way of doing it and how we are currently doing it in our team. It doesn't necessarily mean it's the only way.
Our project is structured a little different and this example works as an individual Java project that will compile all the source code into .class files, assemble them into a single .dex file and to finish, package that single .dex file into a .jar file.
Let's start...
In the root build.gradle we have the following piece of code to define some defaults:
ext.androidSdkDir = System.env.ANDROID_HOME
if(androidSdkDir == null) {
Properties localProps = new Properties()
localProps.load(new FileInputStream(file('local.properties')))
ext.androidSdkDir = localProps['sdk.dir']
}
ext.buildToolsVersion = '18.0.1'
ext.compileSdkVersion = 18
We need the code above because although the example is an individual Java project, we still need to use components from the Android SDK. And we will also be needing some of the other properties later on... So, on the build.gradle of the main project, we have this dependency:
dependencies {
compile files("${androidSdkDir}/platforms/android-${compileSdkVersion}/android.jar")
}
We are also simplifying the source sets of this project, which might not be necessary for your project:
sourceSets {
main {
java.srcDirs = ['src']
}
}
Next, we change the default configuration of the build-in jar task to simply include the classes.dex file instead of all .class files:
configure(jar) {
include 'classes.dex'
}
Now we need to have new task that will actually assemble all .class files into a single .dex file. In our case, we also need to include the Protobuf library JAR into the .dex file. So I'm including that in the example here:
task dexClasses << {
String protobufJarPath = ''
String cmdExt = Os.isFamily(Os.FAMILY_WINDOWS) ? '.bat' : ''
configurations.compile.files.find {
if(it.name.startsWith('protobuf-java')) {
protobufJarPath = it.path
}
}
exec {
commandLine "${androidSdkDir}/build-tools/${buildToolsVersion}/dx${cmdExt}", '--dex',
"--output=${buildDir}/classes/main/classes.dex",
"${buildDir}/classes/main", "${protobufJarPath}"
}
}
Also, make sure you have the following import somewhere (usually at the top, of course) on your build.gradle file:
import org.apache.tools.ant.taskdefs.condition.Os
Now we must make the jar task depend on our dexClasses task, to make sure that our task is executed before the final .jar file is assembled. We do that with a simple line of code:
jar.dependsOn(dexClasses)
And we're done... Simply invoke Gradle with the usual assemble task and your final .jar file, ${buildDir}/libs/${archivesBaseName}.jar will contain a single classes.dex file (besides the MANIFEST.MF file). Just copy that into your app assets folder (you can always automate that with Gradle as we've done but that is out of scope of this question) and follow the rest of the blog post.
If you have any questions, just shout in the comments. I'll try to help to the best of my abilities.
The Android Studio Gradle plugin now provides native multidex support, which effectively solves the Android 65k method limit without having to manually load classes from a jar file, and thus makes Fred Chung's blog obsolete for that purpose. However, loading custom classes from a jar file at runtime in Android is still useful for the purpose of extensibility (e.g. making a plugin framework for your app), so I'll address that usage scenario below:
I have created a port of the original example app on Fred Chung's blog to Android Studio on my github page over here using the Android library plugin rather than the Java plugin. Instead of trying to modify the existing dex process to split up into two modules like in the blog, I've put the code which we want to go into the jar file into its own module, and added a custom task assembleExternalJar which dexes the necessary class files after the main assemble task has finished.
Here is relevant part of the build.gradle file for the library. If your library module has any dependencies which are not in the main project then you will probably need to modify this script to add them.
apply plugin: 'com.android.library'
// ... see github project for the full build.gradle file
// Define some tasks which are used in the build process
task copyClasses(type: Copy) { // Copy the assembled *.class files for only the current namespace into a new directory
// get directory for current namespace (PLUGIN_NAMESPACE = 'com.example.toastlib')
def namespacePath = PLUGIN_NAMESPACE.replaceAll("\\.","/")
// set source and destination directories
from "build/intermediates/classes/release/${namespacePath}/"
into "build/intermediates/dex/${namespacePath}/"
// exclude classes which don't have a corresponding .java entry in the source directory
def remExt = { name -> name.lastIndexOf('.').with {it != -1 ? name[0..<it] : name} }
eachFile {details ->
def thisFile = new File("${projectDir}/src/main/java/${namespacePath}/", remExt(details.name)+".java")
if (!(thisFile.exists())) {
details.exclude()
}
}
}
task assembleExternalJar << {
// Get the location of the Android SDK
ext.androidSdkDir = System.env.ANDROID_HOME
if(androidSdkDir == null) {
Properties localProps = new Properties()
localProps.load(new FileInputStream(file('local.properties')))
ext.androidSdkDir = localProps['sdk.dir']
}
// Make sure no existing jar file exists as this will cause dx to fail
new File("${buildDir}/intermediates/dex/${PLUGIN_NAMESPACE}.jar").delete();
// Use command line dx utility to convert *.class files into classes.dex inside jar archive
String cmdExt = Os.isFamily(Os.FAMILY_WINDOWS) ? '.bat' : ''
exec {
commandLine "${androidSdkDir}/build-tools/${BUILD_TOOLS_VERSION}/dx${cmdExt}", '--dex',
"--output=${buildDir}/intermediates/dex/${PLUGIN_NAMESPACE}.jar",
"${buildDir}/intermediates/dex/"
}
copyJarToOutputs.execute()
}
task copyJarToOutputs(type: Copy) {
// Copy the built jar archive to the outputs folder
from 'build/intermediates/dex/'
into 'build/outputs/'
include '*.jar'
}
// Set the dependencies of the build tasks so that assembleExternalJar does a complete build
copyClasses.dependsOn(assemble)
assembleExternalJar.dependsOn(copyClasses)
For more detailed information see the full source code for the sample app on my github.
See my answer over here. The key points are:
Use the additionalParameters property on the dynamically created dexCamelCase tasks to pass --multi-dex to dx and create multiple dex files.
Use the multidex class loader to use the multiple dex files.

How to shrink code - 65k method limit in dex

I have a rather large Android app that relies on many library projects. The Android compiler has a limitation of 65536 methods per .dex file and I am surpassing that number.
There are basically two paths you can choose (at least that I know of) when you hit the method limit.
1) Shrink your code
2) Build multiple dex files (see this blog post)
I looked into both and tried to find out what was causing my method count to go so high. The Google Drive API takes the biggest chunk with the Guava dependency at over 12,000. Total libs for Drive API v2 reach over 23,000!
My question I guess is, what do you think I should do? Should I remove Google Drive integration as a feature of my app? Is there a way to shrink the API down (yes, I use proguard)? Should I go the multiple dex route (which looks rather painful, especially dealing with third party APIs)?
It looks like Google has finally implementing a workaround/fix for surpassing the 65K method limit of dex files.
About the 65K Reference Limit
Android application (APK) files contain
executable bytecode files in the form of Dalvik Executable (DEX)
files, which contain the compiled code used to run your app. The
Dalvik Executable specification limits the total number of methods
that can be referenced within a single DEX file to 65,536, including
Android framework methods, library methods, and methods in your own
code. Getting past this limit requires that you configure your app
build process to generate more than one DEX file, known as a multidex
configuration.
Multidex support prior to Android 5.0
Versions of the platform prior to Android 5.0 use the Dalvik runtime
for executing app code. By default, Dalvik limits apps to a single
classes.dex bytecode file per APK. In order to get around this
limitation, you can use the multidex support library, which becomes
part of the primary DEX file of your app and then manages access to
the additional DEX files and the code they contain.
Multidex support for Android 5.0 and higher
Android 5.0 and higher uses a runtime called ART which natively
supports loading multiple dex files from application APK files. ART
performs pre-compilation at application install time which scans for
classes(..N).dex files and compiles them into a single .oat file for
execution by the Android device. For more information on the Android
5.0 runtime, see Introducing ART.
See: Building Apps with Over 65K Methods
Multidex Support Library
This library provides support for building
apps with multiple Dalvik Executable (DEX) files. Apps that reference
more than 65536 methods are required to use multidex configurations.
For more information about using multidex, see Building Apps with Over
65K Methods.
This library is located in the /extras/android/support/multidex/
directory after you download the Android Support Libraries. The
library does not contain user interface resources. To include it in
your application project, follow the instructions for Adding libraries
without resources.
The Gradle build script dependency identifier for this library is as
follows:
com.android.support:multidex:1.0.+ This dependency notation specifies
the release version 1.0.0 or higher.
You should still avoid hitting the 65K method limit by actively using proguard and reviewing your dependencies.
you can use the multidex support library for that, To enable multidex
1) include it in dependencies:
dependencies {
...
compile 'com.android.support:multidex:1.0.0'
}
2) Enable it in your app:
defaultConfig {
...
minSdkVersion 14
targetSdkVersion 21
....
multiDexEnabled true
}
3) if you have a application class for your app then Override the attachBaseContext method like this:
package ....;
...
import android.support.multidex.MultiDex;
public class MyApplication extends Application {
....
#Override
protected void attachBaseContext(Context context) {
super.attachBaseContext(context);
MultiDex.install(this);
}
}
4) if you don't have a application class for your application then register android.support.multidex.MultiDexApplication as your application in your manifest file. like this:
<application
...
android:name="android.support.multidex.MultiDexApplication">
...
</application>
and it should work fine!
Play Services 6.5+ helps:
http://android-developers.blogspot.com/2014/12/google-play-services-and-dex-method.html
"Starting with version 6.5, of Google Play services, you’ll be able to
pick from a number of individual APIs, and you can see"
...
"this will transitively include the ‘base’ libraries, which are used
across all APIs."
This is good news, for a simple game for example you probably only need the base, games and maybe drive.
"The complete list of API names is below. More details can be found on
the Android Developer site.:
com.google.android.gms:play-services-base:6.5.87
com.google.android.gms:play-services-ads:6.5.87
com.google.android.gms:play-services-appindexing:6.5.87
com.google.android.gms:play-services-maps:6.5.87
com.google.android.gms:play-services-location:6.5.87
com.google.android.gms:play-services-fitness:6.5.87
com.google.android.gms:play-services-panorama:6.5.87
com.google.android.gms:play-services-drive:6.5.87
com.google.android.gms:play-services-games:6.5.87
com.google.android.gms:play-services-wallet:6.5.87
com.google.android.gms:play-services-identity:6.5.87
com.google.android.gms:play-services-cast:6.5.87
com.google.android.gms:play-services-plus:6.5.87
com.google.android.gms:play-services-appstate:6.5.87
com.google.android.gms:play-services-wearable:6.5.87
com.google.android.gms:play-services-all-wear:6.5.87
In versions of Google Play services prior to 6.5, you had to compile the entire package of APIs into your app. In some cases, doing so made it more difficult to keep the number of methods in your app (including framework APIs, library methods, and your own code) under the 65,536 limit.
From version 6.5, you can instead selectively compile Google Play service APIs into your app. For example, to include only the Google Fit and Android Wear APIs, replace the following line in your build.gradle file:
compile 'com.google.android.gms:play-services:6.5.87'
with these lines:
compile 'com.google.android.gms:play-services-fitness:6.5.87'
compile 'com.google.android.gms:play-services-wearable:6.5.87'
for more reference, you can click here
Use proguard to lighten your apk as methods that are unused will not be in your final build. Double check you have following in your proguard config file to use proguard with guava (my apologies if you already have this, it wasn't known at time of writing) :
# Guava exclusions (http://code.google.com/p/guava-libraries/wiki/UsingProGuardWithGuava)
-dontwarn sun.misc.Unsafe
-dontwarn com.google.common.collect.MinMaxPriorityQueue
-keepclasseswithmembers public class * {
public static void main(java.lang.String[]);
}
# Guava depends on the annotation and inject packages for its annotations, keep them both
-keep public class javax.annotation.**
-keep public class javax.inject.**
In addition, if you are using ActionbarSherlock, switching to the v7 appcompat support library will also reduce your method count by a lot (based on personal experience). Instructions are located :
http://developer.android.com/tools/support-library/features.html#v7-appcompat Actionbar
http://developer.android.com/tools/support-library/setup.html#libs-with-res
You could use Jar Jar Links to shrink huge external libraries like Google Play Services (16K methods!)
In your case you will just rip everything from Google Play Services jar except common internal and drive sub-packages.
For Eclipse users not using Gradle, there are tools that will break down the Google Play Services jar and rebuild it with only the parts you want.
I use strip_play_services.sh by dextorer.
It can be difficult to know exactly which services to include because there are some internal dependencies but you can start small and add to the configuration if it turns out that needed things are missing.
I think that in the long run breaking your app in multiple dex would be the best way.
Multi-dex support is going to be the official solution for this issue. See my answer here for the details.
If not to use multidex which making build process very slow.
You can do the following.
As yahska mentioned use specific google play service library.
For most cases only this is needed.
compile 'com.google.android.gms:play-services-base:6.5.+'
Here is all available packages Selectively compiling APIs into your executable
If this will be not enough you can use gradle script. Put this code in file 'strip_play_services.gradle'
def toCamelCase(String string) {
String result = ""
string.findAll("[^\\W]+") { String word ->
result += word.capitalize()
}
return result
}
afterEvaluate { project ->
Configuration runtimeConfiguration = project.configurations.getByName('compile')
println runtimeConfiguration
ResolutionResult resolution = runtimeConfiguration.incoming.resolutionResult
// Forces resolve of configuration
ModuleVersionIdentifier module = resolution.getAllComponents().find {
it.moduleVersion.name.equals("play-services")
}.moduleVersion
def playServicesLibName = toCamelCase("${module.group} ${module.name} ${module.version}")
String prepareTaskName = "prepare${playServicesLibName}Library"
File playServiceRootFolder = project.tasks.find { it.name.equals(prepareTaskName) }.explodedDir
def tmpDir = new File(project.buildDir, 'intermediates/tmp')
tmpDir.mkdirs()
def libFile = new File(tmpDir, "${playServicesLibName}.marker")
def strippedClassFileName = "${playServicesLibName}.jar"
def classesStrippedJar = new File(tmpDir, strippedClassFileName)
def packageToExclude = ["com/google/ads/**",
"com/google/android/gms/actions/**",
"com/google/android/gms/ads/**",
// "com/google/android/gms/analytics/**",
"com/google/android/gms/appindexing/**",
"com/google/android/gms/appstate/**",
"com/google/android/gms/auth/**",
"com/google/android/gms/cast/**",
"com/google/android/gms/drive/**",
"com/google/android/gms/fitness/**",
"com/google/android/gms/games/**",
"com/google/android/gms/gcm/**",
"com/google/android/gms/identity/**",
"com/google/android/gms/location/**",
"com/google/android/gms/maps/**",
"com/google/android/gms/panorama/**",
"com/google/android/gms/plus/**",
"com/google/android/gms/security/**",
"com/google/android/gms/tagmanager/**",
"com/google/android/gms/wallet/**",
"com/google/android/gms/wearable/**"]
Task stripPlayServices = project.tasks.create(name: 'stripPlayServices', group: "Strip") {
inputs.files new File(playServiceRootFolder, "classes.jar")
outputs.dir playServiceRootFolder
description 'Strip useless packages from Google Play Services library to avoid reaching dex limit'
doLast {
def packageExcludesAsString = packageToExclude.join(",")
if (libFile.exists()
&& libFile.text == packageExcludesAsString
&& classesStrippedJar.exists()) {
println "Play services already stripped"
copy {
from(file(classesStrippedJar))
into(file(playServiceRootFolder))
rename { fileName ->
fileName = "classes.jar"
}
}
} else {
copy {
from(file(new File(playServiceRootFolder, "classes.jar")))
into(file(playServiceRootFolder))
rename { fileName ->
fileName = "classes_orig.jar"
}
}
tasks.create(name: "stripPlayServices" + module.version, type: Jar) {
destinationDir = playServiceRootFolder
archiveName = "classes.jar"
from(zipTree(new File(playServiceRootFolder, "classes_orig.jar"))) {
exclude packageToExclude
}
}.execute()
delete file(new File(playServiceRootFolder, "classes_orig.jar"))
copy {
from(file(new File(playServiceRootFolder, "classes.jar")))
into(file(tmpDir))
rename { fileName ->
fileName = strippedClassFileName
}
}
libFile.text = packageExcludesAsString
}
}
}
project.tasks.findAll {
it.name.startsWith('prepare') && it.name.endsWith('Dependencies')
}.each { Task task ->
task.dependsOn stripPlayServices
}
project.tasks.findAll { it.name.contains(prepareTaskName) }.each { Task task ->
stripPlayServices.mustRunAfter task
}
}
Then apply this script in your build.gradle, like this
apply plugin: 'com.android.application'
apply from: 'strip_play_services.gradle'
If using Google Play Services, you may know that it adds 20k+ methods. As already mentioned, Android Studio has the option for modular inclusion of specific services, but users stuck with Eclipse have to take modularisation into their own hands :(
Fortunately there's a shell script that makes the job fairly easy. Just extract to the google play services jar directory, edit the supplied .conf file as needed and execute the shell script.
An example of its use is here.
If using Google Play Services, you may know that it adds 20k+ methods. As already mentioned, Android Studio has the option for modular inclusion of specific services, but users stuck with Eclipse have to take modularisation into their own hands :(
Fortunately there's a shell script that makes the job fairly easy. Just extract to the google play services jar directory, edit the supplied .conf file as needed and execute the shell script.
An example of its use is here.
Just like he said, I replaces compile 'com.google.android.gms:play-services:9.0.0' just with the libraries that I needed and it worked.

Categories

Resources