下面我们来看看是怎么处理的
首先我们知道要想flutter热更新,所有思路都离不开一个主旨,那就是修改libapp.so的加载路径,把它替换成我们的libapp_hot.so的路径就能实现。经过这么多次flutter sdk的更新,其加载so包的代码基本没有变化。
我们先看下最新版flutter sdk中有关加载libapp.so包的方法体代码:
代码定位到源码中io.flutter.embedding.engine.loader包下的FlutterLoader类中,与1.9.几版本相比,1.12.13版本中看到flutter又把业务代码进行了包装拆分。新版本中在FlutterLoader类中处理加载so包的代码,之前是在FlutterMain类中处理。
FlutterLoader类-----------------》讲解说明的注释在代码中已做处理-------------
/**
* Finds Flutter resources in an application APK and also loads Flutter's native library.
*/
public class FlutterLoader {
private static final String TAG = "FlutterLoader";
// Must match values in flutter::switches
//libapp.so的key值,这个可以对应多个so包
private static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name";
private static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path";
private static final String VM_SNAPSHOT_DATA_KEY = "vm-snapshot-data";
private static final String ISOLATE_SNAPSHOT_DATA_KEY = "isolate-snapshot-data";
private static final String FLUTTER_ASSETS_DIR_KEY = "flutter-assets-dir";
// XML Attribute keys supported in AndroidManifest.xml
private static final String PUBLIC_AOT_SHARED_LIBRARY_NAME =
FlutterLoader.class.getName() + '.' + AOT_SHARED_LIBRARY_NAME;
private static final String PUBLIC_VM_SNAPSHOT_DATA_KEY =
FlutterLoader.class.getName() + '.' + VM_SNAPSHOT_DATA_KEY;
private static final String PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY =
FlutterLoader.class.getName() + '.' + ISOLATE_SNAPSHOT_DATA_KEY;
private static final String PUBLIC_FLUTTER_ASSETS_DIR_KEY =
FlutterLoader.class.getName() + '.' + FLUTTER_ASSETS_DIR_KEY;
// Resource names used for components of the precompiled snapshot.
//万变不离其宗,这里我们只看libapp.so的相关代码,这是它的默认命名形式
private static final String DEFAULT_AOT_SHARED_LIBRARY_NAME = "libapp.so";
private static final String DEFAULT_VM_SNAPSHOT_DATA = "vm_snapshot_data";
private static final String DEFAULT_ISOLATE_SNAPSHOT_DATA = "isolate_snapshot_data";
private static final String DEFAULT_LIBRARY = "libflutter.so";
private static final String DEFAULT_KERNEL_BLOB = "kernel_blob.bin";
private static final String DEFAULT_FLUTTER_ASSETS_DIR = "flutter_assets";
// Mutable because default values can be overridden via config properties
//这个变量才是flutter在代码中最终加载的指定so包,这里也做了默认指向libapp.so
//再往下走找到调用aotSharedLibraryName这个变量的方法中,我们定位到ensureInitializationComplete()中
private String aotSharedLibraryName = DEFAULT_AOT_SHARED_LIBRARY_NAME;
private String vmSnapshotData = DEFAULT_VM_SNAPSHOT_DATA;
private String isolateSnapshotData = DEFAULT_ISOLATE_SNAPSHOT_DATA;
private String flutterAssetsDir = DEFAULT_FLUTTER_ASSETS_DIR;
private static FlutterLoader instance;
/**
* Returns a singleton {@code FlutterLoader} instance.
*
* The returned instance loads Flutter native libraries in the standard way. A singleton object
* is used instead of static methods to facilitate testing without actually running native
* library linking.
*/
@NonNull
public static FlutterLoader getInstance() {
if (instance == null) {
instance = new FlutterLoader();
}
return instance;
}
private boolean initialized = false;
@Nullable
private ResourceExtractor resourceExtractor;
@Nullable
private Settings settings;
/**
* Starts initialization of the native system.
* @param applicationContext The Android application context.
*/
public void startInitialization(@NonNull Context applicationContext) {
startInitialization(applicationContext, new Settings());
}
/**
* Starts initialization of the native system.
*
* This loads the Flutter engine's native library to enable subsequent JNI calls. This also
* starts locating and unpacking Dart resources packaged in the app's APK.
*
* Calling this method multiple times has no effect.
*
* @param applicationContext The Android application context.
* @param settings Configuration settings.
*/
public void startInitialization(@NonNull Context applicationContext, @NonNull Settings settings) {
// Do not run startInitialization more than once.
if (this.settings != null) {
return;
}
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("startInitialization must be called on the main thread");
}
this.settings = settings;
long initStartTimestampMillis = SystemClock.uptimeMillis();
initConfig(applicationContext);
initResources(applicationContext);
System.loadLibrary("flutter");
VsyncWaiter
.getInstance((WindowManager) applicationContext.getSystemService(Context.WINDOW_SERVICE))
.init();
// We record the initialization time using SystemClock because at the start of the
// initialization we have not yet loaded the native library to call into dart_tools_api.h.
// To get Timeline timestamp of the start of initialization we simply subtract the delta
// from the Timeline timestamp at the current moment (the assumption is that the overhead
// of the JNI call is negligible).
long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis;
FlutterJNI.nativeRecordStartTimestamp(initTimeMillis);
}
/**
* Blocks until initialization of the native system has completed.
*
* Calling this method multiple times has no effect.
*
* @param applicationContext The Android application context.
* @param args Flags sent to the Flutter runtime.
*/
//就是这里了,我们可以看到它接收两个参数第一个上下文,第二个是个数组,
//---->本文要讲的重点就在这个args数组<----,我们先进方法体里边去分析一波,
//然后在来说说这个args数组,这里只是快速过略一下更新思路,其它代码不做讲解
public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) {
if (initialized) {
return;
}
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("ensureInitializationComplete must be called on the main thread");
}
if (settings == null) {
throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization");
}
try {
if (resourceExtractor != null) {
resourceExtractor.waitForCompletion();
}
//这个shellArgs就是用来存放flutter各种so包以及打包后的其它产物的集合
List shellArgs = new ArrayList<>();
shellArgs.add("--icu-symbol-prefix=_binary_icudtl_dat");
ApplicationInfo applicationInfo = getApplicationInfo(applicationContext);
shellArgs.add("--icu-native-lib-path=" + applicationInfo.nativeLibraryDir + File.separator + DEFAULT_LIBRARY);
//这里是重点,如果传递进来的args数组不为空那么会一并加入shellArgs中,
//而shellArgs也是存放libapp.so包的那么这里我们想一下,
//只要找到这个args数组传递进来的源头,我们直接在源头处给它存进去
//我们需要替换掉的libapp.so包从而达到热更新就行。。。
//很幸运的是通过对FlutterActivity的启动过程分析中发现,
//在FlutterActivity中有这么一个方法getFlutterShellArgs(),
//它返回的FlutterShellArgs正是最终会传入到这里的第二个参数args所对应的值,
//这就相当容易了。看完这个类请请继续往下看,
//会讲到我们自定义的FlutterActivity的继承类
if (args != null) {
Collections.addAll(shellArgs, args);
}
String kernelPath = null;
if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
String snapshotAssetPath = PathUtils.getDataDirectory(applicationContext) + File.separator + flutterAssetsDir;
kernelPath = snapshotAssetPath + File.separator + DEFAULT_KERNEL_BLOB;
shellArgs.add("--" + SNAPSHOT_ASSET_PATH_KEY + "=" + snapshotAssetPath);
shellArgs.add("--" + VM_SNAPSHOT_DATA_KEY + "=" + vmSnapshotData);
shellArgs.add("--" + ISOLATE_SNAPSHOT_DATA_KEY + "=" + isolateSnapshotData);
} else {
//这是在release下时会存放aotSharedLibraryName所对应的libapp.so包
//我们只要在此之前存入新的so包路径就行
shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName);
// Most devices can load the AOT shared library based on the library name
// with no directory path. Provide a fully qualified path to the library
// as a workaround for devices where that fails.
shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + applicationInfo.nativeLibraryDir + File.separator + aotSharedLibraryName);
}
shellArgs.add("--cache-dir-path=" + PathUtils.getCacheDirectory(applicationContext));
if (settings.getLogTag() != null) {
shellArgs.add("--log-tag=" + settings.getLogTag());
}
String appStoragePath = PathUtils.getFilesDir(applicationContext);
String engineCachesPath = PathUtils.getCacheDirectory(applicationContext);
FlutterJNI.nativeInit(applicationContext, shellArgs.toArray(new String[0]),
kernelPath, appStoragePath, engineCachesPath);
initialized = true;
} catch (Exception e) {
Log.e(TAG, "Flutter initialization failed.", e);
throw new RuntimeException(e);
}
}
/**
* Same as {@link #ensureInitializationComplete(Context, String[])} but waiting on a background
* thread, then invoking {@code callback} on the {@code callbackHandler}.
*/
public void ensureInitializationCompleteAsync(
@NonNull Context applicationContext,
@Nullable String[] args,
@NonNull Handler callbackHandler,
@NonNull Runnable callback
) {
if (Looper.myLooper() != Looper.getMainLooper()) {
throw new IllegalStateException("ensureInitializationComplete must be called on the main thread");
}
if (settings == null) {
throw new IllegalStateException("ensureInitializationComplete must be called after startInitialization");
}
if (initialized) {
return;
}
new Thread(new Runnable() {
@Override
public void run() {
if (resourceExtractor != null) {
resourceExtractor.waitForCompletion();
}
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
ensureInitializationComplete(applicationContext.getApplicationContext(), args);
callbackHandler.post(callback);
}
});
}
}).start();
}
@NonNull
private ApplicationInfo getApplicationInfo(@NonNull Context applicationContext) {
try {
return applicationContext
.getPackageManager()
.getApplicationInfo(applicationContext.getPackageName(), PackageManager.GET_META_DATA);
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* Initialize our Flutter config values by obtaining them from the
* manifest XML file, falling back to default values.
*/
private void initConfig(@NonNull Context applicationContext) {
Bundle metadata = getApplicationInfo(applicationContext).metaData;
// There isn't a `` tag as a direct child of `` in
// `AndroidManifest.xml`.
if (metadata == null) {
return;
}
aotSharedLibraryName = metadata.getString(PUBLIC_AOT_SHARED_LIBRARY_NAME, DEFAULT_AOT_SHARED_LIBRARY_NAME);
flutterAssetsDir = metadata.getString(PUBLIC_FLUTTER_ASSETS_DIR_KEY, DEFAULT_FLUTTER_ASSETS_DIR);
vmSnapshotData = metadata.getString(PUBLIC_VM_SNAPSHOT_DATA_KEY, DEFAULT_VM_SNAPSHOT_DATA);
isolateSnapshotData = metadata.getString(PUBLIC_ISOLATE_SNAPSHOT_DATA_KEY, DEFAULT_ISOLATE_SNAPSHOT_DATA);
}
/**
* Extract assets out of the APK that need to be cached as uncompressed
* files on disk.
*/
private void initResources(@NonNull Context applicationContext) {
new ResourceCleaner(applicationContext).start();
if (BuildConfig.DEBUG || BuildConfig.JIT_RELEASE) {
final String dataDirPath = PathUtils.getDataDirectory(applicationContext);
final String packageName = applicationContext.getPackageName();
final PackageManager packageManager = applicationContext.getPackageManager();
final AssetManager assetManager = applicationContext.getResources().getAssets();
resourceExtractor = new ResourceExtractor(dataDirPath, packageName, packageManager, assetManager);
// In debug/JIT mode these assets will be written to disk and then
// mapped into memory so they can be provided to the Dart VM.
resourceExtractor
.addResource(fullAssetPathFrom(vmSnapshotData))
.addResource(fullAssetPathFrom(isolateSnapshotData))
.addResource(fullAssetPathFrom(DEFAULT_KERNEL_BLOB));
resourceExtractor.start();
}
}
@NonNull
public String findAppBundlePath() {
return flutterAssetsDir;
}
/**
* Returns the file name for the given asset.
* The returned file name can be used to access the asset in the APK
* through the {@link android.content.res.AssetManager} API.
*
* @param asset the name of the asset. The name can be hierarchical
* @return the filename to be used with {@link android.content.res.AssetManager}
*/
@NonNull
public String getLookupKeyForAsset(@NonNull String asset) {
return fullAssetPathFrom(asset);
}
/**
* Returns the file name for the given asset which originates from the
* specified packageName. The returned file name can be used to access
* the asset in the APK through the {@link android.content.res.AssetManager} API.
*
* @param asset the name of the asset. The name can be hierarchical
* @param packageName the name of the package from which the asset originates
* @return the file name to be used with {@link android.content.res.AssetManager}
*/
@NonNull
public String getLookupKeyForAsset(@NonNull String asset, @NonNull String packageName) {
return getLookupKeyForAsset(
"packages" + File.separator + packageName + File.separator + asset);
}
@NonNull
private String fullAssetPathFrom(@NonNull String filePath) {
return flutterAssetsDir + File.separator + filePath;
}
public static class Settings {
private String logTag;
@Nullable
public String getLogTag() {
return logTag;
}
/**
* Set the tag associated with Flutter app log messages.
* @param tag Log tag.
*/
public void setLogTag(String tag) {
logTag = tag;
}
}
}
不需要反射修改源码中任何代码,只需要写一个类,继承自FlutterActivity即可,这里拿我们demo中的继承类MyFlutterActivity来讲:
public class MyFlutterActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@SuppressLint("LongLogTag")
@NotNull
@Override
//重写FlutterActivity的getFlutterShellArgs()方法,往其中添加进我们修复后的so包,这里命名为libapp_hot.so
//这里的修复so包是我们已经存好在了app私有目录下的,也就是app_libs目录下
public FlutterShellArgs getFlutterShellArgs() {
Log.e("MyFlutterActivity", "getFlutterShellArgs");
FlutterShellArgs supFA = super.getFlutterShellArgs();
//isHotFix是用来方便造作是否使用修复so包,会在app的启动页MainActivity中通过intent传进来,so包也是在MainActivity中事先做的存储,存到应用私有目录app_libs目录下
if (getIntent().getBooleanExtra("isHotFix", false)) {
//修复的so包只能放在指定目录下,flutter只会读取这些目录下的so文件,这里测试过放在其它地方都不行
File dir = this.getDir("libs", Activity.MODE_PRIVATE);
String libPath = dir.getAbsolutePath() + File.separator + "libapp_hot.so";
if (dir.exists()) {
Log.e("MyFlutterActivity", "load new :" + libPath);
//这句代码就是关键,aot-shared-library-name这个key值就跟FlutterLoader中libapp.so对应的key值一样,然后指定我们修复后的so包libPath路径,这样就会在初始化flutter页面前flutter就会使用我们新的这个修复so包
supFA.add("--aot-shared-library-name=" + libPath);
}
}
return supFA;
}
public Boolean operatorSDCard() {//判断SD状态
String state = Environment.getExternalStorageState();
if (state.equals(Environment.MEDIA_MOUNTED)) {
return true;
} else {
return false;
}
}
}
下面附上MainActivity测试代码
class MainActivity : AppCompatActivity(), EasyPermissions.PermissionCallbacks,
EasyPermissions.RationaleCallbacks {
private lateinit var textMessage: TextView
private val onNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
oldso.visibility = View.VISIBLE
newso.visibility = View.VISIBLE
oldso.setOnClickListener {
//加载MyFlutterActivity,不使用修复后的so包
val intent = Intent(this@MainActivity, MyFlutterActivity::class.java)
.putExtra(
"initial_route",
"flutterHomePage?{\"name\":\"StephenCurry\", \"msg\":\"native to flutter\"}"
)
.putExtra("destroy_engine_with_activity", true)
.putExtra("isHotFix", false)
startActivity(intent)
}
newso.setOnClickListener {
//事先把修复后的so包放在手机sd卡更目录下,使用FlutterFileUtils下载到app私有目录app_libs下
val path = FlutterFileUtils.copyLibAndWrite(this, "libapp_hot.so")
Toast.makeText(this, "path已存储------->$path", Toast.LENGTH_LONG).show()
//加载MyFlutterActivity并使用修复后的so包
val intent = Intent(this@MainActivity, MyFlutterActivity::class.java)
.putExtra(
"initial_route",
"flutterHomePage?{\"name\":\"StephenCurry\", \"msg\":\"native to flutter\"}"
)
.putExtra("destroy_engine_with_activity", true)
.putExtra("isHotFix", true)
startActivity(intent)
}
return@OnNavigationItemSelectedListener true
}
R.id.navigation_dashboard -> {
return@OnNavigationItemSelectedListener true
}
R.id.navigation_notifications -> {
return@OnNavigationItemSelectedListener true
}
}
false
}
private val LOCATION_AND_CONTACTS =
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
@SuppressLint("LongLogTag")
override fun onCreate(savedInstanceState: Bundle?) {
EasyPermissions.requestPermissions(
this,
"读写权限",
123,
*LOCATION_AND_CONTACTS
)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
textMessage = findViewById(R.id.message)
navView.setOnNavigationItemSelectedListener(onNavigationItemSelectedListener)
navView.selectedItemId = R.id.navigation_home
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
}
override fun onPermissionsGranted(requestCode: Int, perms: List) {}
override fun onPermissionsDenied(requestCode: Int, perms: List) {
// (Optional) Check whether the user denied any permissions and checked "NEVER ASK AGAIN."
// This will display a dialog directing them to enable the permission in app settings.
if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
AppSettingsDialog.Builder(this).build().show()
}
}
override fun onRationaleAccepted(requestCode: Int) {}
override fun onRationaleDenied(requestCode: Int) {}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
textMessage.text = "${textMessage.text}\n接收到的result值:${data?.getStringExtra("message")}"
}
}
}
附上FlutterFileUtils代码
public class FlutterFileUtils {
///将文件拷贝到私有目录
public static String copyLibAndWrite(Context context, String fileName){
try {
File dir = context.getDir("libs", Activity.MODE_PRIVATE);
File destFile = new File(dir.getAbsolutePath() + File.separator + fileName);
if (destFile.exists() ) {
destFile.delete();
}
if (!destFile.exists()){
boolean res = destFile.createNewFile();
if (res){
String path = Environment.getExternalStorageDirectory().toString();
FileInputStream is = new FileInputStream(new File(path + "/" + fileName));
FileOutputStream fos = new FileOutputStream(destFile);
byte[] buffer = new byte[is.available()];
int byteCount;
while ((byteCount = is.read(buffer)) != -1){
fos.write(buffer,0,byteCount);
}
fos.flush();
is.close();
fos.close();
return destFile.getAbsolutePath();
}
}
}catch (IOException e){
e.printStackTrace();
}
return "";
}
}