The Bug that can’t be changed, the pretense that can’t be written. The public account Yang Zhengyou now focuses on mobile basic development, covering audio and video, APM, information security and other knowledge fields; Only do the whole network the most Geek public number, welcome your attention!

preface

In general, as a mature application, native file will be more and more, a resource file Dan an eminent inclusion is more and more big, the growth of the package volume, gradually brought some adverse effects, such as user install the success rate is reduced, the CDN traffic cost increase, loss paid part of the channel, the new development of channel complain inclusions is too big, Limited channel expansion, then how to solve this business pain point? Today, WE will introduce Android dynamic management SO in detail

For example, Douyu Japan client project supports arm32/ ARM64 /x86/ x86-V7A at the same time,so volume increased exponentially. Therefore, whether non-major ABI related SO files can be dynamic has become a priority issue for slimming and optimization of international offshore projects. It is hoped that through package optimization, traffic cost can be reduced and user loss caused by large package can be avoided.

System loading so library workflow

What does the Android Framework do when we call System#loadLibrary(” XXX “)?

The general process diagram is as follows:

The market research

Scheme analysis:

1. JNI code built-in scheme

The code isolation scheme is more suitable for the new Native module, which runs in the direction of dynamic, lazy loading from the beginning.

2. Plug-in solution

Packing the SO file into a separate plugin package and keeping the JNI code inside the host code is a good plugin solution, but injecting the SO plugin path into a nativeLibraryDirectories creates a set of concurrent modification issues. Since the specific implementation of nativeLibraryDirectories is an ArrayList instance, the element reads and writes themselves are not thread-safe, At the end of loading the so plug-in in the Worker thread, we need to inject the new so file path into the ArrayList collection. If another thread happens to be traversing the collection elements because of “so loading”, Will throw ConcurrentModificationException (within the ArrayList)

Plan be born

After a round of investigation, it is found that Du Xiaocai’s SO dynamic loading scheme is still worth recommending. It has the following advantages:

    1. After the path is injected, the posture of loading SO remains unchanged
    1. Support for various CPU architecture platforms
    1. Load SO on demand

A problem that needs to be solved

1. Security issues

All executable code is at risk of being hijacked or corrupted before being copied to a secure installation path (such as Android’s data/data internal path). So dynamic also has to consider this security issue, the best practice is to do a security check before loading the SO library. So how do you do security checks?

The simplest way is to record MD5 or CRC Hash information of SO files (granularity can be each individual SO file or a batch of SO file compression package), and embed the information inside APK or server (if stored in the server, The client needs to obtain this data over a trusted channel such as HTTPS) to verify that the so file Hash information is consistent to ensure security.

Let’s take a look at the code implementation

When entering the core activity, enable the delay detection. Stop the download first to reduce the impact on the page loading. Re-download x seconds later
    public void checkArmSoDelayWhenCreate(int delayMillis) {
        //test(); // Todo is enabled for testing, and the following code is commented to facilitate testing the interaction between downloading and loading so

        if (localSoStatus.isDownloading) {
            ThreadManager.getBackgroundPool().execute(this::pauseDownloadTask);
        }
        weakHandler.removeCallbacks(startCheckRunnable);
        weakHandler.postDelayed(startCheckRunnable, delayMillis);
    }
Copy the code

Put it in a separate thread to check if the SO is complete

    private void checkSoLibsInBackThread(a) {
        weakHandler.removeCallbacks(startCheckRunnable);
        final ThreadManager.ThreadPoolProxy singlePool = ThreadManager.getSinglePool("so-download");
        // Avoid creating duplicate detection tasks.
        singlePool.remove(checkSoRunnable);
        singlePool.execute(checkSoRunnable);
    }

Copy the code

