Anroid graphics

297 阅读15分钟

BitmapFactory

常用的四个decode函数的使用

public static Bitmap decodeResource(Resources res, int id, Options opts):图片保存在res/drawable-xxx文件夹时,使用该函数

public static Bitmap decodeFile(String pathName, Options opts):图片源已知是绝对路径时使用该函数

public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts):图片源已知是数据库源uri时可使用该函数

ParcelFileDescriptor pfd = context.getContentResolver()
        .openFileDescriptor(sourceUri, "r");
FileDescriptor fd = null;
Bitmap bitmap = null;
if (pfd != null) {
    fd = pfd.getFileDescriptor();
}
try {
    if (fd != null) {
        bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options);
    }
} catch (OutOfMemoryError e) {

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts):图片源已知是数据库源uri时可使用该函数

InputStream is = null;
try {
    is = mContext.getContentResolver().openInputStream(uri);
    BitmapFactory.decodeStream(is, null, o);

说明:

1.第一个函数可以在UI线程中直接使用,后三个函数必须使用异步线程。后两个函数有何区别,暂时不明,测试二者所花时间差不多。

2.outPadding一般置为null,如果图片源的bitmap有边界,可以通过该参数获取一个Rect边界对象。

避免OutOfMemoryError

如果允许在一定条件下对原图进行采样可以很好的规避此问题,同时可以得到不为空的bitmap进行下一步处理。

ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(sourceUri, "r");
FileDescriptor fd = null;
Bitmap bitmap = null;
if (pfd != null) {
    fd = pfd.getFileDescriptor();
}
try {
    if (fd != null) {
        bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options);
    }
} catch (OutOfMemoryError e) {
    // / M: As there is a chance no enough dvm memory for decoded
    // Bitmap,
    // Skia will return a null Bitmap. In this case, we have to
    // downscale the decoded Bitmap by increase the options.inSampleSize
    final int maxTryNum = 8;
    for (int i = 0; i < maxTryNum; i++) {
        // we increase inSampleSize to expect a smaller Bitamp
        options.inSampleSize *= 2;
        try {
            bitmap = BitmapFactory.decodeFileDescriptor(fd, null,
                    options);
        } catch (OutOfMemoryError e1) {
            Log.w(LOGTAG, "  saveBitmap :out of memory when decoding:"
                    + e1);
            bitmap = null;
        }
        if (bitmap != null)
            break;
    }
} finally {
    if (pfd != null) {
        Utils.closeSilently(pfd);
    }
}

不耗时获取图片源的宽和高数据。使用如下方式decode不会把数据读取到内存,从而不耗时

InputStream isStream =  mContext.getContentResolver().openInputStream(uri);
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inJustDecodeBounds = true;
BitmapFactory.decodeStream(isStream, null, opt);
photoWidth = opt.outWidth;
photoHeight = opt.outHeight;

BitmapFactory.Options常用相关属性

public Bitmap.Config inPreferredConfig:默认值为Bitmap.Config.ARGB_8888;

public int inSampleSize:采样率,必须是2的指数值。

public boolean inJustDecodeBounds:只解码,不读取数据到内存

public Bitmap extractAlpha()

生成只提取了原图的alpha通道的新图,也就是说新的bitmap只有alpha值,rgb值为0。这个函数的作用是获取原图的轮廓,然后可以填充rgb值。因此可以实现阴影,影子,光晕等效果。

使用如下的代码片段来验证这个函数:

public Bitmap process() {
    Bitmap destImage = Bitmap.createBitmap(400,
            400, Config.ARGB_8888);
    int color = 0xAFFF0000; //半透明红色<br>&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;//0x00FF0000 //透明红色<br>&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;//0xFFFF0000 //不透明红色
    for(int i = 0; i < destImage.getWidth(); i++){
        for(int j = 0; j < destImage.getHeight(); j++){
            destImage.setPixel(i, j, color);//生成400x400分辨率的颜色为color的原图bitmap
        }
    }  
    Log.d(TAG, "--" + TAG + ">>process>>");
    return destImage.extractAlpha();//生成提取了原bitmap alpha通道的新图bitmap
}

