众所周知,安卓的图片都是通过Bitmap来完成设置间接交给Gpu去渲染到屏幕。如果想要优化图片,你还是个入门者,那么就必须要了解Bitmap.接下来我们先介绍Bitmap,再去深入研究如何优化大图如何在App上显示
(1) Bitmap是通过Bitmap.createBitMap创建的
通过源码,可以分析出,Bitmap的堆内存是分布在native.
2.1 创建Bitmap时序图
2.2 分析创建Bitmap代码
创建Bitmap第一步调用Bitmap的createBitmap,然后我们发下他会调用一个自身的重载方法createBitmap
public static Bitmap createBitmap(int width, int height, @NonNull Config config,
boolean hasAlpha, @NonNull ColorSpace colorSpace) {
return createBitmap(null, width, height, config, hasAlpha, colorSpace);
}
然后,这个重载方法会通过调用nativeCreate调用JNI来创建Bitmap,并在Native分配堆内存
public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height,
@NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) {
//...省略
Bitmap bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true,
colorSpace == null ? 0 : colorSpace.getNativeInstance());
//...省略
return bm;
}
2.3 分配内存
接下来我们揭秘,Bitmap到底是怎样在Native分配内存的。
我们下载安卓的源码,nativeCreate对于的JNI的C++代码在Bitmap.h
路径在这: /frameworks/base/core/jni/android/graphics/Bitmap.h
并找到gBitmapMethods这个属性
static const JNINativeMethod gBitmapMethods[] = {
1577 { "nativeCreate", "([IIIIIIZ[FLandroid/graphics/ColorSpace$Rgb$TransferParameters;)Landroid/graphics/Bitmap;",
1578 (void*)Bitmap_creator },
//..省略
}
于是我们可以发现,Java中Bitmap的nativeCreate方法执行的方法对应是Bitmap.h的Bitmap_creator方法。
static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
653 jint offset, jint stride, jint width, jint height,
654 jint configHandle, jboolean isMutable,
655 jfloatArray xyzD50, jobject transferParameters) {
669 ///省略...
//创建SkBitmap引用
670 SkBitmap bitmap;
///省略...
// 设置初始化参数
681 bitmap.setInfo(SkImageInfo::Make(width, height, colorType, kPremul_SkAlphaType, colorSpace));
// 分配Bitmap堆内存
683 sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(&bitmap);
///省略...
693
694 return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable));
695}
696
由此可见,Bitmap内存是分配在Native.
其实,从上面源码我们就已经得出结论了,不管图片是1M还是2M,只要按一样的参数设置,总内存不会变大。
SkImageInfo::Make(width, height, colorType, kPremul_SkAlphaType, colorSpace)
因为图片的显示只有宽,高,图片格式(rgb888之类),透明度方式,颜色空间这些参数。并没有文件大小相关这样的设定。
如何测试?可以使用Android Studio的Profiler查看App内存分布,设置图片后,内存此刻变化较大的那部分增量的内存肯定就是图片的内存。
下面是参数的解释
当我们设置图片后,Native的堆内存就会增大。有兴趣的话可以去试试
在执行Bitmap.createBitmap时候,我们从上面得知,会执行native的Bitmap_creator,然后Bitmap_creator会执行navtive的Bitmap的createBitmap*.从而去创建一个BitmapWrapper对象,这个对象会持有Bitmap对象。最终将BitmapWrapper这个对象指针作为入参,通过JNI创建一个 Java的Bitmap对象让其持有BitmapWrapper ,每次Java操作Bitmap时候,都会通过这个指针传到Native层,然后转成BitmapWrapper 去管理内存的数据。
jobject createBitmap(JNIEnv* env, Bitmap* bitmap,
200 int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
201 int density) {
//..省略
//创建BitmapWrapper对象,这个对象会返回给Java持有
207 BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap);
//通过JNI创建Java的Bitmap对象
//BitmapWrapper对象的指针引用作为入参传给的构造方法,、
// 并创建Java的Bitmap对象返回到Java层
208 jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
209 reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density,
210 isMutable, isPremultiplied, ninePatchChunk, ninePatchInsets);
//..省略
216 return obj;
217}
Java Bitmap的构造方法
Bitmap(long nativeBitmap, int width, int height, int density,
boolean requestPremultiplied, byte[] ninePatchChunk,
NinePatch.InsetStruct ninePatchInsets, boolean fromMalloc) {
mWidth = width;
mHeight = height;
//省略
//将BitmapWrapper指针引用保存下来,通过他去管理native的bitmap
mNativePtr = nativeBitmap;
//省略
}
举个例子,比如使用最常用的方法,回收内存。回收时候是通过把mNativePtr指针传到native,最终将其回收。
public void recycle() {
if (!mRecycled) {
nativeRecycle(mNativePtr);
mNinePatchChunk = null;
mRecycled = true;
mHardwareBuffer = null;
}
}
{ "nativeRecycle", "(J)Z", (void*)Bitmap_recycle },
static jboolean Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) {
852 LocalScopedBitmap bitmap(bitmapHandle);
853 bitmap->freePixels();
854 return JNI_TRUE;
855}
简单需求:修改需要显示的Bitmap数据,修改图片左上方1/4的区域改为红色.
创建后主要有4个文件,分别为
MainAcitivity.java
package com.example.myapplication;
public class MainActivity extends AppCompatActivity {
// Used to load the 'myapplication' library on application startup.
static {
System.loadLibrary("myapplication");
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
}
/**
* A native method that is implemented by the 'myapplication' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
native-lib.cpp
#include <jni.h>
#include <string>
#include <android/bitmap.h>
#include <android/log.h>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
JNIEnv* env,
jobject ) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
CMakeLists.txt
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt
native-lib.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
jnigraphics
log)
我们不做任何修改, 先运行起来,默认的Demo是中间有个Text,显示Native的文字“Hello from C++”
修改xml,加入ImageView,拖到这个文字的正上方,宽高各自设置为200,然后 加入一张备好的名为king.jpg图片放到asset目录。
通过AssetManagerAssetManager,将这个king.jpg显示到ImageView上
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
new Thread(new Runnable() {
@Override
public void run() {
AssetManager assetManager = getAssets();
InputStream inputStream = null;
try {
//从asset目录加载出king.jpg这个图
inputStream = assetManager.open("king.jpg");
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
//切回UI线程,把bitmap设置到ImageView显示出来
runOnUiThread(new Runnable() {
@Override
public void run() {
ImageView imageView = binding.imageView;
imageView.setImageBitmap(bitmap);
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
}
运行效果如下:
首先,我们创建一个类BitmapHelper.java ,通过他来调用JNI接口修改Bitmap,对应的方法是updateBitmap
package com.example.myapplication;
import android.graphics.Bitmap;
public class BitmapHelper {
public native void updateBitmap(Bitmap bitmap);
}
在修改之前,我们先了解c++怎么获取Java的bitmap宽高参数。 他是通过头文件android/bitmap.h的这个AndroidBitmap_getInfo函数获取,并且赋值到AndroidBitmapInfo这个结构体. AndroidBitmapInfo是在安卓源码的这个路径/frameworks/native/include/android/bitmap.h
65/** Bitmap info, see AndroidBitmap_getInfo(). */
66typedef struct {
67 /** The bitmap width in pixels. */
68 uint32_t width; //图片的宽
69 /** The bitmap height in pixels. */
70 uint32_t height;//图片的高
71 /** The number of byte per row. */
72 uint32_t stride;//每一行的字节数,如果是ARGB_8888格式,这个stride/4正好等价于图片的width,因为一个像素点占4个字节,分别为透明度,蓝, 绿 , 红(这里是和平时的位置是倒过来的)
73 /** The bitmap pixel format. See {@link AndroidBitmapFormat} */
74 int32_t format;//图片格式
75 /** Unused. */
76 uint32_t flags; // 0 for now
77} AndroidBitmapInfo;
操作Bitmap系列的方法需要导入jnigraphics这个库, 在CmakeList.txt配置即可,Android Studio编译时候会把这个库导入到我们的环境。修改如下,在target_link_libraries加入一个jnigraphics
CmakeList.txt
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
jnigraphics
log)
我们来到native-lib.cpp, 加入BitmapHelper的updateBitmap对应的native方法
Java_com_example_myapplication_BitmapHelper_updateBitmap
并且导入android/bitmap.h, android/log.h
通过AndroidBitmap_getInfo, 获取图片宽高,打印出来。代码如下
#include <jni.h>
#include <string>
#include <android/bitmap.h>
#include <android/log.h>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
JNIEnv* env,
jobject ) {
std::string hello = "关注King老师,3连哦";
return env->NewStringUTF(hello.c_str());
}
extern "C" JNIEXPORT void JNICALL
Java_com_example_myapplication_BitmapHelper_updateBitmap(
JNIEnv* env,
jobject, jobject bitmap) {
//声明一个AndroidBitmapInfo,用来获取bitmap的参数信息
AndroidBitmapInfo info;
// 将java的bitmap的宽高信息填充到AndroidBitmapInfo
AndroidBitmap_getInfo(env, bitmap, &info);
//这个方法就是打印安卓的日志方法, ANDROID_LOG_INFO对于就是info级别的日志, 然后第二个参数是tag
//如果AndroidBitmap_getInfo获取成功, 打印图片的宽高就能出现
__android_log_print(ANDROID_LOG_INFO, "updateBitmap", "width:%d, height:%d", info.width, info.height);
}
先运行起来,看看效果,就是文字改了下,你懂的。注意了,如果jnigraphics这个库你没有加入,肯定是编译失败的。
打印效果
打印出的宽高和上面设置的一致。
他是通过二维数组存下来的,现在默认的是ARGB_8888格式,所以一个像素是有4个字节组成。
屏幕绘制一个图片时候,是从上到下一行一行的像素列点下来的。由于内存存储方式是顺序存储,内存是分布连续的,图片绘制完一行时候,下一行的位置即为指针加上宽度的偏移量。
从数组维度上面可以这样理解
有了上面的理解,我们就可以修改bitmap堆内存了,如下。
extern "C" JNIEXPORT void JNICALL
Java_com_example_myapplication_BitmapHelper_updateBitmap(
JNIEnv* env,
jobject, jobject bitmap) {
//声明一个AndroidBitmapInfo,用来获取bitmap的参数信息
AndroidBitmapInfo info;
// 将java的bitmap的宽高信息填充到AndroidBitmapInfo
AndroidBitmap_getInfo(env, bitmap, &info);
__android_log_print(ANDROID_LOG_INFO, "updateBitmap", "width:%d, height:%d", info.width, info.height);
//声明一个指向二维数组内存的指针,也就是指向Bitmap内存的指针,初始化是第一行的位置
int *displayRowIndex = NULL;
//当前行数组
int *currentRowPixelsArray = NULL;
int row = info.width;
int column = info.height;
//将bitmap的内存指针赋给displayRowIndex,并锁定内存
AndroidBitmap_lockPixels(env, bitmap, reinterpret_cast<void **>(&displayRowIndex));
//只改上面一半的行数
for (int i = 0; i < column/2; ++i) {
currentRowPixelsArray = displayRowIndex;
//只改左边一半的列
for (int j = 0; j < row/2; ++j) {
//这里的颜色和设计稿的颜色是反过来的,只有透明度位置没变,格式为0x+透明度+蓝+绿+红
//比如红色0xFFFF0000,那么在堆内存存储是从右到左反过来,则为0xFF0000FF
currentRowPixelsArray[j] = 0xFF0000FF;
}
//把索引偏移到下一行的位置
displayRowIndex += info.width;
}
//解锁bitmap内存操作
AndroidBitmap_unlockPixels(env, bitmap);
}
运行效果如下。
未完结,待更新,有时间会第一时间更新。请关注,3连