Use AssetManager to read files in the JNI layer

Binary data is read at the JNI layer, and resources inside an asset can be read through the AAssetManager. Requires header file support:

#include <android/asset_manager_jni.h>
#include <android/asset_manager.h>
Copy the code

Provides a method for accessing files:

/** Available access modes for opening assets with {@link AAssetManager_open} */
enum {
    /** No specific information about how data will be accessed. **/
    AASSET_MODE_UNKNOWN      = 0,
    /** Read chunks, and seek forward and backward. */
    AASSET_MODE_RANDOM       = 1,
    /** Read sequentially, with an occasional forward seek. */
    AASSET_MODE_STREAMING    = 2,
    /** Caller plans to ask for a read-only buffer with all data. */
    AASSET_MODE_BUFFER       = 3
};
Copy the code

Direct access to folders:

/**
 * Open the named directory within the asset hierarchy.  The directory can then
 * be inspected with the AAssetDir functions.  To open the top-level directory,
 * pass in "" as the dirName.
 *
 * The object returned here should be freed by calling AAssetDir_close().
 */
AAssetDir* AAssetManager_openDir(AAssetManager* mgr, const char* dirName);
Copy the code

Direct access to files:

/**
 * Open an asset.
 *
 * The object returned here should be freed by calling AAsset_close().
 */
AAsset* AAssetManager_open(AAssetManager* mgr, const char* filename, int mode);
Copy the code

You can iterate based on the folders you access:

/**
 * Iterate over the files in an asset directory.  A NULL string is returned
 * when all the file names have been returned.
 *
 * The returned file name is suitable for passing to AAssetManager_open().
 *
 * The string returned here is owned by the AssetDir implementation and is not
 * guaranteed to remain valid if any other calls are made on this AAssetDir
 * instance.
 */
const char* AAssetDir_getNextFileName(AAssetDir* assetDir);
Copy the code

You can reset the state of traversal access:

/**
 * Reset the iteration state of AAssetDir_getNextFileName() to the beginning.
 */
void AAssetDir_rewind(AAssetDir* assetDir);
Copy the code

You can turn off folder access:

/**
 * Close an opened AAssetDir, freeing any related resources.
 */
void AAssetDir_close(AAssetDir* assetDir);
Copy the code

File information can be read:

/**
 * Attempt to read 'count' bytes of data from the current offset.
 *
 * Returns the number of bytes read, zero on EOF, or < 0 on error.
 */
int AAsset_read(AAsset* asset, void* buf, size_t count);
Copy the code

Where files can be jumped to:

/**
 * Seek to the specified offset within the asset data.  'whence' uses the
 * same constants as lseek()/fseek().
 *
 * Returns the new position on success, or (off_t) -1 on error.
 */
off_t AAsset_seek(AAsset* asset, off_t offset, int whence)
    __RENAME_IF_FILE_OFFSET64(AAsset_seek64);
Copy the code
/**
 * Seek to the specified offset within the asset data.  'whence' uses the
 * same constants as lseek()/fseek().
 *
 * Uses 64-bit data type for large files as opposed to the 32-bit type used
 * by AAsset_seek.
 *
 * Returns the new position on success, or (off64_t) -1 on error.
 */
off64_t AAsset_seek64(AAsset* asset, off64_t offset, int whence);
Copy the code

There are many more features:

/**
 * Close the asset, freeing all associated resources.
 */
void AAsset_close(AAsset* asset);
/**
 * Get a pointer to a buffer holding the entire contents of the assset.
 *
 * Returns NULL on failure.
 */
const void* AAsset_getBuffer(AAsset* asset);
/**
 * Report the total size of the asset data.
 */
off_t AAsset_getLength(AAsset* asset)
    __RENAME_IF_FILE_OFFSET64(AAsset_getLength64);
/**
 * Report the total size of the asset data. Reports the size using a 64-bit
 * number insted of 32-bit as AAsset_getLength.
 */
off64_t AAsset_getLength64(AAsset* asset);
/**
 * Report the total amount of asset data that can be read from the current position.
 */
off_t AAsset_getRemainingLength(AAsset* asset)
    __RENAME_IF_FILE_OFFSET64(AAsset_getRemainingLength64);
/**
 * Report the total amount of asset data that can be read from the current position.
 *
 * Uses a 64-bit number instead of a 32-bit number as AAsset_getRemainingLength does.
 */
off64_t AAsset_getRemainingLength64(AAsset* asset);
/**
 * Open a new file descriptor that can be used to read the asset data. If the
 * start or length cannot be represented by a 32-bit number, it will be
 * truncated. If the file is large, use AAsset_openFileDescriptor64 instead.
 *
 * Returns < 0 if direct fd access is not possible (for example, if the asset is
 * compressed).
 */
int AAsset_openFileDescriptor(AAsset* asset, off_t* outStart, off_t* outLength)
    __RENAME_IF_FILE_OFFSET64(AAsset_openFileDescriptor64);