Let’s look at the specific detection logic in detail

  • 3.1 If the ZIP file exists, check whether it is valid. Md5 check is performed
  String soZipPath = soFileDownloader.getSoZipFilePath(SOURCE_MD5);
        final boolean allSoFilesExist = isAllSoFilesExist(soZipPath);
        // So does not exist when statistics trigger detection
        StatisticsForSoLoader.sendSoFilesNotExist(allSoFilesExist);
        boolean hasInstalledSoPath = soFileDownloader.hasInstalledSoPath();
        localSoStatus.hasInstalledSoPath = hasInstalledSoPath;
        final boolean isPrepared = allSoFilesExist && hasInstalledSoPath;
Copy the code
  • 3.2 Complete decompression, incomplete deletion of cache, download again
      localSoStatus.isPrepared = isPrepared;
       Log.d(TAG, "handleSoBackground isPrepared=" + isPrepared);
       if (isPrepared) {// All ready, call back, ok
           if(soLoaderListener ! =null) {
               soLoaderListener.prepared(true);
           } else {// call back to continue execution
               MkWeexSoLoader.reloadWeexSoLib(this::notifyCallback);
           }
           return;
       }

           private void startDownload(SoLoaderListener soLoaderListener, String soZipPath) {
               //pauseDownloadTask(); // Pause the previous task before each download to prevent so reading and writing problems
               String soUrl = getServerUrl();
               soFileDownloader.downloadSoFile(soUrl, soZipPath, soLoaderListener);
           }
Copy the code
  • 3.3 Whether the so file specified in soNameList exists
       for (File currentFile : currentFiles) {
            final String currentFileName = currentFile.getName();
            //so library, size>0, and is pre-defined legitimate SO, count so number
            final boolean contains = allSoNameList.contains(currentFileName);
            if (currentFileName.endsWith(".so") && currentFile.length() > 0&& contains) { localSoFileCount++; }}// If the total number of so files in the local download directory is less than the number of so files there should be, it is not complete
        localSoStatus.isAllSoFilesExist = localSoFileCount >= allSoNameList.size();
        return localSoStatus.isAllSoFilesExist;
Copy the code
  • Then download the ZIP package of the SO library and check whether the MD5 values on the server are consistent with those on the client
          localSoStatus.hasInstalledSoPath = hasInstalledSoPath;
                localSoStatus.hasDownloadSoZip = true;// flag whether the download was successful
                localSoStatus.isZipLegal = checkSoZipMd5;// Indicates whether zip is valid, md5 ok is valid
                localSoStatus.isDownloading = false;
                boolean isPrepared = false;
                if(! checkSoZipMd5) { StatisticsForSoLoader.sendZipFileStatus(false);// Statistics are invalid
                    deleteOldCache();// Invalid delete
                    if (countRetryDownload < 3) {// retry
                        reStartDownload();
                        countRetryDownload++;
                        return;
                    }
                    notifyPreparedCallback(false);
                }
Copy the code

Check whether the ZIP has been updated. If it has been updated, download it again

  public boolean isZipNeedUpdate(String md5) {
        String zipDirPath = getDownloadZipTempDir() + File.separator + md5;
        File zipRootFile = new File(zipDirPath);
        if(! zipRootFile.exists()) {// If the md5 zip cache path does not exist, you need to download it again.
            Log.d(TAG, "app upgrade...");
            StatisticsForSoLoader.sendOverwriteInstallApp();
            deleteOldCache();//so update to remove old ZIP cache and so files
            return true;
        }
        return false;
    }
Copy the code
  • After the download is complete, decompress, decompression often fails, so you need to decompress twice
       // Download succeeded
                    handler.post(() -> {
                        if(listener ! =null) {
                            listener.download(true, soUrl); }});/ /
                    final boolean unZip = doUnZip(soZipPath);

                    // re-check file integrity
                    isPrepared = notifyPrepared(hasInstalledSoPath);

                    localSoStatus.isPrepared = isPrepared;
                    localSoStatus.isUnZip = unZip;// flag whether the download was successful

                    if (isPrepared) {// Load the WEEX library again
                        MkWeexSoLoader.reloadWeexSoLib(() -> notifyPreparedCallback(true));
                    } else {
                        notifyPreparedCallback(false);
                    }
