mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-13 09:39:33 +01:00
Android: Hold a client for the storage provider between calls
Without one, every query and DocumentsContract call acquires the provider and releases it again, and when our reference count drops to zero, each of those is a binder call to the system as well. Holding an unstable ContentProviderClient keeps the count up, without tying our process to the provider's. Queries go through it, and a dead provider is dropped and acquired again. Storage test timings on an SD card, median of five runs before and after: exists 9.3 -> 6.7 ms, file info 10.4 -> 7.4, overwrite 21.8 -> 18.6, read 28.5 -> 26.6. Creating, deleting and listing are dominated by the provider's own work, and gain a few percent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
97099b4d96
commit
666dfd2187
1 file changed
+60
-6
@@ -1,6 +1,7 @@
|
||||
package org.ppsspp.ppsspp;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -44,6 +45,61 @@ public class ContentUri {
|
||||
private static final int STORAGE_ERROR_DISK_FULL = -3;
|
||||
private static final int STORAGE_ERROR_ALREADY_EXISTS = -4;
|
||||
|
||||
// Without a client of our own, every query and DocumentsContract call acquires the storage provider and
|
||||
// releases it again, and when our reference count drops to zero, each of those is another binder call,
|
||||
// to the system. Holding one keeps the count up. Unstable, so the provider dying doesn't take our process
|
||||
// with it: queries that then fail drop it, and the next one acquires a new one.
|
||||
private static final Object clientLock = new Object();
|
||||
private static ContentProviderClient client;
|
||||
private static String clientAuthority;
|
||||
|
||||
private static ContentProviderClient getClient(Context context, Uri uri) {
|
||||
final String authority = uri.getAuthority();
|
||||
if (authority == null) {
|
||||
return null;
|
||||
}
|
||||
synchronized (clientLock) {
|
||||
if (client != null && authority.equals(clientAuthority)) {
|
||||
return client;
|
||||
}
|
||||
dropClientLocked();
|
||||
// The application's resolver, so the client doesn't keep the activity alive.
|
||||
client = context.getApplicationContext().getContentResolver().acquireUnstableContentProviderClient(authority);
|
||||
clientAuthority = authority;
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
private static void dropClientLocked() {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
client.close();
|
||||
} else {
|
||||
client.release();
|
||||
}
|
||||
client = null;
|
||||
clientAuthority = null;
|
||||
}
|
||||
|
||||
private static Cursor query(Context context, Uri uri, String[] projection) throws Exception {
|
||||
final ContentProviderClient c = getClient(context, uri);
|
||||
if (c != null) {
|
||||
try {
|
||||
return c.query(uri, projection, null, null, null);
|
||||
} catch (android.os.DeadObjectException e) {
|
||||
Log.w(TAG, "Storage provider died, acquiring it again");
|
||||
synchronized (clientLock) {
|
||||
if (client == c) {
|
||||
dropClientLocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return context.getContentResolver().query(uri, projection, null, null, null);
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static int openContentUri(Activity activity, String uriString, String mode) {
|
||||
@@ -110,10 +166,9 @@ public class ContentUri {
|
||||
DocumentsContract.Document.COLUMN_SIZE,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE, // check for MIME_TYPE_DIR
|
||||
};
|
||||
final ContentResolver resolver = activity.getContentResolver();
|
||||
final String documentId = DocumentsContract.getDocumentId(uri);
|
||||
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, documentId);
|
||||
c = resolver.query(childrenUri, columns, null, null, null);
|
||||
c = query(activity, childrenUri, columns);
|
||||
long sizeSum = 0;
|
||||
|
||||
// Buffer the URIs so we only have one cursor active at once. I don't trust the storage framework
|
||||
@@ -177,7 +232,6 @@ public class ContentUri {
|
||||
public static String[] listContentUriDir(Activity activity, String uriString, String prefix) {
|
||||
try {
|
||||
Uri uri = Uri.parse(uriString);
|
||||
final ContentResolver resolver = activity.getContentResolver();
|
||||
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
|
||||
uri, DocumentsContract.getDocumentId(uri));
|
||||
final ArrayList<String> listing = new ArrayList<>();
|
||||
@@ -194,7 +248,7 @@ public class ContentUri {
|
||||
prefix = prefix.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
try (Cursor c = resolver.query(childrenUri, projection, null, null, null)) {
|
||||
try (Cursor c = query(activity, childrenUri, projection)) {
|
||||
if (c == null) {
|
||||
return new String[]{"X"};
|
||||
}
|
||||
@@ -355,7 +409,7 @@ public class ContentUri {
|
||||
Cursor c = null;
|
||||
try {
|
||||
Uri uri = Uri.parse(fileUri);
|
||||
c = activity.getContentResolver().query(uri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null);
|
||||
c = query(activity, uri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID });
|
||||
if (c != null) {
|
||||
return c.getCount() > 0;
|
||||
} else {
|
||||
@@ -380,7 +434,7 @@ public class ContentUri {
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED // index 4
|
||||
};
|
||||
|
||||
try (Cursor c = activity.getContentResolver().query(Uri.parse(fileName), projection, null, null, null)) {
|
||||
try (Cursor c = query(activity, Uri.parse(fileName), projection)) {
|
||||
if (c != null && c.moveToFirst()) {
|
||||
return cursorToString(c, null);
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user