处理后的图片应用给无背景色的ImageView,ImageView所在的容器以淡绿色作背景。

从上面的三组处理结果可以得出结论:

(1)该函数只提取aplha通道,rgb通道值为0。这是因为原图是红色,新图只有透明通道。

(2)新图保留了原图的alpha通道值,原图如果是半透明的,新图也是半透明的,如图一,原图是透明的,新图也是透明的,如图二,原图是不透明的,新图也不透明,如图三。

这样得到的绘制到canvas上时,argb通道值会依据Paint的颜色值绘制。如果原bitmap没有alpha通道,实验结果如透明,说明新图alpha通道为0。

getPixels

public void getPixels(int[] pixels, int offset, int stride,int x, int y, int width, int height)

获取原Bitmap的像素值存储到pixels数组中。

参数:

pixels 接收位图颜色值的数组

offset 写入到pixels[]中的第一个像素索引值

stride pixels[]中的行间距个数值(必须大于等于位图宽度)。不能为负数

x 从位图中读取的第一个像素的x坐标值。

y 从位图中读取的第一个像素的y坐标值

width 从每一行中读取的像素宽度

height   读取的行数

public Bitmap process(Bitmap bitmap) {
    // TODO Auto-generated method stub
     
    Bitmap destImage = Bitmap.createBitmap(400,
            400, Config.ARGB_8888);
     
    int width = destImage.getWidth();
    int height = destImage.getHeight();
     
    for(int i = 0; i < width/2; i++){
        for(int j = 0; j < height/2; j++){
            destImage.setPixel(i, j, Color.RED);//左上区域红色
        }
        for(int j = height / 2; j < height; j++){
            destImage.setPixel(i, j, Color.CYAN);//左下区域青色
        }
    }
    for(int i = width / 2; i < width; i++){
        for(int j = 0; j < height/2; j++){
            destImage.setPixel(i, j, Color.YELLOW);//右上区域黄色
        }
        for(int j = height / 2; j < height; j++){
            destImage.setPixel(i, j, Color.BLACK);//右下区域黑色
        }
    }//上述代码确定了原图destImage的所有像素值    
 
    int[] mPixels = new int[width * height];
    for(int i = 0; i < mPixels.length; i++){
        mPixels[i] = Color.BLUE;//数组中默认值为蓝色
    }
    destImage.getPixels(mPixels, 0, width, 0, 0,
            width, height);
     
    Bitmap mResultBitmap = Bitmap.createBitmap(mPixels, 0, width, width, height, 
            Bitmap.Config.ARGB_8888);      
 
    return mResultBitmap;
}

原始destImage为四色的。

提取红色区域:

destImage.getPixels(mPixels, 0, width, 0, 0, width/2, height/2);

提取黄色区域:

destImage.getPixels(mPixels, 0, width, width/2, 0, width/2, height/2);

提取青色区域:

destImage.getPixels(mPixels, 0, width, 0, height/2, width/2, height/2);

提取黑色区域:

destImage.getPixels(mPixels, 0, width, width/2, height/2, width/2, height/2);

从上面四个操作可以理解了x,y,width,height四个参数是如何控制提取原图哪块区域位置的元素的。上面这些有两点需要说明:

  1. 剩余的区域并不是透明的或空白的,因为mPixels数组的默认值为蓝色,且新生成的bitmap的宽高值也是按原图大小设定的。如果只需要提取之后的图,那么只需要设置mPixels数组大小为提取的区域的像素数目,新生成的bitmap宽高依照提取区域设置即可。

  2. 所有提取的区域都在新图的左上角。这是因为提取的区域在mPixels中是从0位置存储的。也就是新图完全由提取之后的mPixels数组决定。offset参数则决定了新提取的元素存储在mPixels数组中的起始位置。这一点需要明白提取像素是一行一行的存储到mPixels数组中的。所以有如下结果:

提取区域在新图区域左上角:offset = 0;

提取区域在新图区域右上角:offset = width/2;

提取区域在新图区域左下角:offset = width * (height / 2);

提取区域在新图区域右下角:offset = width * (height / 2) + width / 2;