Copy the code
  • The core is implemented via ZipInputStream
            is = new ZipInputStream(new FileInputStream(zipFilePath));
            ZipEntry zipEntry;
            while((zipEntry = is.getNextEntry()) ! =null) {
                String subfilename = zipEntry.getName();
                if (zipEntry.isDirectory()) {
                    File subDire = new File(folderPath + subfilename);
                    if (subDire.exists() && subDire.isDirectory()) {
                        continue;
                    } else if (subDire.exists() && subDire.isFile()) {
                        subDire.delete();
                    }
                    subDire.mkdirs();
                } else {
                    File subFile = new File(folderPath + subfilename);
                    if (subFile.exists()) {
                        continue;
                    }
                    final File parentFile = subFile.getParentFile();
                    if(parentFile ! =null && !parentFile.exists()) {
                        parentFile.mkdirs();
                    }
                    subFile.createNewFile();
                    os = new FileOutputStream(subFile);
                    int len;
                    byte[] buffer = new byte[5120];
                    while((len = is.read(buffer)) ! = -1) {
                        os.write(buffer, 0, len); os.flush(); }}}Copy the code
  • After decompression, determine whether the so file is decompressed and complete. If it is incomplete, it needs to be downloaded. Before downloading, pause the last task to prevent so reading and writing problems
    private void startDownload(SoLoaderListener soLoaderListener, String soZipPath) {
        pauseDownloadTask();// Pause the previous task before each download to prevent so reading and writing problems
        String soUrl = getServerUrl();
        soFileDownloader.downloadSoFile(soUrl, soZipPath, soLoaderListener);
    }
Copy the code
  • If the total number of so files in the local download directory is less than the number of so files predefined in the collection, it is not complete
  public boolean isSoUnzipAndExist(a) {
        String targetSoDir = SoFileDownloader.getLocalSoInstallDir();
        File dir = new File(targetSoDir);
        File[] currentFiles = dir.listFiles();
        if (currentFiles == null || currentFiles.length == 0) {
            return false;
        }
        int localSoFileCount = 0;
        for (File currentFile : currentFiles) {
            final String currentFileName = currentFile.getName();
            //so library, size>0, and is pre-defined legitimate SO, count so number
            final boolean contains = allSoNameList.contains(currentFileName);
            if (currentFileName.endsWith(".so") && currentFile.length() > 0&& contains) { localSoFileCount++; }}// If the total number of so files in the local download directory is less than the number of so files there should be, it is not complete
        localSoStatus.isAllSoFilesExist = localSoFileCount >= allSoNameList.size();
        return localSoStatus.isAllSoFilesExist;
    }
Copy the code
  • Check to see if the ZIP package exists and unzip it again if so
    public synchronized boolean checkSoZipMd5(String soZipPath) {
        if (TextUtils.isEmpty(soZipPath)) {
            return false;
        }
        final File localSoZipFile = new File(soZipPath);
        if(! localSoZipFile.exists() || localSoZipFile.length() ==0) {
            return false;
        }
        final String localSoMd5 = MD5Utils.getMd5(localSoZipFile);
        //Logger.d(TAG, "localSoMd5=" + localSoMd5);
        final boolean md5Equals = MkSoManager.SOURCE_MD5.equals(localSoMd5);
        if(! md5Equals) {// Delete illegal zip package directly, undownloaded package is not this path, rest easy!
            FileUtils.deleteFile(soZipPath);
        }
        return md5Equals;
    }
