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

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.

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).

Stripping data files from ICU4J when used in a Library

I have an internal library that is included in different android apps.
This library depends on ICU4J. This means we add ~10MB to the final APK.
The lib uses a subset of ICU4J so I would like to remove all the non-necessary data files.
From the ICU documentation:
Currently ICU4J provides no tool for revealing these dependencies between data files, so trimming the data directly in the ICU4J project is a hit-or-miss affair. The key point when you remove data is to make sure to remove all dependencies on that data as well.
I'd like to remove the data files when the app is built.
One question on StackOverflow is related: Exclude specific resources from an aar dep. Unfortunately, the exploded-aar directory does not exist anymore.
Do you know at which step I can remove the files from the ICU4J dependency ? Here's what I've tried to remove the cjdict.dict file:
tasks.create("excludeTask") << {
["java/com/ibm/icu/impl/data/icudt60b/brkitr/cjdict.dict"]
.collect { "${getGroup()}/${getName()}/${getVersion()}/${it}"}
.each {
// Question 2. From which dir should I remove the files?
File file = file("${buildDir}/intermediates/exploded-aar/${it}")
println("Excluding file " + file)
if (file.exists()) {
file.delete();
}
}
}
tasks.whenTaskAdded({
// Question 1. Before which task should I inject my excludeTask?
if (it.name.matches(/^transformClassesAndResources.*$/)) {
it.dependsOn excludeTask
}
})
Question 1: Before which task should I inject my excludeTask?
Question 2: From which dir should I remove the files?
I know that ICU4J can be used in Android 7.0 without all this overhead but I'd like to make the APK as light as I can for older devices.
Proguard/shrinking resources won't work for this (unless I miss something?)
A better mechanism for slicing ICU locale data is a common feature request, so we are working on rolling this out in an upcoming ICU release (either 63 or 64 depending on how things go). In the mean time, freel free to contribute to our design doc:
https://docs.google.com/document/d/1Lt9sHy7VbMLA2KbbEpg-TgNAI8bqtetwM767enWjcUg/edit#

Android Studio Gradle - JavaExec classpath configuration - Java Reflection - class access

Im currently working on an android project where i have to process .java-files to possibly generate another .java-files which should then be compiled and packed into the .apk-file.
Lets assume i have 2 files which will be processed by my library, FILE_A.java and FILE_B.java.
Now i need to access these files within my library via reflection, e.g. with:
Class.forName("com.test.entities.FILE_A");
Class.forName("com.test.entities.FILE_B");
The problem is that i'm not able to access the class files, i think because of the missing classpath configuration. Currently i use this task to call my .jar-file:
task (mytask, type: org.gradle.api.tasks.JavaExec) {
classpath(files('libs/myjar.jar'))
main('com.test.TestMain')
}
preBuild.dependsOn mytask
I found some ressources on the web, but they all don't work.
I tried to add the following to the classpath:
sourceSets.main.runtimeClasspath (main is unknown)
android.sourceSets.main.runtimeClasspath (runtimeClasspath is unkown).
So how can i access the class files in my library?
Try this
task execute(dependsOn: ['compileReleaseJavaWithJavac'], type:JavaExec) {
main = 'com.geniml.Main'
classpath(files('build/intermediates/classes/release',"${android.getSdkDirectory().getAbsolutePath() + '/platforms/' + android.compileSdkVersion + '/android.jar'}"))
}
android gralde is 1.5.
As to build dir, you can use rootProject.getBuildDir(). But normally build dir is a convention. A static way is ok.

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