上面的这四个赋值同时也说明了提取的元素在mPixels这一二维数组中是呈块状区域存储的,用二维图来看比较清晰。我们只需要设定起始的元素的存储位置offset即可。

stride是什么意思呢?它的值必须大于等于原图的宽。通过实际应用来理解它:把两张上述的四色图拼接到一起

destImage.getPixels(mPixels, 0 , 2 * width, 0, 0,
        width, height);
destImage.getPixels(mPixels, width , 2 * width, 0, 0,
        width, height);
 
Bitmap mResultBitmap = Bitmap.createBitmap(mPixels, 0, width * 2, width * 2, height * 2, 
        Bitmap.Config.ARGB_8888);

createBitmap和getPixels中的stride参数的意义是一样的,正是通过这一参数实现了如何把块状区域在二维数组mPixels中也是块状区域的。

这个参数的意义是行间距,也即一行读取完毕后,下一行应该间隔多少读取,数组应该间隔多少才开始存储下一行数据。

glVertexAttribPointer 参数

下面的这三段代码的作用是指定一个三角形的三个顶点和纹理的UV向量,每个顶点由x,y,z三个基向量标识,每个纹理由U,V两个基向量标志,所有的数据开始都保存在数组mTriangleVerticesData之中,通过下面的三段代码实现了顶点处理。

mTriangleVerticesData数组为{
    -1.0f, -0.5f, 0, -0.5f, 0.0f,
    1.0f, -0.5f, 0, 1.5f, -0.0f,
    0.0f, 1.11803399f, 0, 0.5f, 1.61803399f
}

处理后三个顶点的数据分别为(x, y, z)(U, V)如下

(-1.0f, -0.5f, 0)  (-0.5f, 0.0f)

(1.0f, -0.5f, 0)  (1.5f, -0.0f)

(0.0f, 1.11803399f, 0)  (0.5f, 1.61803399f)

可见顶点处理把数组数据分块取出处理,数组中分段存储了顶点数据

private static final int FLOAT_SIZE_BYTES = 4;//一个float数据占四个字节
private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES;//每5个元素表示一个顶点
private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0;
private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3;
private final float[] mTriangleVerticesData = {
        // X, Y, Z, U, V
        -1.0f, -0.5f, 0, -0.5f, 0.0f,
        1.0f, -0.5f, 0, 1.5f, -0.0f,
        0.0f,  1.11803399f, 0, 0.5f,  1.61803399f };
 
private FloatBuffer mTriangleVertices;
private int maPositionHandle;
private int maTextureHandle;
mTriangleVertices = ByteBuffer.allocateDirect(mTriangleVerticesData.length
        * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer();
mTriangleVertices.put(mTriangleVerticesData).position(0);
mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET);//从索引0开始取数据
GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false,//取3个数据
        TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices);//跳转20个字节位(5个数据)再取另外3个数据,这是实现块状数据存储的关键,很多函数里都有这个参数,通常写作int stride
checkGlError("glVertexAttribPointer maPosition");
mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET);//从索引3开始取数据
GLES20.glEnableVertexAttribArray(maPositionHandle);
checkGlError("glEnableVertexAttribArray maPositionHandle");
GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false,//取两个数据U,V
        TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices);//到此基本明晰了如何块状存储数据
checkGlError("glVertexAttribPointer maTextureHandle");
GLES20.glEnableVertexAttribArray(maTextureHandle);
checkGlError("glEnableVertexAttribArray maTextureHandle");

graphics包Matrix类函数

二维图形变换的矩阵如下:

|ScaleX  SkewX  TransX|

|SkewY  ScaleY  TransY|

|Persp0  Persp1  Persp2|

ScaleX:x方向缩放倍率

ScaleY:y方向缩放倍率

TransX:x方向平移值

TransY:y方向平移值

SkewX:x方向错切值

SkewY:y方向错切值

Persp:齐次坐标的值,一般取值0或1。

graphics包中的Matrix类的方法调用native计算,jni调用了skia库中SkMatrix.cpp文件下的函数计算,各个函数实际上都是在对二维变换矩阵进行赋值:

void SkMatrix::setScale(SkScalar sx, SkScalar sy) {
    if (1 == sx && 1 == sy) {
        this->reset();
    } else {
        fMat[kMScaleX] = sx;
        fMat[kMScaleY] = sy;
        fMat[kMPersp2] = 1;
 
        fMat[kMTransX] = fMat[kMTransY] =
        fMat[kMSkewX]  = fMat[kMSkewY] =
        fMat[kMPersp0] = fMat[kMPersp1] = 0;
 
        this->setTypeMask(kScale_Mask | kRectStaysRect_Mask);
    }
}

这是以原点为中心进行缩放的矩阵赋值。

void SkMatrix::setScale(SkScalar sx, SkScalar sy, SkScalar px, SkScalar py) {
    if (1 == sx && 1 == sy) {
        this->reset();
    } else {
        fMat[kMScaleX] = sx;
        fMat[kMScaleY] = sy;
        fMat[kMTransX] = px - sx * px;
        fMat[kMTransY] = py - sy * py;
        fMat[kMPersp2] = 1;
 
        fMat[kMSkewX]  = fMat[kMSkewY] =
        fMat[kMPersp0] = fMat[kMPersp1] = 0;
 
        this->setTypeMask(kScale_Mask | kTranslate_Mask | kRectStaysRect_Mask);
    }
}

这是以指定坐标(px, py)为中心点进行缩放的矩阵赋值。与上一个函数对比可以发现,不仅进行了缩放还进行了平移,所以才可以表现为在指定点处缩放。

void SkMatrix::setTranslate(SkScalar dx, SkScalar dy) {
    if (dx || dy) {
        fMat[kMTransX] = dx;
        fMat[kMTransY] = dy;

        fMat[kMScaleX] = fMat[kMScaleY] = fMat[kMPersp2] = 1;
        fMat[kMSkewX]  = fMat[kMSkewY] =
        fMat[kMPersp0] = fMat[kMPersp1] = 0;

        this->setTypeMask(kTranslate_Mask | kRectStaysRect_Mask);
    } else {
        this->reset();
    }
}
void SkMatrix::preTranslate(SkScalar dx, SkScalar dy) {
    if (!dx && !dy) {
        return;
    }
 
    if (this->hasPerspective()) {
        SkMatrix    m;
        m.setTranslate(dx, dy);
        this->preConcat(m);
    } else {
        fMat[kMTransX] += sdot(fMat[kMScaleX], dx, fMat[kMSkewX], dy);
        fMat[kMTransY] += sdot(fMat[kMSkewY], dx, fMat[kMScaleY], dy);
        this->setTypeMask(kUnknown_Mask | kOnlyPerspectiveValid_Mask);
    }
}
void SkMatrix::postTranslate(SkScalar dx, SkScalar dy) {
    if (!dx && !dy) {
        return;
    }
 
    if (this->hasPerspective()) {
        SkMatrix    m;
        m.setTranslate(dx, dy);
        this->postConcat(m);
    } else {
        fMat[kMTransX] += dx;
        fMat[kMTransY] += dy;
        this->setTypeMask(kUnknown_Mask | kOnlyPerspectiveValid_Mask);
    }
}
static inline SkScalar sdot(SkScalar a, SkScalar b, SkScalar c, SkScalar d) {
    return a * b + c * d;
}

上面的这四个函数说明了set,pre和post的区别,假如有如下操作:

(1)

Matrix.scale(0.5, 0.5);

Matrix.preTranslate(100, 100);

(2)

Matrix.scale(0.5, 0.5);

Matrix.postTranslate(100, 100);

第一段代码会平移(1000.5, 1000.5),第二段代码会平移(100, 100),post不受之前的矩阵变换的影响。

而set则会清除所有之前的矩阵设置。

其它函数的pre和post的意思都是如此。

比如preRotate():那么它的旋转中心点会受到之前矩阵变换的影响,而postRotate()不会。

preTranslate(100, 100);

preRotate(45):那么它的旋转中心点是(100, 100)

postRotate(45):旋转中心点是原点

图片的矩阵变换是对图片在屏幕上的每个坐标点进行变换。