Copy the code
  • After decompressing, inject the driver directly through injectLocalSoLibraryPath
    /** * specify the path to download so */
    public static boolean installLocalSoPath(Context context) {
        try {
            String targetSoDir = SoFileDownloader.getLocalSoInstallDir();
            File soDir = new File(targetSoDir);
            if(! soDir.exists()) { soDir.mkdirs(); }final ClassLoader classLoader = context.getApplicationContext().getClassLoader();
            boolean hasInstalledSoPath = LoadLibraryUtil.injectLocalSoLibraryPath(classLoader, soDir);
            if(! hasInstalledSoPath) {// Only injection failures are counted
                StatisticsForSoLoader.sendInstallPathStatus(false);
                Log.d(TAG, "installLocalSoPath=" + false + ", targetDir =" + targetSoDir);
            }
            MkSoManager.get().getLocalSoStatus().hasInstalledSoPath = hasInstalledSoPath;
            return hasInstalledSoPath;
        } catch (Throwable e) {
            Log.e(TAG, "installLocalSoPath error " + e);
        }
        return false;
    }
Copy the code

However, the Hash information will generally change with the change of the SO file, so it is troublesome to adjust these data every time. The optimization solution is “to ensure security by means of similar APK installation package signature verification” : Package the so file into APK plug-in package and use The Android Keystore for signature. Save the Keystore fingerprint information inside the host package. The security verification process only needs to verify whether the signature information of the plug-in package is consistent with the built-in fingerprint information, please refer to the link in the article for details

2. Version control problems

We have released a certain version of host APK and the corresponding SO plug-in package, but the SO of this version is buggy, which may cause the APP to crash. Through the versioning process, we can disable this version of the SO plug-in on the server side, so that the client can enter the “so plug-in is not available” logic without executing the problematic code. So how does the code do that? Pseudocode is provided here:

    public static void checkX86AndDownload(String baseDownloadUrl) {
        //final boolean isX86Phone = isX86Phone();
        // TODO:2017/8/3 Need to rebuild the download information, SDK information + online address and version number
        if(! checkSoVersion()) {/ / | |! You do not need to download the isX86Phone
            return;
        }
        // The todo interface gets the download path and version information
        String cpuABI = MkSoManager.getMkSupportABI();
        String soUrl = baseDownloadUrl + cpuABI + ".zip";
        SoFileDownloader.init().downloadSoFile(cpuABI, soUrl);
    }

    /** * Determine whether to download */ based on the server version configuration
    private static boolean checkSoVersion(a) {
        // TODO:2017/8/3 Verify with the server to conform to the version corresponding to the current SDK, and check the integrity of the local SO file
        return true;
    }
Copy the code

3. Determine ABI compatibility

Abi compatibility is a dynamic issue that is unique to the SO plug-in. In addition to considering whether the SO plug-in is secure, we also need to check whether the SO library ABI information in the SO plug-in package is consistent with the ABI of the host’s current runtime. Consider this scenario: The host APK contains two kinds of SO files, ARM32 and AMR64, which are also embedded in the plug-in package. When the host APK is installed on the ARM32 device and the so plug-in is dynamically loaded, we must only decompress and load the corresponding AMR32 SO plug-in. The same is true for ARM64 devices. That is: the same APK host, the same SO plug-in, installed on different ABI devices, dynamic framework plug-in processing behavior is different, so what is the implementation logic?

First, define a LocalSoStatus class so that businesses can customize the extension of the download logic

public class LocalSoStatus {
    public boolean hasInstalledSoPath = false;// Whether to inject the so path
    public boolean isDownloading = false;// Whether you are downloading
    public int progress = 0;// Download progress
    public boolean hasStartDownload = false;// Whether the download has been started
    public boolean hasDownloadSoZip = false;// Check whether the zip file is downloaded successfully
    public boolean isZipLegal = false;// Check whether the zip file is valid
    public boolean isUnZip = false;// Check whether the zip file is decompressed successfully
    public boolean isAllSoFilesExist = false;// so whether the file exists completely
    public boolean isPrepared = false;// so is successfully injected and the local file is complete
    public boolean hasLoadSoSuccess = false;// Test whether the so library has been loaded successfully

