Skip to content

Commit 2457a00

Browse files
authored
Merge pull request #177 from OleksandrKucherenko/android-performance-improvements
Android performance improvements
2 parents f32408d + b7e9d6e commit 2457a00

File tree

15 files changed

+739
-225
lines changed

15 files changed

+739
-225
lines changed

.gitignore

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,18 @@ npm-debug.log
2929

3030
# android
3131
#
32-
android/build/
33-
android/.gradle/
34-
android/.idea/
35-
android/*.iml
36-
android/gradle/
32+
.vscode/
33+
.settings/
34+
android/bin
35+
android/gradle/wrapper
3736
android/gradlew
3837
android/gradlew.bat
3938
android/local.properties
39+
*.iml
40+
.gradle
41+
/local.properties
42+
.idea/
43+
captures/
44+
.externalNativeBuild
45+
.project
46+

README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,81 @@ Model tested: iPhone 6 (iOS), Nexus 5 (Android).
174174
3. Component itself lacks platform support.
175175
4. But you can just use the react-native-maps snapshot function: https://github.com/airbnb/react-native-maps#take-snapshot-of-map
176176

177+
## Performance Optimization
178+
179+
During profiling captured several things that influence on performance:
180+
1) (de-)allocation of memory for bitmap
181+
2) (de-)allocation of memory for Base64 output buffer
182+
3) compression of bitmap to different image formats: PNG, JPG
183+
184+
To solve that in code introduced several new approaches:
185+
- reusable images, that reduce load on GC;
186+
- reusable arrays/buffers that also reduce load on GC;
187+
- RAW image format for avoiding expensive compression;
188+
- ZIP deflate compression for RAW data, that works faster in compare to `Bitmap.compress`
189+
190+
more details and code snippet are below.
191+
192+
### RAW Images
193+
194+
Introduced a new image format RAW. it correspond a ARGB array of pixels.
195+
196+
Advantages:
197+
- no compression, so its supper quick. Screenshot taking is less than 16ms;
198+
199+
RAW format supported for `zip-base64`, `base64` and `tmpfile` result types.
200+
201+
RAW file on disk saved in format: `${width}:${height}|${base64}` string.
202+
203+
### zip-base64
204+
205+
In compare to BASE64 result string this format fast try to apply zip/deflate compression on screenshot results
206+
and only after that convert results to base64 string. In combination zip-base64 + raw we got a super fast
207+
approach for capturing screen views and deliver them to the react side.
208+
209+
### How to work with zip-base64 and RAW format?
210+
211+
```js
212+
const fs = require('fs')
213+
const zlib = require('zlib')
214+
const PNG = require('pngjs').PNG
215+
const Buffer = require('buffer').Buffer
216+
217+
const format = Platform.OS === 'android' ? 'raw' : 'png'
218+
const result = Platform.OS === 'android' ? 'zip-base64' : 'base64'
219+
220+
captureRef(this.ref, { result, format }).then(data => {
221+
// expected pattern 'width:height|', example: '1080:1731|'
222+
const resolution = /^(\d+):(\d+)\|/g.exec(data)
223+
const width = (resolution || ['', 0, 0])[1]
224+
const height = (resolution || ['', 0, 0])[2]
225+
const base64 = data.substr((resolution || [''])[0].length || 0)
226+
227+
// convert from base64 to Buffer
228+
const buffer = Buffer.from(base64, 'base64')
229+
// un-compress data
230+
const inflated = zlib.inflateSync(buffer)
231+
// compose PNG
232+
const png = new PNG({ width, height })
233+
png.data = inflated
234+
const pngData = PNG.sync.write(png)
235+
// save composed PNG
236+
fs.writeFileSync(output, pngData)
237+
})
238+
```
239+
240+
Keep in mind that packaging PNG data is a CPU consuming operation as a `zlib.inflate`.
241+
242+
Hint: use `process.fork()` approach for converting raw data into PNGs.
243+
244+
> Note: code is tested in large commercial project.
245+
246+
> Note #2: Don't forget to add packages into your project:
247+
> ```js
248+
> yarn add pngjs
249+
> yarn add zlib
250+
> ```
251+
177252
## Troubleshooting / FAQ
178253
179254
### Saving to a file?

android/build.gradle

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,48 @@
11
buildscript {
2-
repositories {
3-
jcenter()
4-
}
5-
6-
dependencies {
7-
classpath 'com.android.tools.build:gradle:2.3.0'
2+
/* In case of submodule usage, do not try to apply own repositories and plugins,
3+
root project is responsible for that. */
4+
if (rootProject.buildDir == project.buildDir) {
5+
repositories {
6+
google()
7+
jcenter()
8+
}
9+
dependencies {
10+
classpath 'com.android.tools.build:gradle:2.3.0'
11+
}
812
}
913
}
1014

1115
apply plugin: 'com.android.library'
1216