/**
 * Open a new file descriptor that can be used to read the asset data.
 *
 * Uses a 64-bit number for the offset and length instead of 32-bit instead of
 * as AAsset_openFileDescriptor does.
 *
 * Returns < 0 if direct fd access is not possible (for example, if the asset is
 * compressed).
 */
int AAsset_openFileDescriptor64(AAsset* asset, off64_t* outStart, off64_t* outLength);
/**
 * Returns whether this asset's internal buffer is allocated in ordinary RAM (i.e. not * mmapped). */ int AAsset_isAllocated(AAsset* asset);Copy the code

Get the AssetManager of native layer:

/**
 * Given a Dalvik AssetManager object, obtain the corresponding native AAssetManager
 * object.  Note that the caller is responsible for obtaining and holding a VM reference
 * to the jobject to prevent its being garbage collected while the native object is
 * in use.
 */
AAssetManager* AAssetManager_fromJava(JNIEnv* env, jobject assetManager);
Copy the code
  1. Pass an AssetManager to the JNI layer;

    AssetManager assetManager = getAssets();

  2. Put your data into assets and pass it to JNI via JNI Native along with the corresponding file name:

    readFromAssets(assetManager, “yourdata.bin”);

  3. Then it can be read in JNI’s Native implementation function :(it can also be directly called in the corresponding C function, such as fopen and fread)

JNIEXPORT  jstring JNICALL Java_com_lib_MyLib_readFromAssets(JNIEnv* env, jclass clazz,
        jobject assetManager, jstring dataFileName) {
        
    AAssetManager* mManeger = AAssetManager_fromJava(env, assetManager);
    jboolean iscopy;
    const char *dataFile = env->GetStringUTFChars(dataFileName, &iscopy);
    
    int c = dataRead(mManeger, dataFile);  //call the C function

    env->ReleaseStringUTFChars(dataFileName, dataFile);
    
    jstring resultStr;
    resultStr = env->NewStringUTF("success");
    return resultStr;
}
Copy the code
char * dataRead(AAssetManager* mManeger, const char *dataFile) {

	char *content = NULL;

#ifndef __ANDROID_API__
	FILE *fp;

	size_t count=0;

	if(fileName ! ="") {
		fp = fopen(fileName,"rt");

		if(fp ! = NULL) { fseek(fp, 0, SEEK_END); count = ftell(fp); rewind(fp);if (count > 0) {
				content = (char *)malloc(sizeof(char) * (count+1));
				count = fread(content, sizeof(char), count, fp);
				content[count] = '\ 0'; } fclose(fp); }}return content;
#else
	AAsset* asset = AAssetManager_open(mManeger, fileName, AASSET_MODE_UNKNOWN);
	if (NULL == asset) {
		return content;
	}
	size_t size = (size_t)AAsset_getLength(asset);
	content = (char*)malloc(sizeof(char)*size + 1);
	AAsset_read(asset, content, size);
	content[size] = '\ 0';
	AAsset_close(asset);
	return content;
#endif
}
Copy the code

Manipulate bitmaps at the JNI layer

In Android through JNI to call Bitmap, through CMake to edit so dynamic link library, need to add jnigraphics image library.

target_link_libraries( # Specifies the target library.
                       native-operation
                       jnigraphics
                       ${log-lib} )
Copy the code

In Android, JNI Bitmap operations are defined in the bitmap.h header file

#include <android/bitmap.h>
Copy the code

Retrieves Bitmap object information

The AndroidBitmap_getInfo function allows native code to retrieve information about a Bitmap object, such as its size, pixel format, and so on, with the following signature:

/**
 * Given a java bitmap object, fill out the AndroidBitmapInfo struct for it.
 * If the call fails, the info parameter will be ignored.
 */
int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap, AndroidBitmapInfo* info);
Copy the code

The first parameter is a JNI interface pointer, the second parameter is a reference to the Bitmap object, and the third parameter is a pointer to the AndroidBitmapInfo structure.

/** Bitmap info, see AndroidBitmap_getInfo(). */
typedef struct {
    /** The bitmap width in pixels. */
    uint32_t    width;
    /** The bitmap height inpixels. */ uint32_t height; /** The number of byte per row. */ uint32_t stride; /** The bitmap pixel format. See {@link AndroidBitmapFormat} */ int32_t format; /** Unused. */ uint32_t flags; / / 0for now
} AndroidBitmapInfo;
Copy the code

Where, width is the width of the Bitmap, height is the height, format is the format of the image, and stride is the number of bytes for each line.

Image formats are supported as follows:

/** Bitmap pixel format. */
enum AndroidBitmapFormat {
    /** No format. */
    ANDROID_BITMAP_FORMAT_NONE      = 0,
    /** Red: 8 bits, Green: 8 bits, Blue: 8 bits, Alpha: 8 bits. **/
    ANDROID_BITMAP_FORMAT_RGBA_8888 = 1,
    /** Red: 5 bits, Green: 6 bits, Blue: 5 bits. **/
    ANDROID_BITMAP_FORMAT_RGB_565   = 4,
    /** Deprecated in API level 13. Because of the poor quality of this configuration, it is advised to use ARGB_8888 instead. **/
    ANDROID_BITMAP_FORMAT_RGBA_4444 = 7,
    /** Alpha: 8 bits. */
    ANDROID_BITMAP_FORMAT_A_8       = 8,
};
Copy the code

AndroidBitmap_getInfo returns 0 if executed successfully, otherwise returns a negative number representing the list of error codes executed as follows:

/** AndroidBitmap functions result code. */
enum {
    /** Operation was successful. */
    ANDROID_BITMAP_RESULT_SUCCESS           = 0,
    /** Bad parameter. */
    ANDROID_BITMAP_RESULT_BAD_PARAMETER     = -1,
    /** JNI exception occured. */
    ANDROID_BITMAP_RESULT_JNI_EXCEPTION     = -2,
    /** Allocation failed. */
    ANDROID_BITMAP_RESULT_ALLOCATION_FAILED = -3,
};
Copy the code

Access the native pixel cache

The AndroidBitmap_lockPixels function locks the pixel cache to ensure that the pixel’s memory is not moved.

If the Native layer wants to access pixel data and manipulate it, this method returns a Native pointer to the pixel cache:

/**
 * Given a java bitmap object, attempt to lock the pixel address.
 * Locking will ensure that the memory for the pixels will not move
 * until the unlockPixels call, and ensure that, if the pixels had been
 * previously purged, they will have been restored.
 *
 * If this call succeeds, it must be balanced by a call to
 * AndroidBitmap_unlockPixels, after which time the address of the pixels should
 * no longer be used.
 *
 * If this succeeds, *addrPtr will be set to the pixel address. If the call
 * fails, addrPtr will be ignored.
 */
int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);
Copy the code

The first parameter is a JNI interface pointer, the second parameter is a reference to a Bitmap object, and the third parameter is a pointer to the pixel cache address.

AndroidBitmap_lockPixels returns 0 on success, otherwise a negative number is returned.

Release the native pixel cache

AndroidBitmap_unlockPixels should be called once after calling the Bitmap to release the native pixel cache.

After reading or writing to the native pixel cache, it should be released. Once released, the Bitmap Java object can be used in the Java layer again. The function is signed as follows:

/** * Call this to balance a successful call to AndroidBitmap_lockPixels. */ int AndroidBitmap_unlockPixels(JNIEnv* env,  jobject jbitmap);Copy the code

The first argument is a JNI interface pointer, and the second argument is a reference to a Bitmap object that returns 0 on success and 1 otherwise.

The AndroidBitmap_lockPixels function obtains the cached address of all pixels, and then operates on each pixel value to change the Bitmap.

Rotate the Bitmap through JNI

First define a native function like this:

// Rotate 90° clockwise public native Bitmap rotateBitmap(Bitmap Bitmap);Copy the code

Pass in a Bitmap object and return a Bitmap object.

Then in C++ code, first retrieve the Bitmap information to see if it succeeds:

AndroidBitmapInfo bitmapInfo;
int ret;
if ((ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo)) < 0) {
    LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
    return NULL;
}
Copy the code

The next step is to get the Bitmap’s pixel cache pointer:

// Read bitmap pixels into native memory void *bitmapPixels;if ((ret = AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels)) < 0) {
    LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
    return NULL;
}
Copy the code

This pointer points to the Bitmap pixel content, which is a one-dimensional array that holds the values of all the pixels, but when we define the Bitmap image, we will define the width and height, which is relative to a two-dimensional image, so there is how the Bitmap pixel content into the pointer pointing to the one-dimensional content. Is it row or column?

In this case, it’s arranged by rows, and the rows are arranged from left to right, and the columns are arranged from top to bottom, starting at the upper left corner just like the origin of the screen coordinates.

Using the AndroidBitmap_lockPixels method, the bitmapPixels pointer points to the pixel content of a Bitmap, and its length is the product of the width and height of the Bitmap.

The Bitmap can be rotated either by directly changing the value of the pixels pointed to by the bitmapPixels pointer, or by creating a new Bitmap and filling it with pixel values. The latter is selected here.

In Java code, a Bitmap can be created using the createBitmap method, as follows:

Bitmap.createBitmap(int width, int height, @NonNull Config config)
Copy the code

In JNI, static methods of Bitmap are called to create a Bitmap object:

jobject generateBitmap(JNIEnv *env, uint32_t width, uint32_t height) {

    jclass bitmapCls = env->FindClass("android/graphics/Bitmap");
    jmethodID createBitmapFunction = env->GetStaticMethodID(bitmapCls,
                                                            "createBitmap"."(IILandroid/graphics/Bitmap$Config;) Landroid/graphics/Bitmap;");
    jstring configName = env->NewStringUTF("ARGB_8888");
    jclass bitmapConfigClass = env->FindClass("android/graphics/Bitmap$Config");
    jmethodID valueOfBitmapConfigFunction = env->GetStaticMethodID(
            bitmapConfigClass, "valueOf"."(Ljava/lang/String;) Landroid/graphics/Bitmap$Config;");

    jobject bitmapConfig = env->CallStaticObjectMethod(bitmapConfigClass,
                                                       valueOfBitmapConfigFunction, configName);

    jobject newBitmap = env->CallStaticObjectMethod(bitmapCls,
                                                    createBitmapFunction,
                                                    width,
                                                    height, bitmapConfig);
    return newBitmap;
}
Copy the code

First find the Config class using the FindClass method and get a configuration of ARGB_8888. Then get the Bitmap class and call its static method createBitmap to create a new Bitmap object.

Here we pass in the width and height of the new Bitmap. This width and height is also calculated according to different operations after getting the original width and height from AndroidBitmap_getInfo.

// Uint32_t newWidth = bitmapinfo.height; // Uint32_t newWidth = bitmapinfo.height; uint32_t newHeight = bitmapInfo.width;Copy the code

With the new Bitmap object and the old Bitmap pixel pointer, the next step is to create a new pixel pointer, fill the pixel content, and then fill the pixel content into the Bitmap.

Uint32_t *newBitmapPixels = new Uint32_t [newWidth * newHeight]; intwhereToGet = 0;
for (int y = 0; y < newHeight; ++y) {
    for (int x = newWidth - 1; x >= 0; x--) {
        uint32_t pixel = ((uint32_t *) bitmapPixels)[whereToGet++]; newBitmapPixels[newWidth * y + x] = pixel; }}Copy the code

In both of these for loops, we take the pixel value from the original pixel pointer and fill it with the value of the corresponding position in the new pixel pointer in a specific order. Here, the pixel pointer is arranged by row, starting from the upper left corner of the Bitmap.

void *resultBitmapPixels;
if ((ret = AndroidBitmap_lockPixels(env, newBitmap, &resultBitmapPixels)) < 0) {
    LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
    return NULL;
}
int pixelsCount = newWidth * newHeight;
memcpy((uint32_t *) resultBitmapPixels, newBitmapPixels, sizeof(uint32_t) * pixelsCount);
AndroidBitmap_unlockPixels(env, newBitmap);
Copy the code

Create a resultBitmapPixels pointer again and call AndroidBitmap_lockPixels to retrieve the pixel pointer cache of the new Bitmap. Then call memcpy. Fill the pixel pointer to be filled into resultBitmapPixels, thus completing the pixel assignment. Finally, call AndroidBitmap_unlockPixels method to release the pixel pointer cache and complete the entire assignment process.

This completes the JNI operation on the Bitmap by reading the pixel contents of the original Bitmap, performing the operation and assigning the value to the new Bitmap.

Flip the Bitmap upside down and mirror it left and right through JNI

Flipping a Bitmap up and down and mirroring it left and right and rotating it are similar, except for the pixel pointer.

Up-and-down operation:

int whereToGet = 0;
for (int y = 0; y < newHeight; ++y) {
    for (int x = 0; x < newWidth; x++) {
        uint32_t pixel = ((uint32_t *) bitmapPixels)[whereToGet++]; newBitmapPixels[newWidth * (newHeight - 1 - y) + x] = pixel; }}Copy the code

Left and right mirror operation:

int whereToGet = 0;
for (int y = 0; y < newHeight; ++y) {
    for (int x = newWidth - 1; x >= 0; x--) {
        uint32_t pixel = ((uint32_t *) bitmapPixels)[whereToGet++]; newBitmapPixels[newWidth * y + x] = pixel; }}Copy the code

Use Choreographer on the JNI layer

In the choreographer. H header file there are the following definitions:

struct AChoreographer;
typedef struct AChoreographer AChoreographer;

/**
 * Prototype of the function that is called when a new frame is being rendered.
 * It's passed the time that the frame is being rendered as nanoseconds in the * CLOCK_MONOTONIC time base, as well as the data pointer provided by the * application that registered a callback. All callbacks that run as part of * rendering a frame will observe the same frame time, so it should be used * whenever events need to be synchronized (e.g. animations). */ typedef void (*AChoreographer_frameCallback)(long frameTimeNanos, void* data); #if __ANDROID_API__ >= 24 /** * Get the AChoreographer instance for the current thread. This must be called * on an ALooper thread. */ AChoreographer* AChoreographer_getInstance() __INTRODUCED_IN(24); /** * Post a callback to be run on the next frame. The data pointer provided will * be passed to the callback function when it's called.
 */
