前段時間QQ更新後發現下面的Tab欄添加了動態高斯模糊效果,衆所周知,高斯模糊這玩意兒比較耗時,動態的模糊效果在安卓的APP中比較少見。在本身猜想了幾種作法以後想知道QQ是怎麼實現的,因而反編譯了一下QQ的apk。java
鑑於個人逆向基礎門都沒入,屬於只會用一個jadx查查16進制id這種,這裏就不班門弄斧介紹了,感興趣的能夠本身去搜搜相似的文章看看。不過這裏不得不說QQ的措施作得真好,它裏面的全部控件id,資源id,layout的命名混淆後大部分是name,想經過查找id來尋找代碼文件對我來講基本不可能,曾經反編譯過網易雲音樂的app,它就沒有作這項措施,能夠輕易的經過uiautomatorviewer
工具查找到id後定位到具體的代碼文件。下面能夠看看最後的效果。android
首先能夠知道,這個模糊的效果不是自定義view
就是拿到具體視圖模糊後給Tab
當背景,這裏可使用uiautomatorviewer
看看這個頁面的層級和視圖,以下圖 git
TabWidget
下面還有個自定義
View
,可是id被混淆成了name,於是定位代碼就基本不可能了(僅對我而說)。
既然知道這是個自定義View
,那麼咱們就能夠試着反編譯APK去查找代碼了,由於混淆後的自定義view的類名是不會變的。從上圖能夠看到該View的位於com.tencent.mobileqq
下面,因而試着在這個包名下面尋找一下代碼。 這裏用最簡單的jadx打開QQ的apk後就能夠看到QQ混淆後的代碼了。幾十個包名這裏就不上圖演示了,鑑於對QQ團隊的代碼素養的信任,在mobileqq下面直接鎖定的widget這個包,而後一樣鑑於對QQ團隊代碼命名素養的信任,我着重尋找相似blur
或者gauss
這樣的字眼,果真找到了兩個文件QQBlur
和QQBlurView
github
QQBlur
的代碼了。
public class QQBlur$1 implements Runnable{
private int a = -1;
/* renamed from: a */
final /* synthetic */ StackBlurManager f541a;
final /* synthetic */ azlc this$0;
public QQBlur$1(azlc azlc, StackBlurManager stackBlurManager) {
this.this$0 = azlc;
this.f541a = stackBlurManager;
}
public void run() {
if (!this.this$0.f531b) {
long elapsedRealtime = SystemClock.elapsedRealtime();
if (!(this.a == -1 || this.a == azlc.a)) {
this.this$0.a(this.a, azlc.a);
}
this.a = azlc.a;
int i = azlc.a;
Bitmap process = this.f541a.process(this.this$0.f531b);
if (process != null) {
this.this$0.f519a = process;
} else {
QLog.e("QQBlur", 1, "run: outBitmap is null. OOM ?");
}
long elapsedRealtime2 = SystemClock.elapsedRealtime();
this.this$0.f531b;
this.this$0.f = (elapsedRealtime2 - elapsedRealtime) + this.this$0.f;
View a = this.this$0.f531b;
if (a != null && this.this$0.f) {
a.postInvalidate();
}
}
}
複製代碼
這裏能夠看到這個f531b
(其實在混淆後這個命名是b,前面的531是jadx軟件爲了和其餘b命名區分本身添加的)既能夠是boolean
也是process
方法的int類型,下面還變成了View
。 花了一個週末的時間對整個代碼進行了邏輯分析和從新命名後,後面展現的代碼就是根據我本身的理解從新命名的的類和變量名,若是有想看原混淆代碼的能夠本身去反編譯,或者看我上傳的一份。canvas
先看看QQBlurView
這個類的代碼,下面代碼爲了方便查看和理解,是通過我本身的從新命名後的,想看混淆過的源碼傳送門這邊走QQBlurView 。bash
@TargetApi(19)
public class QQBlurView extends View {
//由於這個模糊效果只支持19以上,因此這個drawable是低於19的狀況下顯示的tab背景
private Drawable mDefaultDrawable;
private BlurPreDraw mBlurPreDraw = new BlurPreDraw(this);
public QQBlurManager mManager = new QQBlurManager();
private boolean mEnableBlur = true;
......
protected void onDraw(Canvas canvas) {
if (!isDrawCanvas()) {
if (this.mEnableBlur) {
setBackgroundDrawable(null);
this.mManager.onDraw((View) this, canvas);
super.onDraw(canvas);
return;
}
setBackgroundDrawable(this.mDefaultDrawable);
super.onDraw(canvas);
}
}
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (this.mManager != null) {
onAttached();
}
}
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (this.mManager != null) {
onDetached();
}
}
public void onAttached() {
Log.d("QQBlurView", "onResume() called");
this.mManager.onResume();
}
public void onDetached() {
Log.d("QQBlurView", "onPause() called");
this.mManager.onPause();
}
public void onDestroy() {
getViewTreeObserver().removeOnPreDrawListener(this.mBlurPreDraw);
this.mManager.onDestroy();
}
public void setTargetView(View view) {
this.mManager.setTargetView(view);
}
public void setBlurView(View view) {
this.mManager.setBlurView(view);
}
public void onCreate() {
getViewTreeObserver().addOnPreDrawListener(this.mBlurPreDraw);
this.mManager.onCreate();
}
public boolean isDrawCanvas() {
return this.mManager.isDrawCanvas();
}
}
複製代碼
這裏能夠看到在onDraw
中若是須要模糊效果的話就將模糊的具體操做都交給了QQBlurManager
,下面還須要注意的點是外界須要傳兩個View
進來,一個是自己這個QQBlurView
,另外一個暫且記作targetView
,後面manager
中會用到。app
在建立完成後須要註冊OnPreDrawListener這
個監聽器,該方法主要做用爲:將繪製視圖樹時執行的回調函數。這時全部的視圖都測量完成並肯定了框架。 客戶端可使用該方法來調整滾動邊框,甚至能夠在繪製以前請求新的佈局。框架
public class BlurPreDraw implements ViewTreeObserver.OnPreDrawListener {
final QQBlurView blurView;
public BlurPreDraw(QQBlurView qQBlurView) {
this.blurView = qQBlurView;
}
public boolean onPreDraw() {
if (this.blurView.mManager!=null) {
return this.blurView.mManager.onPreDraw();
}
return true;
}
}
複製代碼
因爲這個頁面代碼過多,這裏就不所有展現了,從前面能夠看到,視圖註冊了PreDraw
監聽器,然後調用了QQBlurManager
的onPreDraw
方法函數
public boolean onPreDraw() {
boolean isDirty = false;
if (this.mTargetView != null) {
isDirty = this.mTargetView.isDirty();
}
View view = this.mBlurView;
if (!this.isDetachToWindow && isDirty && view != null && view.getVisibility() == View.VISIBLE) {
preDrawCanvas();
view.invalidate();
}
return true;
}
混淆後:
public boolean m104a() {
boolean z = false;
if (this.f526a != null) {
z = this.f526a.a();
} else if (this.f525a != null) {
z = this.f525a.isDirty();
}
View view = this.f533b;
if (!this.f530a && z && view != null && view.getVisibility() == 0) {
e();
view.invalidate();
}
return true;
}
複製代碼
這裏說明下,有一些和混淆無關,與其餘頁面關聯的方法這裏就刪除了,後面也是相似。 從代碼中看到先判斷從前面傳進來的targetView
有沒有發生變化,isDirty
就是判斷targetView
從上次繪製完成後有沒有發生改變,對應效果就是,若是QQ中你沒有滑動列表,就中止模糊方法,畢竟這是一個耗時耗資源的事,而後就調用模糊最關鍵的邏輯代碼preDrawCanvas
。工具
private void preDrawCanvas() {
long elapsedRealtime = SystemClock.elapsedRealtime();
if (this.mTargetView != null && this.mBlurView != null && this.mBlurView.getWidth() > 0 && this.mBlurView.getHeight() > 0) {
/**這裏的mScale是由於這裏用的模糊方式是java的StackBlur方法,
先把要模糊的視圖縮小模糊後再放大,這樣可以下降耗時*/
Bitmap createBitmap;
int scaleWidth = QQBlurManager.ceil((float) this.mBlurView.getWidth(), this.mScale);
int scaleHeight = QQBlurManager.ceil((float) this.mBlurView.getHeight(), this.mScale);
int a3 = QQBlurManager.fixBy16(scaleWidth);
int a4 = QQBlurManager.fixBy16(scaleHeight);
//下面計算是爲了獲取16位取整後正確的縮放係數
this.c = ((float) scaleHeight) / ((float) a4);
this.b = ((float) scaleWidth) / ((float) a3);
float f = this.mScale * this.b;
float f2 = this.mScale * this.c;
try {
createBitmap = Bitmap.createBitmap(a3, a4, Config.ARGB_8888);
} catch (Throwable e) {
Log.e("QQBlur", "prepareBlurBitmap: ", e);
createBitmap = null;
}
if (createBitmap != null) {
this.mBlurBitmapWidth = (long) createBitmap.getWidth();
this.mBlurBitmapHeight = (long) createBitmap.getHeight();
if (VERSION.SDK_INT >= 19) {
mBlurBitmapByteCount = (long) createBitmap.getAllocationByteCount();
} else {
mBlurBitmapByteCount = (long) createBitmap.getByteCount();
}
//設置bitmap的是否透明的值,原代碼傳過來的值爲-1
createBitmap.eraseColor(mBlurBitmapEraseColor);
this.mCanvas.setBitmap(createBitmap);
int[] iArr = new int[2];
this.mBlurView.getLocationInWindow(iArr);
int[] iArr2 = new int[2];
this.mTargetView.getLocationInWindow(iArr2);
this.mCanvas.save();
//這裏是造成動態模糊最關鍵的一行代碼,這裏將canvas平移後得到兩個view交叉部分
//的座標點
this.mCanvas.translate(((float) (-(iArr[0] - iArr2[0]))) / f, ((float) (-(iArr[1] - iArr2[1]))) / f2);
this.mCanvas.scale(1.0f / f, 1.0f / f2);
//這個是模糊的具體操做代碼,後面會說明
StackBlurManager stackBlurManager = new StackBlurManager(createBitmap);
stackBlurManager.setDbg(true);
stackBlurManager.setExecutorThreads(stackBlurManager.getExecutorThreads());
this.isDrawCanvas = true;
if (VERSION.SDK_INT <= 27 || this.mBlurView.getContext().getApplicationInfo().targetSdkVersion <= 27) {
//爲何這裏要在27如下采用這種方法其實沒太看懂,不過做用是爲了裁剪出和須要模糊
//同等大小的區域,而後將目標視圖呈現到咱們給定的畫布上
Rect clipBounds = this.mCanvas.getClipBounds();
clipBounds.inset(-createBitmap.getWidth(), -createBitmap.getHeight());
if (this.mCanvas.clipRect(clipBounds, Op.REPLACE)) {
this.mTargetView.draw(this.mCanvas);
} else {
Log.e("QQBlur", "prepareBlurBitmap: canvas clip rect empty. Cannot draw!!!");
}
} else {
//將目標視圖呈現到咱們給定的畫布上
this.mTargetView.draw(this.mCanvas);
}
this.mCanvas.restore();
clearViewVisible();
Log.i("高斯模糊", "建立bitmap" + createBitmap);
this.isDrawCanvas = false;
//將模糊的操做放到線程中進行
this.mHandler.post(new QQBlur(this, stackBlurManager));
} else {
return;
}
}
//這裏的數值是用來調試用的,計算每次裁剪的耗時
long elapsedRealtime2 = SystemClock.elapsedRealtime();
this.mPreViewCount++;
this.mPreViewTime = (elapsedRealtime2 - elapsedRealtime) + this.mPreViewTime;
}
private static int ceil(float f, float f2) {
return (int) Math.ceil((double) (f / f2));
}
public static int fixBy16(int i) {
return i % 16 == 0 ? i : (i - (i % 16)) + 16;
}
複製代碼
上面作了簡單的註釋,這邊作一下總結:
targetView
和blurView
的座標值,經過相減計算出他們交叉區域的座標點targetView
的這一部份內容繪製到canvas
上,也就是會知道了前面建立好的bitmap
上,而後模糊這一bitmap
在繪製到blurView
上就實現了對交叉這一區域的模糊。 下面用簡單的圖展現一下。
在QQ中,上面的聊天列表就是這個targetView
,最下面有一層blurView
,上面是透明的tab,實現動態模糊的邏輯就是不斷去獲取targetView
和blurView
交叉這一部分區域的視圖,將這部分視圖模糊後繪製在blurView
上,就造成了一種動態模糊的效果。在上面展現的例子中就是這麼調用的
QQBlurView qqBlurView = findViewById(R.id.blur);
List<String> list = new ArrayList<>();
for (int i = 0; i < 50; i++) {
list.add("" + i);
}
ListView listView = findViewById(R.id.listView);
listView.setAdapter(new MainAdapter(this, list));
qqBlurView.setEnableBlur(true);
qqBlurView.setBlurView(qqBlurView);
qqBlurView.setTargetView(listView);
qqBlurView.setBlurRadius(1);
qqBlurView.setEraseColor(-1);
qqBlurView.onCreate();
qqBlurView.onAttached();
複製代碼
最後一個就是模糊調用的線程方法,這裏面的還原我其實沒有100%的把握,緣由前面也說了,這裏的命名類型不同,可是命名倒是如出一轍的,因此盡我所能去理解並還原了
public class QQBlur implements Runnable {
private int a = -1;
final StackBlurManager mStackBlurManager;
final QQBlurManager mQQblurManager;
public QQBlur(QQBlurManager QQblurManager, StackBlurManager stackBlurManager) {
this.mQQblurManager = QQblurManager;
this.mStackBlurManager = stackBlurManager;
}
public void run() {
if (!this.mQQblurManager.isDrawCanvas()) {
long elapsedRealtime = SystemClock.elapsedRealtime();
if (!(this.a == -1 || this.a == QQBlurManager.mBlurType)) {
this.mQQblurManager.onPolicyChange(this.a, QQBlurManager.mBlurType);
}
this.a = QQBlurManager.mBlurType;
int i = QQBlurManager.mBlurType;
Bitmap process = this.mStackBlurManager.process(this.mQQblurManager.mRadius);
if (process != null) {
this.mQQblurManager.mBitmap = process;
} else {
Log.e("QQBlur", "run: outBitmap is null. OOM ?");
}
long elapsedRealtime2 = SystemClock.elapsedRealtime();
this.mQQblurManager.mBlurThreadCount++;
this.mQQblurManager.mBlurThreadTime = (elapsedRealtime2 - elapsedRealtime) + this.mQQblurManager.mBlurThreadTime;
View blurView = this.mQQblurManager.mBlurView;
if (blurView != null && this.mQQblurManager.isDrawing) {
blurView.postInvalidate();
}
}
}
}
//該方法不是這個類裏面的,這裏展現下QQ中總共包含了這四種模糊方式,這裏才用的是最後一終
private CharSequence selectBlurType(int i) {
switch (i) {
case 1:
return "StackBlur.Native";
case 2:
return "StackBlur.RS";
case 3:
return "GaussBlur.RS";
default:
return "StackBlur.Java";
}
}
複製代碼
這段代碼最主要的是Bitmap process = this.mStackBlurManager.process(this.mQQblurManager.mRadius);
這一行,調用模糊方法,前面主要是判斷模糊方式有沒有變化,後面mBlurThreadCount
,mBlurThreadTime
也是調試參數用的,最後模糊完成後刷新界面,這裏的StackBlurManager是一個第三方庫,有興趣的小夥伴能夠本身去看一下,QQ作了一些細微的調整,增長了一兩個方法。
public class StackBlurManager {
static int EXECUTOR_THREADS = Runtime.getRuntime().availableProcessors();
static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(EXECUTOR_THREADS);
private static final String TAG = "StackBlurManager";
private static volatile boolean hasRS = true;
private final BlurProcess _blurProcess;
private final Bitmap _image;
private Bitmap _result;
private boolean mDbg = true;
public StackBlurManager(Bitmap image) {
this._image = image;
this._blurProcess = new JavaBlurProcess();
}
public Bitmap process(int radius) {
long start = SystemClock.uptimeMillis();
this._result = this._blurProcess.blur(this._image, 8);
Log.i(TAG, "process: " + this._blurProcess + "=" + (SystemClock.uptimeMillis() - start) + " ms");
return this._result;
}
複製代碼
這裏默認的模糊方式是JavaBlurProcess,該方法後面還有不少其餘模糊方式的方法,應該是在其餘狀況下供別的地方調用的,感興趣的朋友能夠本身看一下StackBlurManager這個類,這裏的是QQ上的,上面那個是第三方框架中的。
其實這兩個是意外發現的,這是QQ中本身作的線程調度和自定義的handler用法,並且這寫方法並無混淆,有興趣的話能夠觀摩學習下代碼,我下面貼幾行QQ裏面這些類的用法。
ThreadManager.getSubThreadHandler().post(runnable);
ThreadManager.getSubThreadHandler().removeCallbacks(runnable);
ThreadManager.getSubThreadHandler().post(new Runnable() {
public void run() {
aron aron = (aron) MainFragment.this.a.getManager(319);
((aivy) MainFragment.this.a.a(2)).w();
}
});
ThreadManager.getSubThreadHandler().postDelayed(anonymousClass1, 2000);
ThreadManager.getUIHandler().postDelayed(new Runnable() {
public void run() {
MainFragment.this.a;
MainFragment.this.C();
}
}, 2000);
ThreadManager.post(new Runnable() {
public void run() {
if (BaseApplicationImpl.getContext() != null) {
ayiv.a(BaseApplicationImpl.getContext(), "");
}
}
}, 2, null, true);
if (this.f29a == null) {
this.f29a = new MqqHandler();
}
this.f29a.postDelayed(new Runnable() {
public void run() {
String a = MainFragment.this.a;
if ("消息".equals(a) || "聯繫人".equals(a)) {
String str = "消息".equals(a) ? "Msg_tab" : "Contacts_tab";
auzd.b(MainFragment.this.a, "CliOper", "", "", str, str, 0, 0, "", "", "", "");
}
if (MainFragment.this.a != null && MainFragment.this.a.getBoolean(ThemeUtil.THEME_VOICE_SETTING + MainFragment.this.a.getCurrentAccountUin(), true)) {
MainFragment.this.a.b(i + 1);
}
if (a != null && AppSetting.c) {
MainFragment.this.a("消息", a);
MainFragment.this.a("聯繫人", a);
MainFragment.this.a("動態", a);
MainFragment.this.a("NOW", a);
}
}
}, 100);
複製代碼
以上就是此次查找代碼的所有收穫,所有的代碼包括我本身命名的,混淆的原代碼,ThreadManager類和StackBlurManager類我都上傳到了這裏,本意是讓你們學習大廠的代碼邏輯和思想,若有侵權,第一時間聯繫我刪除,謝謝了。