    @Override
    public String toString(a) {
        return "LocalSoStatus{" +
                "hasInstalledSoPath=" + hasInstalledSoPath +
                ", isDownloading=" + isDownloading +
                ", progress=" + progress +
                ", hasStartDownload=" + hasStartDownload +
                ", hasDownloadSoZip=" + hasDownloadSoZip +
                ", isZipLegal=" + isZipLegal +
                ", isUnZip=" + isUnZip +
                ", isAllSoFilesExist=" + isAllSoFilesExist +
                ", isPrepared=" + isPrepared +
                ", hasLoadSoSuccess=" + hasLoadSoSuccess +
                '} '; }}Copy the code
  • So you download a specified path directly, through reflection for android. The OS. The SystemProperties private method get ro. Product. CPU. Abi can dynamically obtain CPU architecture
    /** * Obtain the CPU architecture type of the device */
    public static String getCpuArchType(a) {
        if(! TextUtils.isEmpty(cpuArchType)) {return cpuArchType;
        }
        try{ Class<? > clazz = Class.forName("android.os.SystemProperties");
            Method get = clazz.getDeclaredMethod("get".new Class[]{String.class});
            cpuArchType = (String) get.invoke(clazz, new Object[]{"ro.product.cpu.abi"});
        } catch (Exception e) {
        }

        try {
            if (TextUtils.isEmpty(cpuArchType)) {
                cpuArchType = Build.CPU_ABI;// Do not obtain, reobtain, may not be accurate?}}catch (Exception e) {
        }
        if (TextUtils.isEmpty(cpuArchType)) {
            cpuArchType = "armeabi-v7a";
        }
        cpuArchType = cpuArchType.toLowerCase();
        return cpuArchType;
    }
Copy the code

4. System#load code intrusion problem

Use system.load (“{safe path}/libxxx.so”). Native code can be debugged with traditional built-in solutions during development and packaged with dynamic solutions during integration, which means that we have to frequently change directly between System#load and System#loadLibrary(” XXX “). Code intrusion is a serious problem

Loading the so library with System#loadLibrary(” XXX “), the Android Framework iterates through the nativeLibraryDirectories array in the ClassLoader instance in the current context, It looks for a file named libxxx.so in all the directories in the array, so the solution is to inject the internal secure directories into the nativeLibraryDirectories after the so plugin is installed. System#loadLibrary:

Step 1: Inject the so file into the nativeLibraryDirectories via reflection
    private static final class V14 {
        private static void install(ClassLoader classLoader, File folder) throws Throwable {
           // reflect the pathList member of the ClassLoader of the host APK
            Field pathListField = MkReflectUtil.findField(classLoader, "pathList");
            // Gets the value of this member variable in the ClassLoader object of the host APK
            Object dexPathList = pathListField.get(classLoader);
            // Store loaded instances of so to dexPathList
            MkReflectUtil.expandArray(dexPathList, "nativeLibraryDirectories".newFile[]{folder}); }}Copy the code

Note that different system SDK versions need to perform different reflection logic

SDK version: 14
    private static final class V14 {
        private static void install(ClassLoader classLoader, File folder) throws Throwable {
           // reflect the pathList member of the ClassLoader of the host APK
            Field pathListField = MkReflectUtil.findField(classLoader, "pathList");
            // Gets the value of this member variable in the ClassLoader object of the host APK
            Object dexPathList = pathListField.get(classLoader);
            // Store loaded instances of so to dexPathList
            MkReflectUtil.expandArray(dexPathList, "nativeLibraryDirectories".newFile[]{folder}); }}Copy the code
SDK version: 23
 private static final class V23 {
        private static void install(ClassLoader classLoader, File folder) throws Throwable {
            Field pathListField = MkReflectUtil.findField(classLoader, "pathList");
            Object dexPathList = pathListField.get(classLoader);

            Field nativeLibraryDirectories = MkReflectUtil.findField(dexPathList, "nativeLibraryDirectories");
            List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);

            / / to heavy
            if (libDirs == null) {
                libDirs = new ArrayList<>(2);
            }
            final Iterator<File> libDirIt = libDirs.iterator();
            while (libDirIt.hasNext()) {
                final File libDir = libDirIt.next();
                if (folder.equals(libDir)) {
                    libDirIt.remove();
                    Log.d(TAG, "dq libDirIt.remove() " + folder.getAbsolutePath());
                    break;
                }
            }

            libDirs.add(0, folder);
            Field systemNativeLibraryDirectories =
                    MkReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
            List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);

            / / found empty
            if (systemLibDirs == null) {
                systemLibDirs = new ArrayList<>(2);
            }
            //Log.d(TAG, "dq systemLibDirs,size=" + systemLibDirs.size());

            // Get the Element[] array
            Method makePathElements = MkReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class, List.class);
            ArrayList<IOException> suppressedExceptions = new ArrayList<>();
            libDirs.addAll(systemLibDirs);
           // Outputs the call object, the plugin APK directory, the plugin APK full path, and the List used to store IO exceptions, and returns Element[]
            Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs, null, suppressedExceptions);
            Field nativeLibraryPathElements = MkReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
            nativeLibraryPathElements.setAccessible(true); nativeLibraryPathElements.set(dexPathList, elements); }}Copy the code
