Skip to content

Commit

Permalink
Fix support for blobs larger than 64 KB on Android (#31789)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tomekzaw authored and grabbou committed Jul 16, 2021
1 parent 626d25c commit 49253dc
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 7 deletions.
Expand Up @@ -20,9 +20,15 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public final class BlobProvider extends ContentProvider {

private static final int PIPE_CAPACITY = 65536;

private ExecutorService executor = Executors.newSingleThreadExecutor();

@Override
public boolean onCreate() {
return true;
Expand Down Expand Up @@ -72,7 +78,7 @@ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundEx
throw new RuntimeException("No blob module associated with BlobProvider");
}

byte[] data = blobModule.resolve(uri);
final byte[] data = blobModule.resolve(uri);
if (data == null) {
throw new FileNotFoundException("Cannot open " + uri.toString() + ", blob not found.");
}
Expand All @@ -84,12 +90,34 @@ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundEx
return null;
}
ParcelFileDescriptor readSide = pipe[0];
ParcelFileDescriptor writeSide = pipe[1];

try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) {
outputStream.write(data);
} catch (IOException exception) {
return null;
final ParcelFileDescriptor writeSide = pipe[1];

if (data.length <= PIPE_CAPACITY) {
// If the blob length is less than or equal to pipe capacity (64 KB),
// we can write the data synchronously to the pipe buffer.
try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) {
outputStream.write(data);
} catch (IOException exception) {
return null;
}
} else {
// For blobs larger than 64 KB, a synchronous write would fill up the whole buffer
// and block forever, because there are no readers to empty the buffer.
// Writing from a separate thread allows us to return the read side descriptor
// immediately so that both writer and reader can work concurrently.
// Reading from the pipe empties the buffer and allows the next chunks to be written.
Runnable writer =
new Runnable() {
public void run() {
try (OutputStream outputStream =
new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) {
outputStream.write(data);
} catch (IOException exception) {
// no-op
}
}
};
executor.submit(writer);
}

return readSide;
Expand Down
5 changes: 5 additions & 0 deletions packages/rn-tester/android/app/src/main/AndroidManifest.xml
Expand Up @@ -52,6 +52,11 @@
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<provider
android:name="com.facebook.react.modules.blob.BlobProvider"
android:authorities="@string/blob_provider_authority"
android:exported="false"
/>
</application>

</manifest>
@@ -1,3 +1,4 @@
<resources>
<string name="app_name">RNTester App</string>
<string name="blob_provider_authority">com.facebook.react.uiapp.blobs</string>
</resources>
67 changes: 67 additions & 0 deletions packages/rn-tester/js/examples/Image/ImageExample.js
Expand Up @@ -32,6 +32,58 @@ type ImageSource = $ReadOnly<{|
uri: string,
|}>;

type BlobImageState = {|
objectURL: ?string,
|};

type BlobImageProps = $ReadOnly<{|
url: string,
|}>;

class BlobImage extends React.Component<BlobImageProps, BlobImageState> {
state = {
objectURL: null,
};

UNSAFE_componentWillMount() {
(async () => {
const result = await fetch(this.props.url);
const blob = await result.blob();
const objectURL = URL.createObjectURL(blob);
this.setState({objectURL});
})();
}

render() {
return this.state.objectURL !== null ? (
<Image source={{uri: this.state.objectURL}} style={styles.base} />
) : (
<Text>Object URL not created yet</Text>
);
}
}

type BlobImageExampleState = {||};

type BlobImageExampleProps = $ReadOnly<{|
urls: string[],
|}>;

class BlobImageExample extends React.Component<
BlobImageExampleProps,
BlobImageExampleState,
> {
render() {
return (
<View style={styles.horizontal}>
{this.props.urls.map(url => (
<BlobImage key={url} url={url} />
))}
</View>
);
}
}

type NetworkImageCallbackExampleState = {|
events: Array<string>,
startLoadPrefetched: boolean,
Expand Down Expand Up @@ -608,6 +660,21 @@ exports.examples = [
return <Image source={fullImage} style={styles.base} />;
},
},
{
title: 'Plain Blob Image',
description: ('If the `source` prop `uri` property is an object URL, ' +
'then it will be resolved using `BlobProvider` (Android) or `RCTBlobManager` (iOS).': string),
render: function(): React.Node {
return (
<BlobImageExample
urls={[
'https://www.facebook.com/favicon.ico',
'https://www.facebook.com/ads/pics/successstories.png',
]}
/>
);
},
},
{
title: 'Plain Static Image',
description: ('Static assets should be placed in the source code tree, and ' +
Expand Down

0 comments on commit 49253dc

Please sign in to comment.