Loading SO Library in SD Card Supplemented by Android Dynamic Loading

Keywords: Android Java SDK Google

Reprinted from: https://segmentfault.com/a/1190000004062899

JNI and NDK

In fact, the use of JNI in Android includes dynamic loading, dynamic loading of the. so library at APP runtime and invoking its encapsulated method through JNI. The latter is usually compiled from C/C++ code using NDK tools, running in the Native layer, which is much more efficient than Java code executing in the virtual machine. Therefore, Android often loads. so libraries dynamically to accomplish some performance-demanding tasks (such as T9 search, or Bitmap decoding, image Gauss blurring, etc.). In addition, because the. so library is compiled by C++ and can only be decompiled into assembly code, it is more difficult to crack than Smali, so the. so library can also be used in the field of security.

Unlike what we often say about dynamic loading based on lassLoader, SO libraries are loaded using System classes (so support for SO libraries is also the basic function of Android), so this is a supplementary explanation. However, if you use ClassLoader to load SD card plug-in APK, and plug-in APK contains SO libraries, which involves loading SO libraries in plug-in APK, so we also need to know how to load SO libraries in SD card.

Use Posture of General SO Files

For example, if you use Java code to calculate every pixel of image Bitmap, the overall time-consuming will be very large, so you can consider using JNI. (There are many detailed JNI tutorials on the Web, not to mention here.)

Here we recommend an open source Gauss blur project. Android StackBlur

Locate the directory of the Android.mk file on the command line and run the ndk-build command of the NDK tool to compile the SO library we need.

Copy the SO library to the jniLibs directory of the Android Studio project

(Android Studio now supports direct compilation of SO libraries, but there are many pits where I choose to compile manually.)

Then load the module corresponding to SO Library in Java.

// load so file from internal directory
        try {
            System.loadLibrary("stackblur");
            NativeBlurProcess.isLoadLibraryOk.set(true);
            Log.i("MainActivity", "loadLibrary success!");
        } catch (Throwable throwable) {
            Log.i("MainActivity", "loadLibrary error!" + throwable);
        }

Once the load is successful, you can use the Native method directly.

public class NativeBlurProcess {
    public static AtomicBoolean isLoadLibraryOk = new AtomicBoolean(false);
    //native method
    private static native void functionToBlur(Bitmap bitmapOut, int radius, int threadCount, int threadIndex, int round);
    }

Thus, in the Android project, the use of SO libraries is also a dynamic load, which loads executable files at run time. In general, SO libraries are packaged in APK and are not allowed to be modified. This kind of "dynamic loading" does not seem to be the kind we are familiar with. It seems that there is no use for eggs. However, SO libraries can also be stored in external storage paths.

How to Store SO Files in External Storage

Notice that when loading SO libraries above, we used the "loadLibrary" method of the System class. At the same time, we found that the System class also has a "load" method. It looks similar. Let's see how they differ.

/**
     * See {@link Runtime#load}.
     */
    public static void load(String pathName) {
        Runtime.getRuntime().load(pathName, VMStack.getCallingClassLoader());
    }

    /**
     * See {@link Runtime#loadLibrary}.
     */
    public static void loadLibrary(String libName) {
        Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
    }

First look at the loadLibrary, which calls Runtime's loadLibrary, and then goes in and loads the familiar Class Loader dynamically (which also proves that the use of SO libraries is a kind of dynamic loading).

    /*
     * Searches for and loads the given shared library using the given ClassLoader.
     */
    void loadLibrary(String libraryName, ClassLoader loader) {
        if (loader != null) {
            String filename = loader.findLibrary(libraryName);
            String error = doLoad(filename, loader);
            return;
        }
        ......
    }

It looks like getting a file path through the library name, then calling the "doLoad" method to load the file, first look at "loader.findLibrary(libraryName)"

    protected String findLibrary(String libName) {
        return null;
    }

ClassLoader is just an abstract class, and most of its work is implemented in the BaseDexClassLoader class. Let's go in and see

public class BaseDexClassLoader extends ClassLoader {
    public String findLibrary(String name) {
        throw new RuntimeException("Stub!");
    }
}

No, here's just a Runtime Exception exception thrown. Nothing has been done!

In fact, there is a misunderstanding here, which is easy to confuse the students who just started to open Android SDK source code. The source code of Android SDK is only for our developers to refer to, basically just some commonly used classes. Google will not put the source code of the entire Android system here, because the whole project is very large, ClassLoader class we usually have little contact with, so the source code of its specific implementation is not packaged into SDK. If necessary, we need to go to the official AOSP project. Look (by the way, the whole AOSP 5.1 project is over 150 GB in size, and a mobile hard disk storage is recommended if necessary).

For convenience, we can look directly at the online code. BaseDexClassLoader.java

@Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }

Look at the DexPathList class again

