Fix support for blobs larger than 64 KB on Android (#31789)

Summary:
Fixes https://github.com/facebook/react-native/issues/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.

41ecccefcf/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: https://github.com/facebook/react-native/pull/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
This commit is contained in:
Tomek Zawadzki 2021-07-14 11:10:11 -07:00 коммит произвёл Facebook GitHub Bot
Родитель efd4dafc54
Коммит f00e348ca7
4 изменённых файлов: 107 добавлений и 6 удалений

Просмотреть файл

@ -20,9 +20,15 @@ import com.facebook.react.bridge.ReactContext;
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;
@ -72,7 +78,7 @@ public final class BlobProvider extends ContentProvider {
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.");
}
@ -84,12 +90,34 @@ public final class BlobProvider extends ContentProvider {
return null;
}
ParcelFileDescriptor readSide = pipe[0];
ParcelFileDescriptor writeSide = pipe[1];
final ParcelFileDescriptor writeSide = pipe[1];
try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) {
outputStream.write(data);
} catch (IOException exception) {
return null;
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;

Просмотреть файл

@ -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>

Просмотреть файл

@ -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,
@ -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 ' +