前言
-
Matrix
是在Android
源碼中出現(xiàn)頻率較高的工具類
- 雖然
Google
已經(jīng)為我們屏蔽了很多數(shù)學細節(jié)茅主,所以使用者即使不了解Matrix
的源碼與數(shù)學知識爆办,也不影響使用Matrix
實現(xiàn)一些基本的效果 - 但是理解
Matrix
的源碼與數(shù)學知識徽惋,對于理解Android
相關的源碼能事半功倍
目錄
1. 矩陣數(shù)學基礎
矩陣相關的數(shù)學基礎知識總結(jié)如下表所示:
2. Matrix 使用步驟
現(xiàn)在我們將視線回到Matrix
锨推,Matrix
本質(zhì)上是一個利用矩陣運算實現(xiàn)坐標變換的工具類赘理,在Android
很多地方可以看到它的身影宦言,我們以ImageView
為例子介紹Matrix
的使用步驟:
步驟1:創(chuàng)建矩陣
ImageView
對象中有兩個Matrix
成員變量:mMatrix
和mDrawMatrix
,具體如下:
// ImageView.java
private Matrix mMatrix;
private Matrix mDrawMatrix;
// 在構造函數(shù)中調(diào)用
private void initImageView() {
mMatrix = new Matrix();
mScaleType = ScaleType.FIT_CENTER;
}
public void setImageMatrix(Matrix matrix) {
// 省略部分代碼...
// 分析點1:參數(shù) matrix 的值拷貝到 mMatrix
mMatrix.set(matrix);
// 分析點2:設置 mDrawMatrix
configureBounds();
// 重繪:觸發(fā)onDraw(Canvas)
invalidate();
}
// Matrix.java
// 分析點1:參數(shù) matrix 的值拷貝到 mMatrix
public void set(Matrix src) {
if (src == null) {
reset();
} else {
// native 方法
nSet(native_instance, src.native_instance);
}
}
可以看到商模,mMatrix
在ImageView
的構造器中就創(chuàng)建了奠旺,另外ImageView
還提供了setImageMatrix(Matrix)
供外部設置蜘澜。那么mDrawMatrix
是在哪里創(chuàng)建的呢?
// ImageView.java
// 分析點2:設置 mDrawMatrix
private void configureBounds() {
// 省略部分代碼...
if (ScaleType.CENTER == mScaleType) {
mDrawMatrix = mMatrix;
// 居中
mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f),Math.round((vheight - dheight) * 0.5f));
}
// 省略部分代碼...
}
configureBounds()
里有多個分支响疚,其中有些分支里將mMatrix
賦值給mDrawMatrix
鄙信,說明兩者是同一個對象。
步驟2:設置矩陣
創(chuàng)建矩陣之后忿晕,就可以使用Matrix
提供的方法設置矩陣了装诡,例如上面的代碼在ScaleType
為ScaleType.CENTER
時使用setTranslate()
設置為居中。
// ImageView.java
mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f), Math.round((vheight - dheight) * 0.5f));
當然了践盼,創(chuàng)建并設置好Matrix
之后慎王,再使用ImageView#setImageMatrix()
設置進來也可以達到同樣的效果。
步驟3:使用矩陣進行坐標變換
現(xiàn)在我們看使用mDrawMatrix
的地方:
// ImageView.java
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 省略部分代碼...
if (mDrawMatrix != null) {
// 分析點1:左乘mDrawMatrix
canvas.concat(mDrawMatrix);
}
mDrawable.draw(canvas);
}
// Canvas.java
// 分析點1:左乘mDrawMatrix
public void concat(@Nullable Matrix matrix) {
if (matrix != null) nConcat(mNativeCanvasWrapper, matrix.native_instance);
}
可以看到宏侍,ImageView#onDraw(Canvas)
中對Canvas
左乘了mDrawMatrix
赖淤,前面說到:矩陣左乘相當于一次坐標變換。我們通過下面一個簡單的例子展示了ImageView
設置Matrix
前后的效果:
// 圖一:未設置Matrix
iv.setBackgroundColor(0xFF999999.toInt())
iv.scaleType = ImageView.ScaleType.MATRIX
iv.setImageResource(R.color.colorAccent)
// 圖二:設置Matrix谅河,縮放到兩倍
val matrix = Matrix().apply {
setScale(2F,2F)
}
iv.imageMatrix = matrix
在后續(xù)的文章里咱旱,我將專門寫一篇文章分享更多ImageView
源碼的細節(jié),感興趣的同學點一點關注哦
3. Matrix 源碼分析
從這一節(jié)開始我們來閱讀Matrix
的源碼绷耍,源碼中出現(xiàn)了native
方法吐限,這意味著Matrix
中的部分源碼是在native
層實現(xiàn),具體分為:Matrix.h褂始、Matrix.cpp诸典、 Matrix.java
3.1 Java 層初始化
// Matrix.java
public final long native_instance;
// sizeof(SkMatrix) is 9 * sizeof(float) + uint32_t
private static final long NATIVE_ALLOCATION_SIZE = 40;
private static class NoImagePreloadHolder {
// 單例
public static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
Matrix.class.getClassLoader(), nGetNativeFinalizer(), NATIVE_ALLOCATION_SIZE);
}
// 從 Android 8 開始,使用 NativeAllocationRegistry 幫助回收 native 層內(nèi)存
public Matrix() {
// 創(chuàng)建一個native層對象崎苗,具體為 SkMatrix
native_instance = nCreate(0);
NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, native_instance);
}
// 在 Android 8 之前
public Matrix() {
native_instance = native_create(0);
}
// Java 層初始化
// ---------------------------------------------------------------------
// native 層初始化
private static native long nCreate(long nSrc_or_zero);
static jlong create(JNIEnv* env, jobject clazz, jlong srcHandle) {
const SkMatrix* src = reinterpret_cast<SkMatrix*>(srcHandle);
SkMatrix* obj = new SkMatrix();
if (src)
// 淺拷貝
*obj = *src;
else
// 重置
obj->reset();
return reinterpret_cast<jlong>(obj);
}
Java
層初始化要點如下:
Matrix
構造器在native
創(chuàng)建了一個SkMatrix
對象狐粱,并通過reinterpret_cast
強制轉(zhuǎn)換為long
賦值給Java
層的native_instance
;Matrix
在Java
層其實沒有太多操作胆数,真正完成任務的實體是native
層的SkMatrix
肌蜻。SKMatrix
是 Skia 圖形引擎提供的用于完成坐標變換的 3 x 3 矩陣;從
Android 8
開始必尼,使用NativeAllocationRegistry
幫助回收 native 層內(nèi)存蒋搜。NativeAllocationRegistry
綁定了Java
層和native
層的兩個對象,并標記內(nèi)存大小為 40字節(jié)判莉,為什么是 40 個字節(jié)呢豆挽?我們在源碼里尋找答案:SkMatrix.h 、SkMatrix.cpp
# 提示 #
NativeAllocationRegistry
是用來幫助回收native
層內(nèi)存的券盅,即當Java
層對象被垃圾回收時帮哈,立即去釋放Native
層的內(nèi)存,在Canvas
渗饮、Bitmap
等類中也有同樣的機制但汞,詳見文章:《Android | 帶你理解 NativeAllocationRegistry 的原理與設計思想》
3.2 native 層初始化
// SkMatrix.h
SK_BEGIN_REQUIRE_DENSE
class SK_API SkMatrix {
public:
enum {
kMScaleX, //!< horizontal scale factor
kMSkewX, //!< horizontal skew factor
kMTransX, //!< horizontal translation
kMSkewY, //!< vertical skew factor
kMScaleY, //!< vertical scale factor
kMTransY, //!< vertical translation
kMPersp0, //!< input x perspective factor
kMPersp1, //!< input y perspective factor
kMPersp2, //!< perspective bias
};
// 分析點1:
SkScalar get(int index) const {
SkASSERT((unsigned)index < 9);
return fMat[index];
}
// 分析點2:重置
void reset();
// 判斷是否為單位矩陣宿刮,使用單位矩陣進行矩陣乘法是無效的
bool isIdentity() const {
return this->getType() == 0;
}
private:
SkScalar fMat[9];
mutable uint32_t fTypeMask;
// SkMatrix.cpp
// 分析點2:重置為單位矩陣
void SkMatrix::reset() {
fMat[kMScaleX] = fMat[kMScaleY] = fMat[kMPersp2] = 1;
fMat[kMSkewX] = fMat[kMSkewY] =
fMat[kMTransX] = fMat[kMTransY] =
fMat[kMPersp0] = fMat[kMPersp1] = 0;
this->setTypeMask(kIdentity_Mask | kRectStaysRect_Mask);
}
// SkScalar.h
typedef float SkScalar;
native
層初始化要點如下:
SkMatrix
有兩個字段:大小為 9 的數(shù)組fMat
和unit21_t
的fTypeMask
,其中SkScalar
其實是一個float
私蕾,具體可以查看:SkScalar.h僵缺,現(xiàn)在你知道 40個字節(jié)(8 * 4 + 4 = 40)是如何的來了嗎?SkMatrix
邏輯上是一個 3 x 3 矩陣踩叭,物理上是一個1 x 9 數(shù)組初始化時會調(diào)用
reset()
磕潮,設置為單位矩陣,注意:使用單位矩陣進行矩陣乘法是無效的
3.3 設置矩陣
前面我們理解了Matrix
初始化時是一個單位矩陣容贝,現(xiàn)在我們開始為矩陣的元素賦值自脯。從Java
層源碼可以看到,Matrix
的方法主要分為setXXX()
斤富、preXXX()
和postXXX()
三大類膏潮,這三類方法有什么區(qū)別呢?我們以scale
為例:
// Matrix.java
// set
public void setScale(float sx, float sy, float px, float py) {
nSetScale(native_instance, sx, sy, px, py);
}
// 左乘
public boolean preScale(float sx, float sy, float px, float py) {
nPreScale(native_instance, sx, sy, px, py);
return true;
}
// 右乘
public boolean postScale(float sx, float sy, float px, float py) {
nPostScale(native_instance, sx, sy, px, py);
return true;
}
// Java 層
// ---------------------------------------------------------------------
// native 層
// Matrix.cpp
// Matrix 中本質(zhì)上使用了SkMatrix满力,這里省略...
// SkMatrix.cpp
void SkMatrix::setScale(SkScalar sx, SkScalar sy, SkScalar px, SkScalar py) {
if (1 == sx && 1 == sy) {
this->reset();
} else {
this->setScaleTranslate(sx, sy, px - sx * px, py - sy * py);
}
}
// | sx 0 tx |
// | 0 sy ty |
// | 0 0 1 |
void setScaleTranslate(SkScalar sx, SkScalar sy, SkScalar tx, SkScalar ty) {
// 省略...
}
void SkMatrix::preScale(SkScalar sx, SkScalar sy, SkScalar px, SkScalar py) {
if (1 == sx && 1 == sy) {
return;
}
// 1. 棧中分配一個SkMatrix對象
SkMatrix m;
// 2. 先調(diào)用setScale
m.setScale(sx, sy, px, py);
// 3. 兩個矩陣乘法
this->preConcat(m);
}
void SkMatrix::postScale(SkScalar sx, SkScalar sy, SkScalar px, SkScalar py) {
if (1 == sx && 1 == sy) {
return;
}
// 1. 棧中分配一個SkMatrix對象
SkMatrix m;
// 2. 先調(diào)用setScale
m.setScale(sx, sy, px, py);
// 3. 兩個矩陣乘法
this->postConcat(m);
}
void SkMatrix::preConcat(const SkMatrix& mat) {
if(!mat.isIdentity()) {
this->setConcat(*this, mat);
}
}
void SkMatrix::postConcat(const SkMatrix& mat) {
if (!mat.isIdentity()) {
this->setConcat(mat, *this);
}
}
要點如下:
-
setScale()
設置了矩陣的縮放屬性
和偏移屬性
焕参,而其他屬性被清除 -
preScale()
先在棧中分配一個新的SkMatrix
,并執(zhí)行左乘:NEW x CUR -
postScale()
先在棧中分配一個新的SkMatrix
油额,并執(zhí)行右邊乘:CUR x NEW
4. 總結(jié)
- 關于
Matrix
的要點已經(jīng)在前面的內(nèi)容中列舉叠纷,這里就不再重復了; - 在后續(xù)的文章里潦嘶,我將與你探討
ImageView
源碼的細節(jié)涩嚣,并實現(xiàn)高仿微信圖片查看控件
,歡迎關注 彭旭銳 的博客掂僵。
參考資料
- 《程序員的數(shù)學基礎課》 —— 黃申 講航厚,極客時間 出品
- 《矩陣乘法的本質(zhì)是什么?》 —— Alepha E 著
- 《深入理解“特征值”和“特征向量”》 —— 人人都會機器學習 著
- 《齊次坐標系入門級思考》
推薦閱讀
- Java | 帶你理解 ServiceLoader 的原理與設計思想
- Android | 帶你理解 NativeAllocationRegistry 的原理與設計思想
- Android | 一文帶你全面了解 AspectJ 框架
- Android | 使用 AspectJ 限制按鈕快速點擊
- Android | 這是一份詳細的 EventBus 使用教程
- 開發(fā)者 | 淺析App社交分享的5種形式
- 計算機組成原理 | Unicode 和 UTF-8是什么關系看峻?
- 計算機組成原理 | 為什么浮點數(shù)運算不精確阶淘?(阿里筆試)