比如矩形框Rect(100, 100, 600, 600),缩放scale(0.5f, 0.5f)之后变为Rect(50, 50, 300, 300)在屏幕上表现为矩形本身有缩小,但位置也有了偏移,这个时候不能简单的认为是矩形框保持矩形中心位置不变或左上角位置不变作缩放。

所以一般的绕中心缩放,倍数缩放操作之后还需要平移。因为中心坐标点位置不变,所以有关系式

先缩放后平移:

centerX * scaleX + translateX = centerX

centerY * scaleY + translateY = centerY

先平移后缩放:

(centerX + translateX) * scaleX = centerX

(centerY + translateY) * scaleY = centerY

再来理解下preScale(scaleX, scaleY, oriX, oriY),这个指定缩放原点的函数的意思。之前说图片的矩阵变化是对图片在屏幕上的每个坐标点进行变换,这里需要说明的是坐标是相对于坐标原点为(0, 0)而言的。而这里指定了坐标原点,那么所有的矩阵变换是相对于指定的这个坐标原点而言的。指定坐标原点对平移没有影响,但是对旋转和缩放是有影响的。比如RectF(0, 600, 0, 600),矩阵变换后:

preScale(0.5, 0.5) ——> RectF(0, 300, 0, 300)

preScale(0.5, 0.5, 900, 900) ——> RectF(450, 750, 450, 750):可以想象坐标原点移到了(900,900),原矩形上的所有点相对于这个原点的位置进行缩放得到新的矩形。

查看连接矩阵变换的skia库源码发现对于矩阵变换顺序如下:

pre(A)

post(B)

那么连接的矩阵变换为A*B

pre(A)

pre(B)

那么连接的矩阵变换为B*A

即pre的意思是改变矩阵变换的顺序

注意pre和post是相对于在这个操作之前的操作而言的pre(A)之前没有任何操作,所以此时pre和post和set无差别。

另外:

scale缩放取值为-1时可以得到镜像矩阵

protected static Matrix getHorizontalMatrix(float width) {
    Matrix flipHorizontalMatrix = new Matrix();
    flipHorizontalMatrix.setScale(-1, 1);
    flipHorizontalMatrix.postTranslate(width, 0);
    return flipHorizontalMatrix;
}

上面的矩阵表示对原图进行水平镜像变换。

Matrix.mapRect()

RectF r = new RectF(50, 0, 100, 100);
Log.d("m1", "-r.left = " + r.left + ", right = " + r.right + ", top = "
        + r.top + ", bottom = " + r.bottom);
Matrix m = new Matrix();
m.setScale(2, 3);
 
m.mapRect(r);
Log.d("m1", "-r.left = " + r.left + ", right = " + r.right + ", top = "
        + r.top + ", bottom = " + r.bottom); 

上面这段代码log如下:

D/m1      (20694): -r.left = 50.0, right = 100.0, top = 0.0, bottom = 100.0
D/m1      (20694): -r.left = 100.0, right = 200.0, top = 0.0, bottom = 300.0

所以mapRect是单独对RectF的坐标点进行矩阵变换。

RectF rf = new RectF(100, 100, 300, 300);
Matrix m = new Matrix();
float centerX = 500;
float centerY = 500;
float scale = 1.5f;
m.preScale(scale, scale);
m.mapRect(rf);
float cx = rf.centerX();
float cy = rf.centerY();
m.postTranslate(centerX - cx, centerY -cy);
//m.setTranslate(centerX - cx, centerY - cy);
m.mapRect(rf);

上面这段代码目的是对原矩形以指定点(500,500)坐缩放,使用postTranslate怎么也得不到正确的值,但是使用setTranslate之后结果正确。

原因:第一次mapRect为了得到原点缩放的中心已经对rf作了变换。而postTranslate是一个连接操作,所以整个过程的矩阵变换为:preScale -> preScale -> postTranslate,相当于对原矩形作了两次preScale操作。

canvas.saveLayerAlpha实现“回”矩形框绘制效果

代码如下:

canvas.drawBitmap(mb, 100, 100, mPaint);
 
RectF innerRect = new RectF(250, 250, 550, 550);
RectF outterRect = new RectF(100, 100, 100 + mb.getWidth(), 100 + mb.getHeight());
 
