Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit ffc77f0

Browse files
authored
Search multiple paths when loading deferred component .so files. (#23849)
This is a partial revert of 7c19824 On some devices we still need to the original search paths approach because dlopen with just the base file name doesn't work. We're combining both approaches now, adding the base filename as the first entry in the searchPaths.
1 parent 0118b54 commit ffc77f0

File tree

5 files changed

+151
-30
lines changed

5 files changed

+151
-30
lines changed

shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,19 +1110,21 @@ public void requestDartDeferredLibrary(int loadingUnitId) {
11101110
* @param loadingUnitId The loadingUnitId is assigned during compile time by gen_snapshot and is
11111111
* automatically retrieved when loadLibrary() is called on a dart deferred library. This is
11121112
* used to identify which Dart deferred library the resolved correspond to.
1113-
* @param sharedLibraryName File name of the .so file to be loaded, or if the file is not already
1114-
* in LD_LIBRARY_PATH, the full path to the file. The .so files in the lib/[abi] directory are
1115-
* already in LD_LIBRARY_PATH and in this case you only need to pass the file name.
1113+
* @param searchPaths An array of paths in which to look for valid dart shared libraries. This
1114+
* supports paths within zipped apks as long as the apks are not compressed using the
1115+
* `path/to/apk.apk!path/inside/apk/lib.so` format. Paths will be tried first to last and ends
1116+
* when a library is sucessfully found. When the found library is invalid, no additional paths
1117+
* will be attempted.
11161118
*/
11171119
@UiThread
1118-
public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String sharedLibraryName) {
1120+
public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String[] searchPaths) {
11191121
ensureRunningOnMainThread();
11201122
ensureAttachedToNative();
1121-
nativeLoadDartDeferredLibrary(nativeShellHolderId, loadingUnitId, sharedLibraryName);
1123+
nativeLoadDartDeferredLibrary(nativeShellHolderId, loadingUnitId, searchPaths);
11221124
}
11231125

11241126
private native void nativeLoadDartDeferredLibrary(
1125-
long nativeShellHolderId, int loadingUnitId, @NonNull String sharedLibraryName);
1127+
long nativeShellHolderId, int loadingUnitId, @NonNull String[] searchPaths);
11261128