/**
     * Finds the named native code library on any of the library
     * directories pointed at by this instance. This will find the
     * one in the earliest listed directory, ignoring any that are not
     * readable regular files.
     *
     * @return the complete path to the library or {@code null} if no
     * library was found
     */
    public String findLibrary(String libraryName) {
        String fileName = System.mapLibraryName(libraryName);
        for (File directory : nativeLibraryDirectories) {
            File file = new File(directory, fileName);
            if (file.exists() && file.isFile() && file.canRead()) {
                return file.getPath();
            }
        }
        return null;
    }

Now it's clear that, according to the libName passed in, scan the native library directory inside the APK to get and return the complete path filename of the internal SO library file. Go back to the Runtime class, get filename and call the "doLoad" method. Look at this

private String doLoad(String name, ClassLoader loader) {
        String ldLibraryPath = null;
        String dexPath = null;
        if (loader == null) {
            ldLibraryPath = System.getProperty("java.library.path");
        } else if (loader instanceof BaseDexClassLoader) {
            BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
            ldLibraryPath = dexClassLoader.getLdLibraryPath();
        }
        synchronized (this) {
            return nativeLoad(name, loader, ldLibraryPath);
        }
    }

So it's clear to call the Native method "nativeLoad" to load the target SO library through the complete SO library path filename.

Having said that for half a day, we haven't got to the point yet, but we can think that if we use the load library method to find the complete path of the target SO library and load the SO Library in the end, can we give the complete path of the SO library at the beginning and then load it directly? Let's guess that's what the load method does. Let's see.

void load(String absolutePath, ClassLoader loader) {
        if (absolutePath == null) {
            throw new NullPointerException("absolutePath == null");
        }
        String error = doLoad(absolutePath, loader);
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
    }

Let me go and go directly to the doLoad method, which proves that our guess may be correct, so test it in the actual project.

We first put SO in Asset, then copy it to internal storage, and then load it in using load method.

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        File dir = this.getDir("jniLibs", Activity.MODE_PRIVATE);
        File distFile = new File(dir.getAbsolutePath() + File.separator + "libstackblur.so");

        if (copyFileFromAssets(this, "libstackblur.so", distFile.getAbsolutePath())){
            //Loading internal stored SO libraries using load method
            System.load(distFile.getAbsolutePath());
            NativeBlurProcess.isLoadLibraryOk.set(true);
        }
    }

    public void onDoBlur(View view){
        ImageView imageView = (ImageView) findViewById(R.id.iv_app);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), android.R.drawable.sym_def_app_icon);
        Bitmap blur = NativeBlurProcess.blur(bitmap,20,false);
        imageView.setImageBitmap(blur);
    }


    public static boolean copyFileFromAssets(Context context, String fileName, String path) {
        boolean copyIsFinish = false;
        try {
            InputStream is = context.getAssets().open(fileName);
            File file = new File(path);
            file.createNewFile();
            FileOutputStream fos = new FileOutputStream(file);
            byte[] temp = new byte[1024];
            int i = 0;
            while ((i = is.read(temp)) > 0) {
                fos.write(temp, 0, i);
            }
            fos.close();
            is.close();
            copyIsFinish = true;
        } catch (IOException e) {
            e.printStackTrace();
            Log.e("MainActivity", "[copyFileFromAssets] IOException "+e.toString());
        }
        return copyIsFinish;
    }
}

Click on the onDoBlur button, and the load is successful!

Can you load the SO library directly on the external storage? Try copying the SO library onto the SD card.

It looks impossible, Permission denied!

java.lang.UnsatisfiedLinkError: dlopen failed: couldn't map "/storage/emulated/0/libstackblur.so" segment 1: Permission denied

It looks like you don't have permissions. Look at the exceptions thrown by the source code.

    /*
     * Loads the given shared library using the given ClassLoader.
     */
    void load(String absolutePath, ClassLoader loader) {
        if (absolutePath == null) {
            throw new NullPointerException("absolutePath == null");
        }
        String error = doLoad(absolutePath, loader);
        if (error != null) {
            // The exception thrown here
            throw new UnsatisfiedLinkError(error);
        }
    }

There should be an error in executing the doLoad method, but as I've seen above, the call to the Native method "nativeLoad" in the doLoad method should be an error in the Native code. Usually I seldom see the inside of Native. The last time I saw it, I needed to see the specific function of NinePath Drawable's chunk array of zoom control information. It took me a long time to find a piece of code I wanted. So I will not follow up here for the time being. Interested students can tell me the location of the key code.

I found some answers at a Google Developer Forum

The SD Card is mounted noexec, so I'm not sure this will work.

Moreover, using the SD Card as a storage location is a really bad idea, since any other application can modify/delete/corrupt it easily.Try downloading the library to your application's data directory instead, and load it from here.

It is also easy to understand that external storage paths such as SD cards are a mounted and non-exec storage medium, which can not be directly used as the running directory of executable files. Before using them, the executable files should be copied to the internal storage of APP and run again.

Finally, we can also see the official API documentation.


Posted by LoganK on Wed, 19 Jun 2019 15:49:41 -0700