Skip to content

Commit f00e348

Browse files
tomekzawfacebook-github-bot
authored andcommitted
Fix support for blobs larger than 64 KB on Android (#31789)
Summary: Fixes #31774. This pull request resolves a problem related to accessing blobs greater than 64 KB on Android. When an object URL for such blob is passed as source of `<Image />` component, the image does not load. This issue was related to the fact that pipe buffer has a limited capacity of 65536 bytes (https://man7.org/linux/man-pages/man7/pipe.7.html, section "Pipe capacity"). If there is more bytes to be written than free space in the buffer left, the write operation blocks and waits until the content is read from the pipe. The current implementation of `BlobProvider.openFile` first creates a pipe, then writes the blob data to the pipe and finally returns the read side descriptor of the pipe. For blobs larger than 64 KB, the write operation will block forever, because there are no readers to empty the buffer. https://github.com/facebook/react-native/blob/41ecccefcf16ac8bcf858dd955af709eb20f7e4a/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobProvider.java#L86-L95 This pull request moves the write operation to a separate thread. The read side descriptor is returned immediately so that both writer and reader can work simultaneously. Reading from the pipe empties the buffer and allows the next chunks to be written. ## Changelog <!-- Help reviewers and the release process by writing your own changelog entry. For an example, see: https://github.com/facebook/react-native/wiki/Changelog --> [Android] [Fixed] - Fix support for blobs larger than 64 KB Pull Request resolved: #31789 Test Plan: A new example has been added to RN Tester app to verify if the new implementation properly loads the image of size 455 KB from a blob via object URL passed as image source. <img src="https://user-images.githubusercontent.com/20516055/123859163-9eba6d80-d924-11eb-8a09-2b1f353bb968.png" alt="Screenshot_1624996413" width="300" /> Reviewed By: ShikaSD Differential Revision: D29674273 Pulled By: yungsters fbshipit-source-id: e0ac3ec0a23690b05ab843061803f95f7666c0db
1 parent efd4daf commit f00e348

File tree

4 files changed

+108
-7
lines changed

4 files changed

+108
-7
lines changed

ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobProvider.java

+35-7
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@
2020
import java.io.FileNotFoundException;
2121
import java.io.IOException;
2222
import java.io.OutputStream;
23+
import java.util.concurrent.ExecutorService;
24+
import java.util.concurrent.Executors;
2325

2426
public final class BlobProvider extends ContentProvider {
2527

28+
private static final int PIPE_CAPACITY = 65536;
29+
30+
private ExecutorService executor = Executors.newSingleThreadExecutor();
31+
2632
@Override
2733
public boolean onCreate() {
2834
return true;
@@ -72,7 +78,7 @@ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundEx
7278
throw new RuntimeException("No blob module associated with BlobProvider");
7379
}
7480

75-
byte[] data = blobModule.resolve(uri);
81+
final byte[] data = blobModule.resolve(uri);
7682
if (data == null) {
7783
throw new FileNotFoundException("Cannot open " + uri.toString() + ", blob not found.");
7884
}
@@ -84,12 +90,34 @@ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundEx
8490
return null;
8591
}
8692
ParcelFileDescriptor readSide = pipe[0];
87-
ParcelFileDescriptor writeSide = pipe[1];
88-
89-
try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) {
90-
outputStream.write(data);
91-
} catch (IOException exception) {
92-
return null;
93+
final ParcelFileDescriptor writeSide = pipe[1];
94+
95+
if (data.length <= PIPE_CAPACITY) {
96+
// If the blob length is less than or equal to pipe capacity (64 KB),
97+
// we can write the data synchronously to the pipe buffer.
98+
try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) {
99+
outputStream.write(data);
100+
} catch (IOException exception) {
101+
return null;
102+
}
103+
} else {
104+
// For blobs larger than 64 KB, a synchronous write would fill up the whole buffer
105+
// and block forever, because there are no readers to empty the buffer.
106+
// Writing from a separate thread allows us to return the read side descriptor
107+
// immediately so that both writer and reader can work concurrently.
108+
// Reading from the pipe empties the buffer and allows the next chunks to be written.
109+
Runnable writer =
110+
new Runnable() {
111+
public void run() {
112+
try (OutputStream outputStream =
113+
new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) {
114+
outputStream.write(data);
115+
} catch (IOException exception) {
116+
// no-op
117+
}
118+
}
119+
};
120+
executor.submit(writer);
93121
}
94122

95123
return readSide;

packages/rn-tester/android/app/src/main/AndroidManifest.xml

+5
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
</intent-filter>
5353
</activity>
5454
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
55+
<provider
56+
android:name="com.facebook.react.modules.blob.BlobProvider"
57+
android:authorities="@string/blob_provider_authority"
58+
android:exported="false"
59+
/>
5560
</application>
5661

5762
</manifest>
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
<resources>
22
<string name="app_name">RNTester App</string>
3+
<string name="blob_provider_authority">com.facebook.react.uiapp.blobs</string>
34
</resources>

packages/rn-tester/js/examples/Image/ImageExample.js

+67
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,58 @@ type ImageSource = $ReadOnly<{|
3232
uri: string,
3333
|}>;
3434

35+
type BlobImageState = {|
36+
objectURL: ?string,
37+
|};
38+
39+
type BlobImageProps = $ReadOnly<{|
40+
url: string,
41+
|}>;
42+
43+
class BlobImage extends React.Component<BlobImageProps, BlobImageState> {
44+
state = {
45+
objectURL: null,
46+
};
47+
48+
UNSAFE_componentWillMount() {
49+
(async () => {
50+
const result = await fetch(this.props.url);
51+
const blob = await result.blob();
52+
const objectURL = URL.createObjectURL(blob);
53+
this.setState({objectURL});
54+
})();
55+
}
56+
57+
render() {
58+
return this.state.objectURL !== null ? (
59+
<Image source={{uri: this.state.objectURL}} style={styles.base} />
60+
) : (
61+
<Text>Object URL not created yet</Text>
62+
);
63+
}
64+
}
65+
66+
type BlobImageExampleState = {||};
67+
68+
type BlobImageExampleProps = $ReadOnly<{|
69+
urls: string[],
70+
|}>;
71+
72+
class BlobImageExample extends React.Component<
73+
BlobImageExampleProps,
74+
BlobImageExampleState,
75+
> {
76+
render() {
77+
return (
78+
<View style={styles.horizontal}>
79+
{this.props.urls.map(url => (
80+
<BlobImage key={url} url={url} />
81+
))}
82+
</View>
83+
);
84+
}
85+
}
86+
3587
type NetworkImageCallbackExampleState = {|
3688
events: Array<string>,
3789
startLoadPrefetched: boolean,
@@ -608,6 +660,21 @@ exports.examples = [
608660
return <Image source={fullImage} style={styles.base} />;
609661
},
610662
},
663+
{
664+
title: 'Plain Blob Image',
665+
description: ('If the `source` prop `uri` property is an object URL, ' +
666+
'then it will be resolved using `BlobProvider` (Android) or `RCTBlobManager` (iOS).': string),
667+
render: function(): React.Node {
668+
return (
669+
<BlobImageExample
670+
urls={[
671+
'https://www.facebook.com/favicon.ico',
672+
'https://www.facebook.com/ads/pics/successstories.png',
673+
]}
674+
/>
675+
);
676+
},
677+
},
611678
{
612679
title: 'Plain Static Image',
613680
description: ('Static assets should be placed in the source code tree, and ' +

0 commit comments

Comments
 (0)