In my React Native app, I have two flavours. Each flavour has some image and video assets that are only used in that flavour.
To reduce the size of the APK, I would like to exclude unnecessary assets prior to, or during build time.
What would be the best way to accomplish this?
I did try using aaptOptions, along the lines of the below, but this never worked for me:
aaptOptions {
'/folder:*.jpg:*.png'
ignoreAssetsPattern "!/"+projectName+":*.mp4:"
}
I previously had the following task at the bottom of my build.gradle file:
task deleteOtherFlavourAssets(type: Delete) {
description = 'Deleting non $projectName assets'
delete fileTree("src").matching {
exclude "**/app_assets_$projectName**"
include "**/app_assets_**"
}
}
preBuild.dependsOn deleteOtherFlavourAssets
This did the job for a while, however it stopped working a few months ago. I've not been able to get it working again.
I should mention that when running the above task as a copy (instead of delete), the right files are duplicated into this directory.
task otherFlavourAssets(type: Copy) {
description = 'Copying non $projectName assets to temp directory'
copy {
from fileTree("src").matching {
exclude "**/app_assets_$projectName**"
include "**/app_assets_**"
}
into 'tempAssetStore'
}
}
There are no other relevant custom tasks. So it is very possible that a subsequent React Native build task is copying/merging the superfluous assets back in, or perhaps it's a cache issue.
I don't know whether Gradle changed in this context or whether it could be a problem at all, but your filter patterns look a little weird to me. ** is normally used to describe zero or more directory levels and I never saw patterns that use ** in combination with a directory name. Could you try the patterns below instead?
task deleteOtherFlavourAssets(type: Delete) {
description = 'Deleting non $projectName assets'
delete fileTree("src").matching {
exclude "**/app_assets_$projectName*/**"
include "**/app_assets_*/**"
}
}
Related
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#
I have an Android Studio project which uses NDK and CMake and externalNativeBuild. To reduce packet size I have several flavors for different texture compression formats. There are no code changes, i.e. all the resulting APKs are using exactly the same code.
productFlavors {
ETC2 {
manifestPlaceholders = [supportedTexture: "GL_OES_compressed_ETC2_RGB8_texture"]
}
DXT {
manifestPlaceholders = [supportedTexture: "GL_EXT_texture_compression_dxt1"]
}
ATC {
manifestPlaceholders = [supportedTexture: "GL_AMD_compressed_ATC_texture"]
}
//...and list goes on...
}
What this means in practice is that I have a Copy task which checks the current flavor and copies the correct texture pack into the APK. The getCurrentFlavor() function is copied from How to get current flavor in gradle:
task copyTexSD(type: Copy) {
def currentFlavor = getCurrentFlavor()
if(currentFlavor == "etc2") {
from 'bin/tex/ETC2.bin'
}
else if(currentFlavor == "dxt") {
from 'bin/tex/DXT.bin'
}
else if(currentFlavor == "atc") {
from 'bin/tex/ATC.bin'
}
//...
into 'src/main/assets/tex'
}
To build averything I use the following batch command:
call gradlew clean
call gradlew assembleETC2Release
call gradlew assembleDXTRelease
call gradlew assembleATCRelease
This works otherwise well, but for some reason the texture packets which are copied to previous APKs are also included in the subsequent APKs like this:
app-ETC2-release.apk contains only ETC2.bin file
app-DXT-release.apk contains DXT.bin and ETC2.bin
app-ATC-release.apk contains ATC.bin, DXT.bin and ETC2.bin
Why the build process includes assets from previous Gradle task? How can I make the build process to have only one texture file per APK?
Somehow I had a wrong assumption that each gradle task would be independent. Of course all the file(s) that have been copied in the assets folder in previous tasks are still there if they are not explicitly deleted.
So, there seems to be two possibilities to get this to work:
1) Modify the batch file to delete data from the texture folder before calling next gradle task.
OR
2) Create delete task in gradle file which runs before copy task. Examples can be found here: Gradle - Delete files with certain extension .
I am trying to do a very simple thing. As gradle removes all files in the build dir when cleaning I want to move the apks somewhere else when creating release versions. So I added a copy task into the chain and I set it to be the last. Anything I tried did't work. So I simplified it and added some logging to make a point. I think it just doesn't work.
Using two variables, I can check that at task definition time and execution time the input and output paths are valid. I can also check that the task is executed. I put some more files in the input directory to make sure there is also something there in any case. This is the script:
def buildPath
def outPath
task copyApks(type: Copy) {
buildPath = "$buildDir\\outputs\\apk"
outPath ="$buildDir\\outputs\\apk2"
logger.error("Source Folder is $buildPath")
logger.error("Destination Folder is $outPath")
from buildPath
into outPath
}
assembleRelease.doLast {
android.applicationVariants.all { variant ->
println "Variant $variant.name"
logger.error("Source Folder is $buildPath")
logger.error("Destination Folder is $outPath")
copyApks
}
}
And this is the output, where one can see that the paths are correct (they exist and are valid) both at definition and execution time. Also one can see that the task is executed:
What is wrong?
Executing external task 'assembleRelease'...
Parallel execution with configuration on demand is an incubating feature.
Source Folder is C:\Users\Administrator\Projects\Gradle\MB6\app\build\outputs\apk
Destination Folder is C:\Users\Administrator\Projects\Gradle\MB6\app\build\outputs\apk2
................
some other gradle logs
................
:app:assembleRelease
Variant debug
Source Folder is C:\Users\Administrator\Projects\Gradle\MB6\app\build\outputs\apk
Destination Folder is C:\Users\Administrator\Projects\Gradle\MB6\app\build\outputs\apk2
Variant release
Source Folder is C:\Users\Administrator\Projects\Gradle\MB6\app\build\outputs\apk
Destination Folder is C:\Users\Administrator\Projects\Gradle\MB6\app\build\outputs\apk2
BUILD SUCCESSFUL
First of all, you have to know, that just adding the task name into your closure, in your case it's copyApks, doesn't really mean that this task should be executed. It's just the same, as you specified a variable, but do nothing with it.
And one more, note, the both variants paths are the same, that means that you are trying to copy tha same files twice. Actually, that not the only reason, you have to understand, that your copy task is configured yet in the configuration phase, when you are trying to call it during the execution phase, so you can't change it's from and into parameters, and this task will always behave the same.
If you want to call some tasks one after another, you have a number of choices, like task dependencies, task finalization or task ordering. You can read about it in the official user guide. There is a way to call some task like a method call, but this is a very poor solution and you have to avoid using it.
So, if you want to call a copy task, then you may try solution like this
assembleRelease.finalizedBy copyApks
This will call a copy task always every time assembling is done.
We have a gradle task that will automatically generate codes for us before building. See the following as an example,
task djinniTask(type: org.gradle.api.tasks.Exec) {
commandLine 'sh', './Djinni/run_djinni.sh'
}
assembleDebug.dependsOn djinniTask
Basically, the above run_djinni.sh is using a library djinni to generate JNI codes. The above works fine except that it will run this script every time we build even if we didn't update the script file, which is obviously not very efficient. We did a bit of research and found 17.9. Skipping tasks that are up-to-date. And as a result, the following works fine. It will skip this task if we didn't modify run_djinni.sh.
task transform {
ext.srcFile = file('./Djinni/run_djinni.sh')
ext.destDir = new File(buildDir, 'generated')
doLast {
commandLine 'sh', './Djinni/run_djinni.sh'
}
}
Now the problem is, the run_djinni.sh is not the only script file that we have. The project is big and we multiple scripts files like: run_foo_djinni.sh, run_bar_djinni.sh and etc. run_djinni.sh will call each of the other scripts. So is there a way to declare the inputs of a gradle task as multiple files, for example, in our case, every files that is under the Djinni folder?
Ok, according to gradle DSL you can define multiple inputs:
task transform {
inputs.files('file path', 'another file path')
}
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.