void AChoreographer_postFrameCallback(AChoreographer* choreographer,
                AChoreographer_frameCallback callback, void* data) __INTRODUCED_IN(24);
/**
 * Post a callback to be run on the frame following the specified delay. The
 * data pointer provided will be passed to the callback function when it's * called. */ void AChoreographer_postFrameCallbackDelayed(AChoreographer* choreographer, AChoreographer_frameCallback callback, void* data, long delayMillis) __INTRODUCED_IN(24); #endif /* __ANDROID_API__ >= 24 */Copy the code

As you can see, choreographer is not accessible from JNI until after API 24. Other times it can only be retrieved from the Java layer.

// Indicate API mode to achieve 30 FPS.
enum APIMode {
  kAPINativeChoreographer,
  kAPIJavaChoreographer,
};

struct android_app;

// Declaration for native chreographer API.
struct AChoreographer;

typedef void (*AChoreographer_frameCallback)(long frameTimeNanos, void* data);

typedef AChoreographer* (*func_AChoreographer_getInstance)();

typedef void (*func_AChoreographer_postFrameCallback)(
    AChoreographer* choreographer, AChoreographer_frameCallback callback,
    void* data);
    
func_AChoreographer_getInstance AChoreographer_getInstance_;
func_AChoreographer_postFrameCallback AChoreographer_postFrameCallback_;

auto apilevel = AConfiguration_getSdkVersion(app_->config);

if (apilevel >= 24) {
    // Native Choreographer API is supported in API level 24~.
    void* lib = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL);
    if(lib ! = nullptr) { LOGI("Run with Choreographer Native API.");
      api_mode_ = kAPINativeChoreographer;

      // Retrieve function pointers from shared object.
      AChoreographer_getInstance_ =
          reinterpret_cast<func_AChoreographer_getInstance>(
              dlsym(lib, "AChoreographer_getInstance"));
      AChoreographer_postFrameCallback_ =
          reinterpret_cast<func_AChoreographer_postFrameCallback>(
              dlsym(lib, "AChoreographer_postFrameCallback"));
} else if (apilevel >= 16) {
    // Choreographer Java API is supported API level 16~.
    LOGI("Run with Chreographer Java API.");
    api_mode_ = kAPIJavaChoreographer;
}

if (api_mode_ == kAPINativeChoreographer) {
    // Initiate choreographer callback.
    StartChoreographer();
} else if (api_mode_ == kAPIJavaChoreographer) {
    // Initiate Java choreographer callback.
    StartJavaChoreographer();
}

// Native Chreographer API support.
void Engine::StartChoreographer() {
  // Initiate choreographer callbacks.
  if (api_mode_ == kAPINativeChoreographer) {
    auto choreographer = AChoreographer_getInstance_();
    AChoreographer_postFrameCallback_(choreographer, choreographer_callback,
                                      this);
  }
}

// Native Choreographer callback.
void Engine::choreographer_callback(long frameTimeNanos, void* data) {
  auto engine = reinterpret_cast<Engine*>(data);

  // Post next callback for self.
  if (engine->has_focus_) {
    engine->StartChoreographer();
  }

  // Swap buffer if the timing meets the 30fps time interval condition.
  // The callback is in the same thread context, so that we can just invoke
  // eglSwapBuffers().
  if (COULD_RENDER(frameTimeNanos, engine->prevFrameTimeNanos_)) {
    engine->should_render_ = true;
    engine->Swap();
    // Wake up main looper so that it will continue rendering.
    ALooper_wake(engine->app_->looper);
    engine->prevFrameTimeNanos_ = frameTimeNanos;
  }
}

// Java choreographer API support.
// With Java API, we uses synch primitive to synchronize Java thread and render
// thread.
void Engine::StartJavaChoreographer() {
  JNIEnv* jni;
  app_->activity->vm->AttachCurrentThread(&jni, NULL);
  // Intiate Java Chreographer API.
  jclass clazz = jni->GetObjectClass(app_->activity->clazz);
  jmethodID methodID = jni->GetMethodID(clazz, "startChoreographer"."()V");
  jni->CallVoidMethod(app_->activity->clazz, methodID);
  app_->activity->vm->DetachCurrentThread();
  return;
}

void Engine::StopJavaChoreographer() {
  JNIEnv* jni;
  app_->activity->vm->AttachCurrentThread(&jni, NULL);
  // Intiate Java Chreographer API.
  jclass clazz = jni->GetObjectClass(app_->activity->clazz);
  jmethodID methodID = jni->GetMethodID(clazz, "stopChoreographer"."()V");
  jni->CallVoidMethod(app_->activity->clazz, methodID);
  app_->activity->vm->DetachCurrentThread();
  // Make sure the render thread is not blocked.
  cv_.notify_one();
  return;
}
Copy the code

Use configuration.h on the JNI layer