SDK version: 25
 private static final class V25 {
        private static void install(ClassLoader classLoader, File folder) throws Throwable {
            Field pathListField = MkReflectUtil.findField(classLoader, "pathList");
            Object dexPathList = pathListField.get(classLoader);
            Field nativeLibraryDirectories = MkReflectUtil.findField(dexPathList, "nativeLibraryDirectories");

            List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
            / / to heavy
            if (libDirs == null) {
                libDirs = new ArrayList<>(2);
            }
            final Iterator<File> libDirIt = libDirs.iterator();
            while (libDirIt.hasNext()) {
                final File libDir = libDirIt.next();
                if (folder.equals(libDir)) {
                    libDirIt.remove();
                    Log.d(TAG, "dq libDirIt.remove()" + folder.getAbsolutePath());
                    break;
                }
            }

            libDirs.add(0, folder);
            //system/lib
            Field systemNativeLibraryDirectories = MkReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
            List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);

            / / found empty
            if (systemLibDirs == null) {
                systemLibDirs = new ArrayList<>(2);
            }
            //Log.d(TAG, "dq systemLibDirs,size=" + systemLibDirs.size());

            Method makePathElements = MkReflectUtil.findMethod(dexPathList, "makePathElements", List.class);
            libDirs.addAll(systemLibDirs);

            Object[] elements = (Object[]) makePathElements.invoke(dexPathList, libDirs);
            Field nativeLibraryPathElements = MkReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
            nativeLibraryPathElements.setAccessible(true); nativeLibraryPathElements.set(dexPathList, elements); }}Copy the code

The logic for injecting the SO path is as follows:

  1. APK ClassLoader’s pathList member variable,
  2. PathList is actually a List instance of SoPathList, the internal member variable of an instance of the class
  3. This List stores instances of the loaded so file

Let’s take a look at the code implementation

    /** * 1. Get the dexElements value by reflection * 2. Insert the object[] obtained by the findField method at the top of the array. * 3. The inserted object[] array is the compiled queue of the external fix pack storage path collection * that is, the resources and.class queue of the external fix pack *@paramInstance host the member variable pathList of the ClassLoader instance of APK (DexPathList is similar) *@paramFieldName The member variable "dexElements" of the DexPathList class object that needs to be reflected and replaced, used to store the.dex loading object dex *@paramExtraElements is loaded with a list of.dex instances of the plug-in APk */
    public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        // 1 Obtain the member variable dexElements of the classLoader instance pathList(an instance of the DexPathList class) by reflection
        Field jlrField = findField(instance, fieldName);
        // 2 Gets the value of the current dexElements member variable in the pathList(an instance of the DexPathList class) of the classLoader instance
        Object[] original = (Object[]) jlrField.get(instance);
        // 3 Create an array containing elements[] from the host apk. dex file and elements[] from the plugin apk. dex file.
        Object[] combined = (Object[]) Array.newInstance(original.getClass().getComponentType(), original.length + extraElements.length);
      Elements [] and dexFileArr obtained from apk are copied into the array for dynamic loading
        System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
       // 5 Copy all apK's dexElements values into the array
        System.arraycopy(original, 0, combined, extraElements.length, original.length);
         // 6 Overrides the value of the dexElements member variable
        jlrField.set(instance, combined);
    }