11271129
/**
11281130
* Adds the specified AssetManager as an APKAssetResolver in the Flutter Engine's AssetManager.

shell/platform/android/io/flutter/embedding/engine/deferredcomponents/DeferredComponentManager.java

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@
2222
* This call retrieves a unique identifier called the loading unit id, which is assigned by
2323
* gen_snapshot during compilation. The loading unit id is passed down through the engine and
2424
* invokes installDeferredComponent. Once the feature module is downloaded, loadAssets and
25-
* loadDartLibrary should be invoked. loadDartLibrary should pass the file name of the shared
26-
* library .so file to FlutterJNI.loadDartDeferredLibrary for the engine to dlopen, or if the file
27-
* is not in LD_LIBRARY_PATH, it should find the shared library .so file and pass the full path.
28-
* loadAssets should typically ensure the new assets are available to the engine's asset manager by
29-
* passing an updated Android AssetManager to the engine via FlutterJNI.updateAssetManager.
25+
* loadDartLibrary should be invoked. loadDartLibrary should find shared library .so files for the
26+
* engine to open and pass the .so path to FlutterJNI.loadDartDeferredLibrary. loadAssets should
27+
* typically ensure the new assets are available to the engine's asset manager by passing an updated
28+
* Android AssetManager to the engine via FlutterJNI.updateAssetManager.
3029
*
3130
* <p>The loadAssets and loadDartLibrary methods are separated out because they may also be called
3231
* manually via platform channel messages. A full installDeferredComponent implementation should
@@ -183,10 +182,14 @@ public interface DeferredComponentManager {
183182
* Load the .so shared library file into the Dart VM.
184183
*
185184
* <p>When the download of a deferred component module completes, this method should be called to
186-
* find the .so library file. The filenames, or path if it's not in LD_LIBRARY_PATH, should then
187-
* be passed to FlutterJNI.loadDartDeferredLibrary to be dlopen-ed and loaded into the Dart VM.
188-
* The .so files in the lib/[abi] directory are already in LD_LIBRARY_PATH and in this case you
189-
* only need to pass the file name.
185+
* find the path .so library file. The path(s) should then be passed to
186+
* FlutterJNI.loadDartDeferredLibrary to be dlopen-ed and loaded into the Dart VM.
187+
*
188+
* <p>Specifically, APKs distributed by Android's app bundle format may vary by device and API
189+
* number, so FlutterJNI's loadDartDeferredLibrary accepts a list of search paths with can include
190+
* paths within APKs that have not been unpacked using the
191+
* `path/to/apk.apk!path/inside/apk/lib.so` format. Each search path will be attempted in order
192+
* until a shared library is found. This allows for the developer to avoid unpacking the apk zip.
190193
*
191194
* <p>Upon successful load of the Dart library, the Dart future from the originating loadLibary()
192195
* call completes and developers are able to use symbols and assets from the feature module.

shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import android.content.Context;
99
import android.content.pm.PackageManager.NameNotFoundException;
1010
import android.content.res.AssetManager;
11+
import android.os.Build;
1112
import android.util.SparseArray;
1213
import android.util.SparseIntArray;
1314
import androidx.annotation.NonNull;
@@ -25,10 +26,13 @@
2526
import io.flutter.embedding.engine.loader.ApplicationInfoLoader;
2627
import io.flutter.embedding.engine.loader.FlutterApplicationInfo;
2728
import io.flutter.embedding.engine.systemchannels.DeferredComponentChannel;
29+
import java.io.File;
2830
import java.util.ArrayList;
2931
import java.util.HashMap;
32+
import java.util.LinkedList;
3033
import java.util.List;
3134
import java.util.Map;
35+
import java.util.Queue;
3236

3337
/**
3438
* Flutter default implementation of DeferredComponentManager that downloads deferred component
@@ -341,7 +345,57 @@ public void loadDartLibrary(int loadingUnitId, String moduleName) {
341345
String aotSharedLibraryName =
342346
flutterApplicationInfo.aotSharedLibraryName + "-" + loadingUnitId + ".part.so";
343347

344-
flutterJNI.loadDartDeferredLibrary(loadingUnitId, aotSharedLibraryName);
348+
// Possible values: armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, mips, mips64
349+
String abi;
350+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
351+
abi = Build.SUPPORTED_ABIS[0];
352+
} else {
353+
abi = Build.CPU_ABI;
354+
}
355+
String pathAbi = abi.replace("-", "_"); // abis are represented with underscores in paths.
356+
357+
// TODO(garyq): Optimize this apk/file discovery process to use less i/o and be more
358+
// performant and robust.
359+
360+
// Search directly in APKs first
361+
List<String> apkPaths = new ArrayList<>();
362+
// If not found in APKs, we check in extracted native libs for the lib directly.
363+
List<String> soPaths = new ArrayList<>();
364+
Queue<File> searchFiles = new LinkedList<>();
365+
searchFiles.add(context.getFilesDir());
366+
while (!searchFiles.isEmpty()) {
367+
File file = searchFiles.remove();
368+
if (file != null && file.isDirectory()) {
369+
for (File f : file.listFiles()) {
370+
searchFiles.add(f);
371+
}
372+
continue;
373+
}
374+
String name = file.getName();
375+
if (name.endsWith(".apk") && name.startsWith(moduleName) && name.contains(pathAbi)) {
376+
apkPaths.add(file.getAbsolutePath());
377+
continue;
378+
}
379+
if (name.equals(aotSharedLibraryName)) {
380+
soPaths.add(file.getAbsolutePath());
381+
}
382+
}
383+
384+
List<String> searchPaths = new ArrayList<>();
385+
386+
// Add the bare filename as the first search path. In some devices, the so
387+
// file can be dlopen-ed with just the file name.
388+
searchPaths.add(aotSharedLibraryName);
389+
390+
for (String path : apkPaths) {
391+
searchPaths.add(path + "!lib/" + abi + "/" + aotSharedLibraryName);
392+
}
393+
for (String path : soPaths) {
394+
searchPaths.add(path);
395+
}
396+
397+
flutterJNI.loadDartDeferredLibrary(
398+
loadingUnitId, searchPaths.toArray(new String[apkPaths.size()]));
345399
}
346400

347401
public boolean uninstallDeferredComponent(int loadingUnitId, String moduleName) {

shell/platform/android/platform_view_android_jni_impl.cc

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -567,19 +567,23 @@ static void LoadDartDeferredLibrary(JNIEnv* env,
567567
jobject obj,
568568
jlong shell_holder,
569569
jint jLoadingUnitId,
570-
jstring jSharedLibraryName) {
570+
jobjectArray jSearchPaths) {
571571
// Convert java->c++
572572
intptr_t loading_unit_id = static_cast<intptr_t>(jLoadingUnitId);
573-
std::string sharedLibraryName =
574-
fml::jni::JavaStringToString(env, jSharedLibraryName);
573+
std::vector<std::string> search_paths =
574+
fml::jni::StringArrayToVector(env, jSearchPaths);
575575

576576
// Use dlopen here to directly check if handle is nullptr before creating a
577577
// NativeLibrary.
578-
void* handle = ::dlopen(sharedLibraryName.c_str(), RTLD_NOW);
578+
void* handle = nullptr;
579+
while (handle == nullptr && !search_paths.empty()) {
580+
std::string path = search_paths.back();
581+
handle = ::dlopen(path.c_str(), RTLD_NOW);
582+
search_paths.pop_back();
583+
}
579584
if (handle == nullptr) {
580585
LoadLoadingUnitFailure(loading_unit_id,
581-
"Shared library not found for the provided name.",
582-
true);
586+
"No lib .so found for provided search paths.", true);
583587
return;
584588
}
585589
fml::RefPtr<fml::NativeLibrary> native_lib =
@@ -777,7 +781,7 @@ bool RegisterApi(JNIEnv* env) {
777781
},
778782
{
779783
.name = "nativeLoadDartDeferredLibrary",
780-
.signature = "(JILjava/lang/String;)V",
784+
.signature = "(JI[Ljava/lang/String;)V",
781785
.fnPtr = reinterpret_cast<void*>(&LoadDartDeferredLibrary),
782786
},
783787
{

shell/platform/android/test/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManagerTest.java

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package io.flutter.embedding.engine.deferredcomponents;
66

77
import static junit.framework.TestCase.assertEquals;
8+
import static junit.framework.TestCase.assertTrue;
89
import static org.mockito.Mockito.any;
910
import static org.mockito.Mockito.anyInt;
1011
import static org.mockito.Mockito.doReturn;
@@ -35,17 +36,17 @@ private class TestFlutterJNI extends FlutterJNI {
3536
public int loadDartDeferredLibraryCalled = 0;
3637
public int updateAssetManagerCalled = 0;
3738
public int deferredComponentInstallFailureCalled = 0;
38-
public String sharedLibraryName;
39+
public String[] searchPaths;
3940
public int loadingUnitId;
4041
public AssetManager assetManager;
4142
public String assetBundlePath;
4243

4344
public TestFlutterJNI() {}
4445

4546
@Override
46-
public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String sharedLibraryName) {
47+
public void loadDartDeferredLibrary(int loadingUnitId, @NonNull String[] searchPaths) {
4748
loadDartDeferredLibraryCalled++;
48-
this.sharedLibraryName = sharedLibraryName;
49+
this.searchPaths = searchPaths;
4950
this.loadingUnitId = loadingUnitId;
5051
}
5152

@@ -85,7 +86,9 @@ public void downloadCallsJNIFunctions() throws NameNotFoundException {
8586
Context spyContext = spy(RuntimeEnvironment.application);
8687
doReturn(spyContext).when(spyContext).createPackageContext(any(), anyInt());
8788
doReturn(null).when(spyContext).getAssets();
88-
String soTestPath = "libapp.so-123.part.so";
89+
String soTestFilename = "libapp.so-123.part.so";
90+
String soTestPath = "test/path/" + soTestFilename;
91+
doReturn(new File(soTestPath)).when(spyContext).getFilesDir();
8992
TestPlayStoreDeferredComponentManager playStoreManager =
9093
new TestPlayStoreDeferredComponentManager(spyContext, jni);
9194
jni.setDeferredComponentManager(playStoreManager);
@@ -96,7 +99,9 @@ public void downloadCallsJNIFunctions() throws NameNotFoundException {
9699
assertEquals(jni.updateAssetManagerCalled, 1);
97100
assertEquals(jni.deferredComponentInstallFailureCalled, 0);
98101

99-
assertEquals(jni.sharedLibraryName, soTestPath);
102+
assertEquals(jni.searchPaths[0], soTestFilename);
103+
assertTrue(jni.searchPaths[1].endsWith(soTestPath));
104+
assertEquals(jni.searchPaths.length, 2);
100105
assertEquals(jni.loadingUnitId, 123);
101106
assertEquals(jni.assetBundlePath, "flutter_assets");
102107
}
@@ -118,7 +123,9 @@ public void downloadCallsJNIFunctionsWithFilenameFromManifest() throws NameNotFo
118123
.thenReturn(applicationInfo);
119124
doReturn(packageManager).when(spyContext).getPackageManager();
120125

121-
String soTestPath = "custom_name.so-123.part.so";
126+
String soTestFilename = "custom_name.so-123.part.so";
127+
String soTestPath = "test/path/" + soTestFilename;
128+
doReturn(new File(soTestPath)).when(spyContext).getFilesDir();
122129
TestPlayStoreDeferredComponentManager playStoreManager =
123130
new TestPlayStoreDeferredComponentManager(spyContext, jni);
124131
jni.setDeferredComponentManager(playStoreManager);
@@ -129,11 +136,62 @@ public void downloadCallsJNIFunctionsWithFilenameFromManifest() throws NameNotFo
129136
assertEquals(jni.updateAssetManagerCalled, 1);
130137
assertEquals(jni.deferredComponentInstallFailureCalled, 0);
131138

132-
assertEquals(jni.sharedLibraryName, soTestPath);
139+
assertEquals(jni.searchPaths[0], soTestFilename);
140+
assertTrue(jni.searchPaths[1].endsWith(soTestPath));
141+
assertEquals(jni.searchPaths.length, 2);
133142
assertEquals(jni.loadingUnitId, 123);
134143
assertEquals(jni.assetBundlePath, "custom_assets");
135144
}
136145

146+
@Test
147+
public void searchPathsAddsApks() throws NameNotFoundException {
148+
TestFlutterJNI jni = new TestFlutterJNI();
149+
Context spyContext = spy(RuntimeEnvironment.application);
150+
doReturn(spyContext).when(spyContext).createPackageContext(any(), anyInt());
151+
doReturn(null).when(spyContext).getAssets();
152+
String apkTestPath = "test/path/TestModuleName_armeabi_v7a.apk";
153+
doReturn(new File(apkTestPath)).when(spyContext).getFilesDir();
154+
TestPlayStoreDeferredComponentManager playStoreManager =
155+
new TestPlayStoreDeferredComponentManager(spyContext, jni);
156+
jni.setDeferredComponentManager(playStoreManager);
157+
158+
assertEquals(jni.loadingUnitId, 0);
159+
160+
playStoreManager.installDeferredComponent(123, "TestModuleName");
161+
assertEquals(jni.loadDartDeferredLibraryCalled, 1);
162+
assertEquals(jni.updateAssetManagerCalled, 1);
163+
assertEquals(jni.deferredComponentInstallFailureCalled, 0);
164+
165+
assertEquals(jni.searchPaths[0], "libapp.so-123.part.so");
166+
assertTrue(jni.searchPaths[1].endsWith(apkTestPath + "!lib/armeabi-v7a/libapp.so-123.part.so"));
167+
assertEquals(jni.searchPaths.length, 2);
168+
assertEquals(jni.loadingUnitId, 123);
169+
}
170+
171+
@Test
172+
public void invalidSearchPathsAreIgnored() throws NameNotFoundException {
173+
TestFlutterJNI jni = new TestFlutterJNI();
174+
Context spyContext = spy(RuntimeEnvironment.application);
175+
doReturn(spyContext).when(spyContext).createPackageContext(any(), anyInt());
176+
doReturn(null).when(spyContext).getAssets();
177+
String apkTestPath = "test/path/invalidpath.apk";
178+
doReturn(new File(apkTestPath)).when(spyContext).getFilesDir();
179+
TestPlayStoreDeferredComponentManager playStoreManager =
180+
new TestPlayStoreDeferredComponentManager(spyContext, jni);
181+
jni.setDeferredComponentManager(playStoreManager);
182+
183+
assertEquals(jni.loadingUnitId, 0);
184+
185+
playStoreManager.installDeferredComponent(123, "TestModuleName");
186+
assertEquals(jni.loadDartDeferredLibraryCalled, 1);
187+
assertEquals(jni.updateAssetManagerCalled, 1);
188+
assertEquals(jni.deferredComponentInstallFailureCalled, 0);
189+
190+
assertEquals(jni.searchPaths[0], "libapp.so-123.part.so");
191+
assertEquals(jni.searchPaths.length, 1);
192+
assertEquals(jni.loadingUnitId, 123);
193+
}
194+
137195
@Test
138196
public void assetManagerUpdateInvoked() throws NameNotFoundException {
139197
TestFlutterJNI jni = new TestFlutterJNI();

0 commit comments

Comments
 (0)