canvas.saveLayerAlpha(outterRect, 0xC8,LAYER_FLAGS);
mPaint.setStyle(Style.FILL_AND_STROKE);
mPaint.setColor(getResources().getColor(R.color.the_color));
canvas.drawRect(innerRect, mPaint);
 
PorterDuffXfermode mode = new PorterDuffXfermode(
        PorterDuff.Mode.SRC_OUT);
mPaint.setXfermode(mode);
canvas.drawRect(outterRect, mPaint);
canvas.restore();

bitmap在原图层上绘制,调用saveLayerAlpha函数另取一个图层绘制回形框,0xC8是该图层的透明度,图层大小这里设置对应为bitmap大小,当然也可以设置为屏幕大小。

然后绘制回形框的内矩形和外矩形,并使用SRC_OUT设置只绘制两个矩形框的不相交部分。回形框可以使用画笔绘制任意颜色,这里指定土灰色是为了配合透明度实现类似磨砂玻璃屏的效果,当然实际效果使用模糊算法实现最好。

PorterDuff.Mode为枚举类,一共有16个枚举值:

1.PorterDuff.Mode.CLEAR
所绘制不会提交到画布上。

2.PorterDuff.Mode.SRC 显示上层绘制图片 3.PorterDuff.Mode.DST 显示下层绘制图片

4.PorterDuff.Mode.SRC_OVER 正常绘制显示,上下层绘制叠盖。

5.PorterDuff.Mode.DST_OVER 上下层都显示。下层居上显示。

6.PorterDuff.Mode.SRC_IN 取两层绘制交集。显示上层。

7.PorterDuff.Mode.DST_IN 取两层绘制交集。显示下层。

8.PorterDuff.Mode.SRC_OUT 取上层绘制非交集部分。

9.PorterDuff.Mode.DST_OUT 取下层绘制非交集部分。

10.PorterDuff.Mode.SRC_ATOP 取下层非交集部分与上层交集部分

11.PorterDuff.Mode.DST_ATOP 取上层非交集部分与下层交集部分

12.PorterDuff.Mode.XOR 异或:去除两图层交集部分

13.PorterDuff.Mode.DARKEN 取两图层全部区域,交集部分颜色加深

14.PorterDuff.Mode.LIGHTEN 取两图层全部,点亮交集部分颜色

15.PorterDuff.Mode.MULTIPLY 取两图层交集部分叠加后颜色

16.PorterDuff.Mode.SCREEN 取两图层全部区域,交集部分变为透明色

其中后绘制的内容在上层,先绘制的内容在下层。

绘制与缩放

开发涂鸦过程中,把涂鸦内容绘制到bitmap上的时候,保存的图片上没有内容。原因是我使用的是200x200的小图,在View上显示出来的是拉伸了的。使用Canvas c = new Canvas(Bitmap)创建的实际上只有200x200大小的画布,涂鸦测试的时候,坐标都在(200, 200)开外,导致实际不成功。

线程安全

public static Bitmap resizeBitmapByScale(
           Bitmap bitmap, float scale, boolean recycle) {
   int width = Math.round(bitmap.getWidth() * scale);
   int height = Math.round(bitmap.getHeight() * scale);
   if (width == 0 || height == 0) return bitmap;
   if (width == bitmap.getWidth()
           && height == bitmap.getHeight()) return bitmap;
   Bitmap target = Bitmap.createBitmap(width, height, getConfig(bitmap));
   Canvas canvas = new Canvas(target);
   canvas.scale(scale, scale);
   Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
   canvas.drawBitmap(bitmap, 0, 0, paint);
   if (recycle) bitmap.recycle();
   return target;
}

没有使用任何全局静态变量,经检查bitmap对象也是线程安全的。但是仍然出现了线程安全问题,在方法前面加上synchronized关键字后,解决了这个线程问题。但我们如何确定这个不明显的线程安全问题到底是因为这个方法里的哪个“共享资源”引起的呢?可以采用Lock锁的方式来确定共享资源的范围

private static Object mLock = new Object();
synchronized(mLock){
    canvas.drawBitmap();
}

目前怀疑是该方法的native方法中存在静态的全局变量导致。这应该是不被允许的,是框架性问题,还在进一步核实之中。