Copy the code

Problems encountered

Problem a

N At first things are different: libxxx.so loads normally, while liblog.so fails to load

E/ExceptionHandler: Uncaught Exception java.lang.UnsatisfiedLinkError: dlopen failed: library "liblog.so" not found
at java.lang.Runtime.loadLibrary0(Runtime.java:xxx)
at java.lang.System.loadLibrary(System.java:xxx)
Copy the code
Cause analysis of problem 1

After Android P, the path retrieved in Linker is bound to the Namespace mechanism after the ClassLoader instance is created. When we inject the new path, although the path in ClassLoader is increased, However, the set of paths bound to the Namespace in Linker is not updated synchronously

Problem one solution

Fully control the retrieval logic of so files

ARM mobile phone actively detects SO. When entering the core activity, enable delay detection, stop downloading first to reduce the impact on page loading, and download again x seconds later

    public void checkArmSoDelayWhenCreate(int delayMillis) {

        if (localSoStatus.isDownloading) {
            ThreadManager.getBackgroundPool().execute(this::pauseDownloadTask);
        }
        weakHandler.removeCallbacks(startCheckRunnable);
        weakHandler.postDelayed(startCheckRunnable, delayMillis);
    }
Copy the code

Put it in a separate thread to check if the SO is complete

    private void checkSoLibsInBackThread(a) {
        weakHandler.removeCallbacks(startCheckRunnable);
        final ThreadManager.ThreadPoolProxy singlePool = ThreadManager.getSinglePool("so-download");
        // Avoid creating duplicate detection tasks.
        singlePool.remove(checkSoRunnable);
        singlePool.execute(checkSoRunnable);
    }

Copy the code

Let’s look at the specific detection logic in detail

If the zip file exists, check whether it is valid. Md5 check

  String soZipPath = soFileDownloader.getSoZipFilePath(SOURCE_MD5);
        final boolean allSoFilesExist = isAllSoFilesExist(soZipPath);
        // So does not exist when statistics trigger detection
        StatisticsForSoLoader.sendSoFilesNotExist(allSoFilesExist);
        boolean hasInstalledSoPath = soFileDownloader.hasInstalledSoPath();
        localSoStatus.hasInstalledSoPath = hasInstalledSoPath;
        final boolean isPrepared = allSoFilesExist && hasInstalledSoPath;
Copy the code

Complete decompression, incomplete deletion cache, download again

      localSoStatus.isPrepared = isPrepared;
       Log.d(TAG, "handleSoBackground isPrepared=" + isPrepared);
       if (isPrepared) { // All ready, call back, ok
           if(soLoaderListener ! =null) {
               soLoaderListener.prepared(true);
           } else { // call back to continue execution
               MKWeexSoLoader.reloadWeexSoLib(this::notifyCallback);
           }
           return;
       }

           private void startDownload(SoLoaderListener soLoaderListener, String soZipPath) {
               //pauseDownloadTask(); // Pause the previous task before each download to prevent so reading and writing problems
               String soUrl = getServerUrl();
               soFileDownloader.downloadSoFile(soUrl, soZipPath, soLoaderListener);
           }
Copy the code