The configuration.h header mainly defines the attributes of the phone. Obtained from AConfiguration_xxx when used.

Use hardware_buffer_jni.h at the JNI layer

There are two main functions defined in the header:

/**
 * Return the AHardwareBuffer associated with a Java HardwareBuffer object,
 * for interacting with it through native code. This method does not acquire any
 * additional reference to the AHardwareBuffer that is returned. To keep the
 * AHardwareBuffer live after the Java HardwareBuffer object got garbage
 * collected, be sure to use AHardwareBuffer_acquire() to acquire an additional
 * reference.
 */
AHardwareBuffer* AHardwareBuffer_fromHardwareBuffer(JNIEnv* env,
        jobject hardwareBufferObj);
/**
 * Return a new Java HardwareBuffer object that wraps the passed native
 * AHardwareBuffer object.
 */
jobject AHardwareBuffer_toHardwareBuffer(JNIEnv* env,
        AHardwareBuffer* hardwareBuffer);
Copy the code

Use input.h in the JNI layer

Use keycodes.h at the JNI layer

Use looper.h on the JNI layer

If we call a native method in a child thread, how do we switch threads in C++ code when we want to switch to the main thread to call a method?

final String s = mEditTest.getText().toString();
for (int i = 0 ; i < 3 ; i++){
    new Thread(new Runnable() {
        @Override
        public void run() {
            nativeToast(s);
        }
    }).start();
}
public native void init();
public native void nativeToast(String text);

public static void toast(String text){
    Toast.makeText(MyAppImpl.getAppContext(), text, Toast.LENGTH_SHORT).show();
}
Copy the code

In the above code, the nativeToast of the native layer actually calls the Toast method of the Java layer. Just before the call, we did a thread conversion and called toast in the main thread of the C++ layer.

native-lib.cpp

#include <jni.h>
#include <string>
#include "main_looper.h"
#include "jvm_helper.h"

extern "C"
{

JNIEXPORT void JNICALL
Java_com_example_oceanlong_ndkmaintest_MainActivity_init(JNIEnv *env, jobject instance) {

    JniHelper::setJVM(env);
    MainLooper::GetInstance()->init();
    LOGD("init env : %p", env);
}

JNIEXPORT void JNICALL
Java_com_example_oceanlong_ndkmaintest_MainActivity_nativeToast(JNIEnv *env, jobject instance,jstring text_) {

    const char* ctext = JniHelper::jstr2char(env, text_);
    LOGD("nativeToast : %s", ctext); MainLooper::GetInstance()->send(ctext); env->ReleaseStringUTFChars(text_, ctext); }}Copy the code

In the initialization code, there are really only two things:

  • Cache a global JNIEnv *
  • Initialize native Looper

Initialization must be done in the main thread!

main_looper.h & main_looper.cpp

#include <android/looper.h>
#include <string>
#include "logger.h"

class MainLooper
{
public:
    static MainLooper *GetInstance();
    ~MainLooper();
    void init();
    void send(const char* msg);

private:
    static MainLooper *g_MainLooper;
    MainLooper();
    ALooper* mainlooper;
    int readpipe;
    int writepipe;
    pthread_mutex_t looper_mutex_;
    static int handle_message(int fd, int events, void *data);
};
Copy the code
#include <fcntl.h>
#include "main_looper.h"
#include <stdint.h>
#include "string.h"
#include <stdlib.h>
#include <unistd.h>
#include "toast_helper.h"

#define LOOPER_MSG_LENGTH 81

MainLooper *MainLooper::g_MainLooper = NULL;


MainLooper *MainLooper::GetInstance()
{
    if(! g_MainLooper) { g_MainLooper = new MainLooper(); }return g_MainLooper;
}

MainLooper::MainLooper(){
    pthread_mutex_init(&looper_mutex_, NULL);
}

MainLooper::~MainLooper() {
    if(mainlooper && readpipe ! = -1) { ALooper_removeFd(mainlooper, readpipe); }if(readpipe ! = -1) { close(readpipe); }if(writepipe ! = -1) { close(writepipe); } pthread_mutex_destroy(&looper_mutex_); } void MainLooper::init() {

    int msgpipe[2];
    pipe(msgpipe);
    readpipe = msgpipe[0];
    writepipe = msgpipe[1];

    mainlooper = ALooper_prepare(0);
    int ret = ALooper_addFd(mainlooper, readpipe, 1, ALOOPER_EVENT_INPUT, MainLooper::handle_message, NULL);
}

int MainLooper::handle_message(int fd, int events, void *data) {

    char buffer[LOOPER_MSG_LENGTH];
    memset(buffer, 0, LOOPER_MSG_LENGTH);
    read(fd, buffer, sizeof(buffer));
    LOGD("receive msg %s" , buffer);
    Toast::GetInstance()->toast(buffer);
    return 1;
}
Copy the code

In initialization, the two most critical sentences are:

