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這個單例變?yōu)樽远x的MyFlutterLoader。重寫startInitialization()绳泉、ensureInitializationComplete()方法逊抡。下面上代碼,默認大家已經(jīng)集成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'
        }
    }

這里提供一個思路栗弟,可自行實現(xiàn)熱更新。核心不變工闺,操作shellArgs.add("--" + AOT_SHARED_LIBRARY_NAME + "=" + aotSharedLibraryName)替換aotSharedLibraryName乍赫。

FlutterLoader.ensureInitializationComplete()接收數(shù)組參數(shù)args

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

追本溯源,該參數(shù)在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揪惦,可自行實現(xiàn)遍搞。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市器腋,隨后出現(xiàn)的幾起案子溪猿,更是在濱河造成了極大的恐慌,老刑警劉巖纫塌,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诊县,死亡現(xiàn)場離奇詭異,居然都是意外死亡措左,警方通過查閱死者的電腦和手機依痊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怎披,“玉大人胸嘁,你說我怎么就攤上這事×构洌” “怎么了性宏?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長状飞。 經(jīng)常有香客問我毫胜,道長书斜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任酵使,我火速辦了婚禮荐吉,結果婚禮上,老公的妹妹穿的比我還像新娘口渔。我一直安慰自己稍坯,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布搓劫。 她就那樣靜靜地躺著瞧哟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪枪向。 梳的紋絲不亂的頭發(fā)上勤揩,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音秘蛔,去河邊找鬼陨亡。 笑死,一個胖子當著我的面吹牛深员,可吹牛的內容都是我干的负蠕。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼倦畅,長吁一口氣:“原來是場噩夢啊……” “哼遮糖!你這毒婦竟也來了?” 一聲冷哼從身側響起叠赐,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤欲账,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后芭概,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赛不,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年罢洲,在試婚紗的時候發(fā)現(xiàn)自己被綠了踢故。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡惹苗,死狀恐怖殿较,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情鸽粉,我是刑警寧澤斜脂,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站触机,受9級特大地震影響帚戳,放射性物質發(fā)生泄漏玷或。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一片任、第九天 我趴在偏房一處隱蔽的房頂上張望偏友。 院中可真熱鬧,春花似錦对供、人聲如沸位他。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鹅髓。三九已至,卻和暖如春京景,著一層夾襖步出監(jiān)牢的瞬間窿冯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工确徙, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留醒串,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓鄙皇,卻偏偏與公主長得像芜赌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子伴逸,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內容