Flutter android端快速实现热更新方案《基于1.12.13版本》

ps:无需反射~~~几行代码搞定~~~

下面我们来看看是怎么处理的

首先我们知道要想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 "";
    }
 
}

最后测试的时候,来回切换so包记得每次都需重启才能生效,因为flutter每次初始化完成后就不会在执行加载so的造作,在flutterloader类中变量initialized就哼好的说明了这一切。。。。。至此收工

你可能感兴趣的:(玩转flutter,flutter,android,studio,dart,kotlin,java)