1317
android {
14-
compileSdkVersion 26
15-
buildToolsVersion "26.0.1"
18+
compileSdkVersion 27
19+
buildToolsVersion "28.0.3"
1620

1721
defaultConfig {
1822
minSdkVersion 16
19-
targetSdkVersion 26
23+
targetSdkVersion 27
24+
2025
versionCode 1
2126
versionName "1.0"
2227
}
28+
2329
lintOptions {
2430
abortOnError false
2531
}
2632
}
2733

28-
allprojects {
29-
repositories {
30-
mavenLocal()
31-
jcenter()
32-
maven {
33-
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
34-
url "$rootDir/../node_modules/react-native/android"
35-
}
34+
repositories {
35+
google()
36+
jcenter()
37+
mavenLocal()
38+
maven {
39+
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
40+
url "$rootDir/../node_modules/react-native/android"
3641
}
3742
}
3843

3944
dependencies {
40-
compile 'com.facebook.react:react-native:+'
45+
implementation 'com.android.support:support-v4:27.+'
46+
47+
api 'com.facebook.react:react-native:+'
4148
}
Lines changed: 77 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,36 @@
11

22
package fr.greweb.reactnativeviewshot;
33

4+
import android.app.Activity;
45
import android.content.Context;
5-
import android.graphics.Bitmap;
66
import android.net.Uri;
77
import android.os.AsyncTask;
8-
import android.os.Environment;
8+
import android.support.annotation.NonNull;
99
import android.util.DisplayMetrics;
10-
import android.view.View;
11-
12-
import com.facebook.react.bridge.ReactApplicationContext;
13-
import com.facebook.react.bridge.ReactContextBaseJavaModule;
14-
import com.facebook.react.bridge.ReactMethod;
10+
import android.util.Log;
1511

1612
import com.facebook.react.bridge.GuardedAsyncTask;
17-
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
1813
import com.facebook.react.bridge.Promise;
14+
import com.facebook.react.bridge.ReactApplicationContext;
1915
import com.facebook.react.bridge.ReactContext;
16+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
17+
import com.facebook.react.bridge.ReactMethod;
2018
import com.facebook.react.bridge.ReadableMap;
21-
import com.facebook.react.uimanager.UIBlock;
2219
import com.facebook.react.uimanager.UIManagerModule;
2320

2421
import java.io.File;
2522
import java.io.FilenameFilter;
2623
import java.io.IOException;
2724
import java.util.Collections;
28-
import java.util.HashMap;
2925
import java.util.Map;
3026

27+
import fr.greweb.reactnativeviewshot.ViewShot.Formats;
28+
import fr.greweb.reactnativeviewshot.ViewShot.Results;
29+
3130
public class RNViewShotModule extends ReactContextBaseJavaModule {
3231

32+
public static final String RNVIEW_SHOT = "RNViewShot";
33+
3334
private final ReactApplicationContext reactContext;
3435

3536
public RNViewShotModule(ReactApplicationContext reactContext) {
@@ -39,7 +40,7 @@ public RNViewShotModule(ReactApplicationContext reactContext) {
3940

4041
@Override
4142
public String getName() {
42-
return "RNViewShot";
43+
return RNVIEW_SHOT;
4344
}
4445

4546
@Override
@@ -67,30 +68,40 @@ public void releaseCapture(String uri) {
6768

6869
@ReactMethod
6970
public void captureRef(int tag, ReadableMap options, Promise promise) {
70-
ReactApplicationContext context = getReactApplicationContext();
71-
String format = options.getString("format");
72-
Bitmap.CompressFormat compressFormat =
73-
format.equals("jpg")
74-
? Bitmap.CompressFormat.JPEG
75-
: format.equals("webm")
76-
? Bitmap.CompressFormat.WEBP
77-
: Bitmap.CompressFormat.PNG;
78-
double quality = options.getDouble("quality");
79-
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
80-
Integer width = options.hasKey("width") ? (int)(displayMetrics.density * options.getDouble("width")) : null;
81-
Integer height = options.hasKey("height") ? (int)(displayMetrics.density * options.getDouble("height")) : null;
82-
String result = options.getString("result");
83-
Boolean snapshotContentContainer = options.getBoolean("snapshotContentContainer");
71+
final ReactApplicationContext context = getReactApplicationContext();
72+
final DisplayMetrics dm = context.getResources().getDisplayMetrics();
73+
74+
final String extension = options.getString("format");
75+
final int imageFormat = "jpg".equals(extension)
76+
? Formats.JPEG
77+
: "webm".equals(extension)
78+
? Formats.WEBP
79+
: "raw".equals(extension)
80+
? Formats.RAW
81+
: Formats.PNG;
82+
83+
final double quality = options.getDouble("quality");
84+
final Integer scaleWidth = options.hasKey("width") ? (int) (dm.density * options.getDouble("width")) : null;
85+
final Integer scaleHeight = options.hasKey("height") ? (int) (dm.density * options.getDouble("height")) : null;
86+
final String resultStreamFormat = options.getString("result");
87+
final Boolean snapshotContentContainer = options.getBoolean("snapshotContentContainer");
88+
8489
try {
85-
File file = null;
86-
if ("tmpfile".equals(result)) {
87-
file = createTempFile(getReactApplicationContext(), format);
90+
File outputFile = null;
91+
if (Results.TEMP_FILE.equals(resultStreamFormat)) {
92+
outputFile = createTempFile(getReactApplicationContext(), extension);
8893
}
89-
UIManagerModule uiManager = this.reactContext.getNativeModule(UIManagerModule.class);
90-
uiManager.addUIBlock(new ViewShot(tag, format, compressFormat, quality, width, height, file, result, snapshotContentContainer,reactContext, getCurrentActivity(), promise));
91-
}
92-
catch (Exception e) {
93-
promise.reject(ViewShot.ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag "+tag);
94+
95+
final Activity activity = getCurrentActivity();
96+
final UIManagerModule uiManager = this.reactContext.getNativeModule(UIManagerModule.class);
97+
98+
uiManager.addUIBlock(new ViewShot(
99+
tag, extension, imageFormat, quality,
100+
scaleWidth, scaleHeight, outputFile, resultStreamFormat,
101+
snapshotContentContainer, reactContext, activity, promise)
102+
);
103+
} catch (final Throwable ignored) {
104+
promise.reject(ViewShot.ERROR_UNABLE_TO_SNAPSHOT, "Failed to snapshot view tag " + tag);
94105
}
95106
}
96107

@@ -106,34 +117,41 @@ public void captureScreen(ReadableMap options, Promise promise) {
106117
* image files. This is run when the catalyst instance is being destroyed (i.e. app is shutting
107118
* down) and when the module is instantiated, to handle the case where the app crashed.
108119
*/
109-
private static class CleanTask extends GuardedAsyncTask<Void, Void> {
110-
private final Context mContext;
120+
private static class CleanTask extends GuardedAsyncTask<Void, Void> implements FilenameFilter {
121+
private final File cacheDir;
122+
private final File externalCacheDir;
111123

112124
private CleanTask(ReactContext context) {
113125
super(context);
114-
mContext = context;
126+
127+
cacheDir = context.getCacheDir();
128+
externalCacheDir = context.getExternalCacheDir();
115129
}
116130

117131
@Override
118132
protected void doInBackgroundGuarded(Void... params) {
119-
cleanDirectory(mContext.getCacheDir());
120-
File externalCacheDir = mContext.getExternalCacheDir();
133+
if (null != cacheDir) {
134+
cleanDirectory(cacheDir);
135+
}
136+
121137
if (externalCacheDir != null) {
122138
cleanDirectory(externalCacheDir);
123139
}
124140
}
125141

126-
private void cleanDirectory(File directory) {
127-
File[] toDelete = directory.listFiles(
128-
new FilenameFilter() {
129-
@Override
130-
public boolean accept(File dir, String filename) {
131-
return filename.startsWith(TEMP_FILE_PREFIX);
132-
}
133-
});
142+
@Override
143+
public final boolean accept(File dir, String filename) {
144+
return filename.startsWith(TEMP_FILE_PREFIX);
145+
}
146+
147+
private void cleanDirectory(@NonNull final File directory) {
148+
final File[] toDelete = directory.listFiles(this);
149+
134150
if (toDelete != null) {
135-
for (File file: toDelete) {
136-
file.delete();
151+
for (File file : toDelete) {
152+
if (file.delete()) {
153+
Log.d(RNVIEW_SHOT, "deleted file: " + file.getAbsolutePath());
154+
}
137155
}
138156
}
139157
}
@@ -143,26 +161,26 @@ public boolean accept(File dir, String filename) {
143161
* Create a temporary file in the cache directory on either internal or external storage,
144162
* whichever is available and has more free space.
145163
*/
146-
private File createTempFile(Context context, String ext)
147-
throws IOException {
148-
File externalCacheDir = context.getExternalCacheDir();
149-
File internalCacheDir = context.getCacheDir();
150-
File cacheDir;
164+
@NonNull
165+
private File createTempFile(@NonNull final Context context, @NonNull final String ext) throws IOException {
166+
final File externalCacheDir = context.getExternalCacheDir();
167+
final File internalCacheDir = context.getCacheDir();
168+
final File cacheDir;
169+
151170
if (externalCacheDir == null && internalCacheDir == null) {
152171
throw new IOException("No cache directory available");
153172
}
173+
154174
if (externalCacheDir == null) {
155175
cacheDir = internalCacheDir;
156-
}
157-
else if (internalCacheDir == null) {
176+
} else if (internalCacheDir == null) {
158177
cacheDir = externalCacheDir;
159178
} else {
160179
cacheDir = externalCacheDir.getFreeSpace() > internalCacheDir.getFreeSpace() ?
161180
externalCacheDir : internalCacheDir;
162181
}
163-
String suffix = "." + ext;
164-
File tmpFile = File.createTempFile(TEMP_FILE_PREFIX, suffix, cacheDir);
165-
return tmpFile;
166-
}
167182

183+
final String suffix = "." + ext;
184+
return File.createTempFile(TEMP_FILE_PREFIX, suffix, cacheDir);
185+
}
168186
}

0 commit comments

Comments
 (0)