mainlooper = ALooper_prepare(0);
int ret = ALooper_addFd(mainlooper, readpipe, 1, ALOOPER_EVENT_INPUT, MainLooper::handle_message, NULL);
Copy the code

The ALooper_prepare method is defined as follows:

/**
 * Prepares a looper associated with the calling thread, and returns it.
 * If the thread already has a looper, it is returned.  Otherwise, a new
 * one is created, associated with the thread, and returned.
 *
 * The opts may be ALOOPER_PREPARE_ALLOW_NON_CALLBACKS or 0.
 */
ALooper* ALooper_prepare(int opts);
Copy the code

ALooper_prepare returns the looper of the called thread. Since we initialize the MainLooper on the main thread, we return the main thread looper as well.

Now let’s look at the ALooper_addFd method:

/**
 * Adds a new file descriptor to be polled by the looper.
 * If the same file descriptor was previously added, it is replaced.
 *
 * "fd" is the file descriptor to be added.
 * "ident" is an identifier for this event, which is returned from ALooper_pollOnce().
 * The identifier must be >= 0, or ALOOPER_POLL_CALLBACK if providing a non-NULL callback.
 * "events" are the poll events to wake up on.  Typically this is ALOOPER_EVENT_INPUT.
 * "callback" is the function to call when there is an event on the file descriptor.
 * "data" is a private data pointer to supply to the callback.
 *
 * There are two main uses of this function:
 *
 * (1) If "callback" is non-NULL, then this function will be called when there is
 * data on the file descriptor.  It should execute any events it has pending,
 * appropriately reading from the file descriptor.  The 'ident' is ignored in this case.
 *
 * (2) If "callback" is NULL, the 'ident' will be returned by ALooper_pollOnce
 * when its file descriptor has data available, requiring the caller to take
 * care of processing it.
 *
 * Returns 1 if the file descriptor was added or -1 if an error occurred.
 *
 * This method can be called on any thread.
 * This method may block briefly if it needs to wake the poll.
 */
int ALooper_addFd(ALooper* looper, int fd, int ident, int events,
        ALooper_callbackFunc callback, void* data);
Copy the code

In a nutshell, when a FD detects a change, it calls the callback method in the same thread as Looper.

With these two initial methods, we build a channel to the main thread.

Send to the main thread

In the initialization method, we build a message channel. Next, we need to send the message to the main thread.

void MainLooper::init() {

    int msgpipe[2];
    pipe(msgpipe);
    readpipe = msgpipe[0];
    writepipe = msgpipe[1];

    mainlooper = ALooper_prepare(0);
    int ret = ALooper_addFd(mainlooper, readpipe, 1, ALOOPER_EVENT_INPUT, MainLooper::handle_message, NULL);
}

int MainLooper::handle_message(int fd, int events, void *data) {

    char buffer[LOOPER_MSG_LENGTH];
    memset(buffer, 0, LOOPER_MSG_LENGTH);
    read(fd, buffer, sizeof(buffer));
    LOGD("receive msg %s" , buffer);
    Toast::GetInstance()->toast(buffer);
    return 1;
}

void MainLooper::send(const char *msg) {

    pthread_mutex_lock(&looper_mutex_);
    LOGD("send msg %s" , msg);
    write(writepipe, msg, strlen(msg));
    pthread_mutex_unlock(&looper_mutex_);
}
Copy the code

First we can see that in the init method, we create the channel MSgpipe. Add readPIPE to ALooper_addFd.

So all we need to do next is write to Writepipe to send the message to the main thread.

Call toast & toast_helper.cpp & jvm_helper.cpp

#include "toast_helper.h"
#include "jvm_helper.h"
#include "logger.h"

Toast *Toast::g_Toast = NULL;

Toast *Toast::GetInstance() {
    if(! g_Toast){ g_Toast = new Toast(); }return g_Toast;
}

void Toast::toast(std::string text) {
    JNIEnv *env = JniHelper::getJVM();
    LOGD("toast env : %p", env);

    jstring jtext = JniHelper::char2jstr(text.c_str());

    jclass javaclass = JniHelper::findClass(env,"com/example/oceanlong/ndkmaintest/MainActivity");
    jmethodID jfuncId = env->GetStaticMethodID(javaclass, "toast"."(Ljava/lang/String;) V");
    env->CallStaticVoidMethod(javaclass, jfuncId, jtext);
    env->DeleteLocalRef(jtext);
}
Copy the code
jstring JniHelper::char2jstr(const char* pat) {
    JNIEnv *env = getJVM();
    LOGD("char2jstr %p", env); StrClass = (env)->FindClass(env)"java/lang/String"); JmethodID ctorID = (env)->GetMethodID(strClass, String)"<init>"."([BLjava/lang/String;)V"); JbyteArray bytes = (env)->NewByteArray(strlen(pat)); // Convert char* to byte array (env)->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte*) pat); Jstring encoding = (env)->NewStringUTF("UTF-8"); // Convert the Byte array to a Java String and print itreturn (jstring) (env)->NewObject(strClass, ctorID, bytes, encoding);


}