Is there any so file specified in soNameList

       for (File currentFile : currentFiles) {
            final String currentFileName = currentFile.getName();
            // so library, size>0, and is pre-defined legitimate SO, count so number
            final boolean contains = allSoNameList.contains(currentFileName);
            if (currentFileName.endsWith(".so") && currentFile.length() > 0&& contains) { localSoFileCount++; }}// If the total number of so files in the local download directory is less than the number of so files there should be, it is not complete
        localSoStatus.isAllSoFilesExist = localSoFileCount >= allSoNameList.size();
        return localSoStatus.isAllSoFilesExist;
Copy the code

Problem two: Moving the associated load code out of the static code block

After the so dynamic transformation, if someone accidentally references the relevant JNI class in the subsequent development of the project before the SO plug-in is installed, it is better to move the relevant loading code out of the static code block when the transformation is dynamic, and add the onFail logic when the SO load fails

  • If it is an X86 phone, initialize the list of so file names for X86 platforms
    isX86Phone = isX86Phone();
         if (isX86Phone) {
            SOURCE_MD5 = MD5_X86;
            initX86SoFileNameList();
        }
            private void initX86SoFileNameList(a) {
                if(allSoNameList ! =null) {
                    addAgoraSoLibs();/ / XXXXXX library
                     ```
                }
            }
Copy the code
  • If it is an Armeabi-v7a phone, initialize the list of so file names for the Armeabi-v7a platform
else {
            SOURCE_MD5 = MD5_ARMEABI_V7A;
            initArmSoFileNameList();
        }

private void initArmSoFileNameList(a) {
                if(allSoNameList ! =null) { addAgoraSoLibs(); addWeexSolibs(); }}private void addAgoraSoLibs(a) {
        allSoNameList.add("xxxxxx.so"); ` ` ` ` ` `}private void addWeexSolibs(a) {// WeeX core libraries, x86 arm need to be delivered, do not add unnecessary
           allSoNameList.add("xxxxxx.so"); ` ` ` ` ` `}Copy the code
  • Check if the SO library is ready. ARM mobile phone only has sound network related business and weeX creation, so needs to be detected
  • Nothing else is required; X86 phones need to be tested anyway
    public void checkSoLibReady(Context context, boolean isNeedCheckWhenArm, CheckSoCallback callback) {
        if (isX86Phone && isNeedCheckWhenArm) {// If it is an x86 phone, ignore what ARM needs to detect
            doCallback(callback);
            return;
        }
        if(! isX86Phone && ! isNeedCheckWhenArm) {// Arm mobile phone, ignore without detection
            doCallback(callback);
            return;
        }
        this.mCallback = callback;
        boolean doCheck = doCheck(context);
        if (doCheck) {// Go back to callbackdoCallback(callback); }}Copy the code

The callback to detect SO is then passed on to the business layer for processing

public interface CheckSoCallback {
    void prepared(a);
}
Copy the code

Problem 3: Dynamic code is disabled in Google Play Store

APK packages containing dynamic codes cannot be uploaded to the Play Store. You can deliver the binding version of “One main resource package + one Patch package” to the APK client, with a maximum volume of 1 GB. So is dynamic and version-bound and cannot be changed once released

Fault 4: Some ROM models delete the build.version.preview_sdk_int attribute, so SDK VERSION information cannot be obtained

   @TargetApi(Build.VERSION_CODES.M)
    private static int getPreviousSdkInt(a) {
        try {
            return Build.VERSION.PREVIEW_SDK_INT;
        } catch (Throwable ignore) {
        }
        return 1;
    }
Copy the code

conclusion

In actual projects, SO dynamic delivery encountered a lot of pits, familiar with the system loading so library workflow and reflection process. To address security issues during dynamic processes, versioning issues,abi compatibility judgments, and System#load code intrusion. Of course, theory is the cornerstone. Only by analyzing so state and network state online can we ensure the stability of our application online. There is so much to talk about about App weight loss, if this article has been read more than 2000, the next section will write about PNG to WebPNG automatic conversion tutorial, to satisfy your curiosity about App weight loss