Flutter 热更新-1.12.13+hotfix5

接上篇1.12.13版本启动流程

贴个效果图,这里并非跳转到两个不同页面=。=

hotfix.gif

上篇结尾分析过加载逻辑在FlutterLoader

private static FlutterLoader instance;

    /**
     * Returns a singleton {@code FlutterLoader} instance.
     * <p>
     * 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;
    }

熟悉的单例,hook这个单例变为自定义的MyFlutterLoader。重写startInitialization()、ensureInitializationComplete()方法。下面上代码,默认大家已经集成flutter作为module。

MyFlutterLoader

package com.chenxuan.flutter112.hotfix;

import android.app.Activity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
import android.view.WindowManager;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import io.flutter.BuildConfig;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.loader.FlutterLoader;
import io.flutter.util.PathUtils;
import io.flutter.view.VsyncWaiter;

public class MyFlutterLoader extends FlutterLoader {
    private static final String TAG = "FlutterLoader";

    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";

    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.
    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";

    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 boolean initialized = false;
    private Settings settings;

    @Nullable
    private ResourceExtractor resourceExtractor;

    @Override
    public void startInitialization(@NotNull Context applicationContext) {
        startInitialization(applicationContext, new Settings());
    }

    @Override
    public void startInitialization(@NotNull Context applicationContext, @NotNull Settings settings) {
        Log.d("chenxuan-------", "method---" + "startInitialization");
        // 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);
    }

    @Override
    public void ensureInitializationComplete(@NotNull Context applicationContext, String[] args) {
        Log.d("chenxuan-------", "method---" + "ensureInitializationComplete");
        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();
            }

            List<String> 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);

            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 {
                //shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + "fix.so");
                //关键点
                File dir = applicationContext.getDir("libs", Activity.MODE_PRIVATE);
                String libPath = dir.getAbsolutePath() + File.separator + "fix.so";
                shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + libPath);
                Log.d("chenxuan-------", libPath);
                // 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);
        }
    }

    /**
     * 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 `<meta-data>` tag as a direct child of `<application>` 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
    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);
        }
    }

    @NonNull
    private String fullAssetPathFrom(@NonNull String filePath) {
        return flutterAssetsDir + File.separator + filePath;
    }
}

三个无法导包的类ResourceCleaner、ResourceExtractor、ResourcePaths,复制一下。

ResourceExtractor

package com.chenxuan.flutter112.hotfix;

import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;

import com.chenxuan.flutter112.BuildConfig;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;

import static java.util.Arrays.asList;

public class ResourceExtractor {
    private static final String TAG = "ResourceExtractor";
    private static final String TIMESTAMP_PREFIX = "res_timestamp-";
    private static final String[] SUPPORTED_ABIS = getSupportedAbis();

    @SuppressWarnings("deprecation")
    static long getVersionCode(@NonNull PackageInfo packageInfo) {
        // Linter needs P (28) hardcoded or else it will fail these lines.
        if (Build.VERSION.SDK_INT >= 28) {
            return packageInfo.getLongVersionCode();
        } else {
            return packageInfo.versionCode;
        }
    }

    private static class ExtractTask extends AsyncTask<Void, Void, Void> {
        @NonNull
        private final String mDataDirPath;
        @NonNull
        private final HashSet<String> mResources;
        @NonNull
        private final AssetManager mAssetManager;
        @NonNull
        private final String mPackageName;
        @NonNull
        private final PackageManager mPackageManager;

        ExtractTask(@NonNull String dataDirPath,
                    @NonNull HashSet<String> resources,
                    @NonNull String packageName,
                    @NonNull PackageManager packageManager,
                    @NonNull AssetManager assetManager) {
            mDataDirPath = dataDirPath;
            mResources = resources;
            mAssetManager = assetManager;
            mPackageName = packageName;
            mPackageManager = packageManager;
        }

        @Override
        protected Void doInBackground(Void... unused) {
            final File dataDir = new File(mDataDirPath);

            final String timestamp = checkTimestamp(dataDir, mPackageManager, mPackageName);
            if (timestamp == null) {
                return null;
            }

            deleteFiles(mDataDirPath, mResources);

            if (!extractAPK(dataDir)) {
                return null;
            }

            if (timestamp != null) {
                try {
                    new File(dataDir, timestamp).createNewFile();
                } catch (IOException e) {
                    Log.w(TAG, "Failed to write resource timestamp");
                }
            }

            return null;
        }


        /// Returns true if successfully unpacked APK resources,
        /// otherwise deletes all resources and returns false.
        @WorkerThread
        private boolean extractAPK(@NonNull File dataDir) {
            for (String asset : mResources) {
                try {
                    final String resource = "assets/" + asset;
                    final File output = new File(dataDir, asset);
                    if (output.exists()) {
                        continue;
                    }
                    if (output.getParentFile() != null) {
                        output.getParentFile().mkdirs();
                    }

                    try (InputStream is = mAssetManager.open(asset);
                         OutputStream os = new FileOutputStream(output)) {
                        copy(is, os);
                    }
                    if (io.flutter.BuildConfig.DEBUG) {
                        Log.i(TAG, "Extracted baseline resource " + resource);
                    }
                } catch (FileNotFoundException fnfe) {
                    continue;

                } catch (IOException ioe) {
                    Log.w(TAG, "Exception unpacking resources: " + ioe.getMessage());
                    deleteFiles(mDataDirPath, mResources);
                    return false;
                }
            }

            return true;
        }
    }

    @NonNull
    private final String mDataDirPath;
    @NonNull
    private final String mPackageName;
    @NonNull
    private final PackageManager mPackageManager;
    @NonNull
    private final AssetManager mAssetManager;
    @NonNull
    private final HashSet<String> mResources;
    private ExtractTask mExtractTask;

    ResourceExtractor(@NonNull String dataDirPath,
                      @NonNull String packageName,
                      @NonNull PackageManager packageManager,
                      @NonNull AssetManager assetManager) {
        mDataDirPath = dataDirPath;
        mPackageName = packageName;
        mPackageManager = packageManager;
        mAssetManager = assetManager;
        mResources = new HashSet<>();
    }

    ResourceExtractor addResource(@NonNull String resource) {
        mResources.add(resource);
        return this;
    }

    ResourceExtractor addResources(@NonNull Collection<String> resources) {
        mResources.addAll(resources);
        return this;
    }

    ResourceExtractor start() {
        if (io.flutter.BuildConfig.DEBUG && mExtractTask != null) {
            Log.e(TAG, "Attempted to start resource extraction while another extraction was in progress.");
        }
        mExtractTask = new ResourceExtractor.ExtractTask(mDataDirPath, mResources, mPackageName, mPackageManager, mAssetManager);
        mExtractTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
        return this;
    }

    void waitForCompletion() {
        if (mExtractTask == null) {
            return;
        }

        try {
            mExtractTask.get();
        } catch (CancellationException | ExecutionException | InterruptedException e) {
            deleteFiles(mDataDirPath, mResources);
        }
    }

    private static String[] getExistingTimestamps(File dataDir) {
        return dataDir.list(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.startsWith(TIMESTAMP_PREFIX);
            }
        });
    }

    private static void deleteFiles(@NonNull String dataDirPath, @NonNull HashSet<String> resources) {
        final File dataDir = new File(dataDirPath);
        for (String resource : resources) {
            final File file = new File(dataDir, resource);
            if (file.exists()) {
                file.delete();
            }
        }
        final String[] existingTimestamps = getExistingTimestamps(dataDir);
        if (existingTimestamps == null) {
            return;
        }
        for (String timestamp : existingTimestamps) {
            new File(dataDir, timestamp).delete();
        }
    }

    // Returns null if extracted resources are found and match the current APK version
    // and update version if any, otherwise returns the current APK and update version.
    private static String checkTimestamp(@NonNull File dataDir,
                                         @NonNull PackageManager packageManager,
                                         @NonNull String packageName) {
        PackageInfo packageInfo = null;

        try {
            packageInfo = packageManager.getPackageInfo(packageName, 0);
        } catch (PackageManager.NameNotFoundException e) {
            return TIMESTAMP_PREFIX;
        }

        if (packageInfo == null) {
            return TIMESTAMP_PREFIX;
        }

        String expectedTimestamp =
                TIMESTAMP_PREFIX + getVersionCode(packageInfo) + "-" + packageInfo.lastUpdateTime;

        final String[] existingTimestamps = getExistingTimestamps(dataDir);

        if (existingTimestamps == null) {
            if (io.flutter.BuildConfig.DEBUG) {
                Log.i(TAG, "No extracted resources found");
            }
            return expectedTimestamp;
        }

        if (existingTimestamps.length == 1) {
            if (io.flutter.BuildConfig.DEBUG) {
                Log.i(TAG, "Found extracted resources " + existingTimestamps[0]);
            }
        }

        if (existingTimestamps.length != 1
                || !expectedTimestamp.equals(existingTimestamps[0])) {
            if (BuildConfig.DEBUG) {
                Log.i(TAG, "Resource version mismatch " + expectedTimestamp);
            }
            return expectedTimestamp;
        }

        return null;
    }

    private static void copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException {
        byte[] buf = new byte[16 * 1024];
        for (int i; (i = in.read(buf)) >= 0; ) {
            out.write(buf, 0, i);
        }
    }

    @SuppressWarnings("deprecation")
    private static String[] getSupportedAbis() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            return Build.SUPPORTED_ABIS;
        } else {
            ArrayList<String> cpuAbis = new ArrayList<String>(asList(Build.CPU_ABI, Build.CPU_ABI2));
            cpuAbis.removeAll(asList(null, ""));
            return cpuAbis.toArray(new String[0]);
        }
    }
}

ResourceCleaner

package com.chenxuan.flutter112.hotfix;

import android.content.Context;
import android.os.AsyncTask;
import android.os.Handler;
import android.util.Log;

import java.io.File;
import java.io.FilenameFilter;

import io.flutter.BuildConfig;

public class ResourceCleaner {
    private static final String TAG = "ResourceCleaner";
    private static final long DELAY_MS = 5000;

    private static class CleanTask extends AsyncTask<Void, Void, Void> {
        private final File[] mFilesToDelete;

        CleanTask(File[] filesToDelete) {
            mFilesToDelete = filesToDelete;
        }

        boolean hasFilesToDelete() {
            return mFilesToDelete != null && mFilesToDelete.length > 0;
        }

        @Override
        protected Void doInBackground(Void... unused) {
            if (BuildConfig.DEBUG) {
                Log.i(TAG, "Cleaning " + mFilesToDelete.length + " resources.");
            }
            for (File file : mFilesToDelete) {
                if (file.exists()) {
                    deleteRecursively(file);
                }
            }
            return null;
        }

        private void deleteRecursively(File parent) {
            if (parent.isDirectory()) {
                for (File child : parent.listFiles()) {
                    deleteRecursively(child);
                }
            }
            parent.delete();
        }
    }

    private final Context mContext;

    ResourceCleaner(Context context) {
        mContext = context;
    }

    void start() {
        File cacheDir = mContext.getCacheDir();
        if (cacheDir == null) {
            return;
        }

        final CleanTask task = new CleanTask(cacheDir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                boolean result = name.startsWith(ResourcePaths.TEMPORARY_RESOURCE_PREFIX);
                return result;
            }
        }));

        if (!task.hasFilesToDelete()) {
            return;
        }

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            }
        }, DELAY_MS);
    }
}

ResourcePaths

package com.chenxuan.flutter112.hotfix;

import android.content.Context;

import java.io.File;
import java.io.IOException;

public class ResourcePaths {
    // The filename prefix used by Chromium temporary file APIs.
    public static final String TEMPORARY_RESOURCE_PREFIX = ".org.chromium.Chromium.";

    // Return a temporary file that will be cleaned up by the ResourceCleaner.
    public static File createTempFile(Context context, String suffix) throws IOException {
        return File.createTempFile(TEMPORARY_RESOURCE_PREFIX, "_" + suffix,
                context.getCacheDir());
    }
}

MainActivity添加两个按钮,模拟热更新

package com.chenxuan.flutter112

import android.Manifest
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import com.chenxuan.flutter112.hotfix.FileUtils
import com.chenxuan.flutter112.hotfix.MyFlutterLoader
import com.tbruyelle.rxpermissions2.RxPermissions
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.loader.FlutterLoader
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val request = RxPermissions(this)
            .request(
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.READ_EXTERNAL_STORAGE
            )
            .subscribe { granted ->
                if (granted) {
                    init()
                } else {
                    Toast.makeText(this,"请打开权限", Toast.LENGTH_SHORT).show()
                }
            }
    }

    private fun init() {
        tv1.setOnClickListener {
            startActivity(
                FlutterActivity.createDefaultIntent(this)
            )
        }

        tv2.setOnClickListener {
            //将fix.so push到手机根目录,复制到data/data/package/app_libs/下
            FileUtils.copy(this@MainActivity, "fix.so")
            hookFlutterLoader()
            startActivity(
                FlutterActivity.createDefaultIntent(this)
            )
        }
    }

    private fun hookFlutterLoader() {
        try {
            val clazz = Class.forName("io.flutter.embedding.engine.loader.FlutterLoader")
            val instance = clazz.newInstance() as FlutterLoader
            val field = clazz.getDeclaredField("instance")
            field.isAccessible = true
            field.set(instance, MyFlutterLoader())
        } catch (e: Exception) {
            e.printStackTrace()
        }

    }
}

FileUtils

package com.chenxuan.flutter112.hotfix;

import android.app.Activity;
import android.content.Context;
import android.os.Environment;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileUtils {
    ///拷贝fix.so到私有目录
    public static String copy(Context context, String fileName) {
        try {
            File dir = context.getDir("libs", Activity.MODE_PRIVATE);
            File original = new File(dir.getAbsolutePath() + File.separator + fileName);
            if (original.exists()) {
                boolean delete = original.delete();
            }
            if (!original.exists()) {
                boolean bool = original.createNewFile();
                if (bool) {
                    String path = Environment.getExternalStorageDirectory().toString();
                    FileInputStream fileInputStream = new FileInputStream(new File(path + File.separator + fileName));
                    FileOutputStream fileOutputStream = new FileOutputStream(original);
                    byte[] buffer = new byte[fileInputStream.available()];
                    int count;
                    while ((count = fileInputStream.read(buffer)) != -1) {
                        fileOutputStream.write(buffer, 0, count);
                    }
                    fileInputStream.close();
                    fileOutputStream.flush();
                    fileOutputStream.close();
                    return original.getAbsolutePath();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }
}

接下来动手测试:
打release包安装apk。改动flutter module中main.dart代码,打新包,解压apk。将解压目录lib下libapp.so重命名为fix.so并push到手机根目录。点击tv1启动正常flutter页面,此时flutter页面按钮点击自增1;重启APP点击tv2加载fix.so热更新生效,此时flutter页面按钮点击自增100。

建议打armeabi-v7a单平台包,打多平台包需要把fix.so拷贝到对应的平台目录。例如:data/data/package/app_libs/armeabi-v7a/fix.so,单平台直接拷贝到data/data/package/app_libs/下即可。

app下build.gradle

defaultConfig {
       ...
        ndk {
            abiFilters 'armeabi-v7a'
        }
    }

这里提供一个思路,可自行实现热更新。核心不变,操作shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName)替换aotSharedLibraryName。

FlutterLoader.ensureInitializationComplete()接收数组参数args

public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) 

追本溯源,该参数在FlutterActivityAndFragmentDelegate.setupFlutterEngine()中传入

void setupFlutterEngine() {
...
flutterEngine = new FlutterEngine(host.getContext(), host.getFlutterShellArgs().toArray());
}

host是FlutterActivity,FlutterActivity.getFlutterShellArgs()

@Override
  public FlutterShellArgs getFlutterShellArgs() {
    return FlutterShellArgs.fromIntent(getIntent());
  }

可以在这里做文章,提前插入fix.so,可自行实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容