jclass JniHelper::findClass(JNIEnv *env, const char* name) {
    jclass result = nullptr;
    if(env) {result = env->FindClass(name); jthrowable exception = env->ExceptionOccurred();if (exception)
        {
            env->ExceptionClear();
            returnstatic_cast<jclass>(env->CallObjectMethod(gClassLoader, gFindClassMethod, env->NewStringUTF(name))); }}return result;
}
Copy the code

FindClass failure

Usually, when we want to call a Java method in the Native layer, we first need to get the class of the method in Java. Our general approach is:

result = env->FindClass(name);

However, if the class is retrieved from a child thread, the class cannot be found.

When we create a child thread that wants to fetch a Class from the JVM, Android will start the system ClassLoader for us instead of our App ClassLoader.

By caching a static global ClassLoader object, if env->findClass fails, the cached ClassLoader gets the required class.

void JniHelper::setJVM(JNIEnv *env) {
    jvmEnv = env;
    jclass randomClass = env->FindClass("com/example/oceanlong/ndkmaintest/MainActivity");
    jclass classClass = env->GetObjectClass(randomClass);
    jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
    jmethodID getClassLoaderMethod = env->GetMethodID(classClass, "getClassLoader"."()Ljava/lang/ClassLoader;");
    jobject localClassLoader = env->CallObjectMethod(randomClass, getClassLoaderMethod);
    gClassLoader = env->NewGlobalRef(localClassLoader); Class gFindClassMethod = env->GetMethodID(classLoaderClass)"findClass"."(Ljava/lang/String;) Ljava/lang/Class;");
}

jclass JniHelper::findClass(JNIEnv *env, const char* name) {
    jclass result = nullptr;
    if (env)
    {
        result = env->FindClass(name);
        jthrowable exception = env->ExceptionOccurred();
        if (exception)
        {
            env->ExceptionClear();
            returnstatic_cast<jclass>(env->CallObjectMethod(gClassLoader, gFindClassMethod, env->NewStringUTF(name))); }}return result;
}
Copy the code

ALooper_addFd “sticky packet”

When I also sent a message to Main_looper, ALooper_addFd did not solve the concurrency problem.

mBtnTest.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {

        final String s = mEditTest.getText().toString();
        for (int i = 0 ; i < 5 ; i++){
            new Thread(new Runnable() {
                @Override
                public void run() { nativeToast(s); } }).start(); }}});Copy the code

Five threads send messages almost simultaneously. The final log is:

A total of five times were sent, but handle_message was only called twice.

Multinetwork.h is used in the JNI layer

Use native_activity.h at the JNI layer

Use native_WINDOW_jni.h in the JNI layer

Use obB.h in the JNI layer

Use sensor. H on the JNI layer

Use Sharedmem.h at the JNI layer

Use sharedmem_jni.h in the JNI layer

Use storage_manager.h at the JNI layer

Use surface_texture.h for the JNI layer

Use surface_texture_jni.h at the JNI layer

Use trace.h on the JNI layer

Native layer supports trace on Android 6.0 (API Level 23) and above.

  1. Define a function pointer for the ATrace function, as shown in the following code snippet:
#include <android/trace.h>
#include <dlfcn.h>

void *(*ATrace_beginSection) (const char* sectionName);
void *(*ATrace_endSection) (void);

typedef void *(*fp_ATrace_beginSection) (const char* sectionName);
typedef void *(*fp_ATrace_endSection) (void);
Copy the code
  1. Load the ATrace_xxx symbol as shown in the following code snippet:
// Retrieve a handle to libandroid.
void *lib = dlopen("libandroid.so", RTLD_NOW || RTLD_LOCAL);

// Access the native tracing functions.
if (lib != NULL) {
    // Use dlsym() to prevent crashes on devices running Android 5.1
    // (API level 22) or lower.
    ATrace_beginSection = reinterpret_cast<fp_ATrace_beginSection>(
        dlsym(lib, "ATrace_beginSection"));
    ATrace_endSEction = reinterpret_cast<fp_ATrace_endSection>(
        dlsym(lib, "ATrace_endSection"));
}
Copy the code

Note: For security reasons, the dlopen operation is only used for debugging, and if you want to use Trace on Android 4.3 (API level 18), you can call the above interface through JNI.

  1. Call ATrace_beginSection() and ATrace_endSection() at the beginning and end of the function you want to analyze:
#include <android/trace.h>

char *customEventName = new char[32];
sprintf(customEventName, "User tapped %s button", buttonName);

ATrace_beginSection(customEventName);
// Your app or game's response to the button being pressed.
ATrace_endSection();
Copy the code

Use window.h at the JNI layer