一個開發仔的平常離不開和產品經理的Speak,但大多數時候嗶嗶一堆,不如一句「直接說抄哪一個APP」。借(chao)鑑(xi)是門手藝活,簡單的瞄一下,點幾下,可能就知道大概的實現邏輯了,可是「知道 != 寫得出來」,一看就會,一作就廢是常事。既然本身寫不出來,那就去「偷」!是的,你沒聽錯,去偷別人的代碼。「盜亦有道」:掌握適當的技巧能夠幫咱們更快,更順利得偷到別人的APP代碼,本節以偷「掘金的消息卡片代碼」爲例,講解一波。java
發完《Kotlin刨根問底——你真的瞭解Kotlin中的空安全嗎?》這篇文章後,習慣性地把文章連接往個人小破羣裏一丟,接着套用模(mú)板:剛擼的文章,講xxx的,取需。簡短的一句,如無病呻吟,換來幾句「看不懂,可是,羣主牛逼的商業互吹」以及「十位數的閱讀量」android
羣分享完了,接着小號分享朋友圈,分享時,看到「消息卡片」的這個選項:git
點擊生成後的卡片還挺精美,嘖嘖嘖,正所謂:愛漂亮之心,人皆有之~github
這種分享生成卡片圖的操做很常見,經常使用於各類導流,好比抖音的抖音碼:面試
感受能夠給「摳腚早報速讀」也搞一個,畢竟 花裏胡哨的圖片 比 沒有靈魂的文字和連接 有趣得多。行吧,偷一波「掘金消息卡片的代碼」:shell
其實吧,實現原理還挺簡單的(噗嗤~):canvas
寫一個卡片頁面的佈局,而後調用 View.draw() 實現View截圖Bitmap,把Bitmap保存到相冊。緩存
接着的內容,你們配合下個人演出,開啓裝傻模式吧!安全
僞裝客戶端和服務端在那裏激烈甩鍋:bash
爭論信息圖片「由服務端生成的」仍是「由客戶端生成的」狀:
- 客戶端:我丟,寫個接口,我調用的時候給我生成卡片,直接顯示,美滋滋啊!
- 服務端:美毛線,吃太飽的一直點生成,接口一直調?並且生成要時間啊!
- 客戶端:緩存啊,生成過的緩存起來,生成過的直接返回,還要我教,菜虛鯤?
- 服務端:你這樣浪費資源啊,還要找個服務器放這些圖,請求生成卡片的併發量 太大後臺會炸的,一個簡單的生成頁面,搞那麼複雜?高內聚低耦合,你懂不懂?
此時我化身一個 和事佬 出現:
嗶嗶那麼多,驗證下,看別人是怎麼作的不就行了,最簡單的方法,手機依次點擊:
設置 -> 開發者選項 -> 勾選顯示佈局邊界
接着:
回到掘金點擊消息卡片 -> 截圖 -> 點擊保存 -> 打開圖庫
能夠看到下面這兩個圖片:
是的,右側生成的卡片有「佈局邊界」,就是客戶端生成的!除此以外,還能夠經過抓包來驗證。
抓包區間是「打開信息卡片前」和「點擊保存後」,看下是否有拉取卡片圖的請求。
熟練的打開Fidder,安裝證書,打開WIFI手動設置下代理:主機ip,8888,卻發現抓不了HTTPS包。即便換成Charles,Wireshark等其餘抓包工具也抓不到,緣由是:
Android 7.0(Nougat,牛軋糖)開始,Android更改了對用戶安裝證書的默認信任行爲,應用程序「只信任系統級別的CA」。
對此,若是是本身寫的APP想抓HTTPS的包,能夠在 res/xml
目錄下新建一個network_security_config.xml
文件,複製粘貼以下內容:
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" overridePins="true" /> <!--信任系統證書-->
<certificates src="user" overridePins="true" /> <!--信任用戶證書-->
</trust-anchors>
</base-config>
</network-security-config>
複製代碼
接着**AndroidManifest.xml
文件中新增networkSecurityConfig**屬性引用xml文件,以下:
<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
<application android:networkSecurityConfig="@xml/network_security_config"
... >
...
</application>
</manifest>
複製代碼
調試期爲了方便本身抓包能夠加上,發版本的時候,記得刪掉哦!!!固然,大部分時候都是想抓別人APP的HTTPS包,最簡單的兩種處理方法:
- 一、「系統降級」即採用Android 7.0如下手機,好比筆者的抓包雞魅藍E2就是Android 6.0。
稍微複雜點,也是比較常見的方法「手機Root,把證書安裝到系統證書中」,具體操做步驟以下:
# 打開終端,輸入下述命令把 cer或者der 證書轉換爲pem格式
openssl x509 -inform der -in xxx.cer -out xxx.pem
# 證書重命名,命名規則爲:<Certificate_Hash>.<Number>
# Certificate_Hash:表示證書文件的hash值
# Number:爲了防止證書文件的hash值一致而增長的後綴
# 經過下述命令計算出hash值
openssl x509 -subject_hash_old -in Fiddler.pem |head -1
# 重命名,hash值是上面命令執行輸出的hash值,好比269953fb
mv Fiddler.pem <hash>.0
# adb push命令把文件複製到內部存儲
adb push <hash>.0 /sdcard/
adb shell # 啓動命令行交互
su # 得到超級用戶權限
mount -o remount,rw /system # 將/system目錄從新掛載爲可讀寫
cp /sdcard/<hash>.0 /system/etc/security/cacerts/ # 把文件複製過去
cd /system/etc/security/cacerts/ # 來到目錄下
chmod 644 <hash>.0 # 修改文件權限
# 重啓設備
adb reboot
複製代碼
重啓後,看下可否抓到HTTPS包就知道是否安裝成功,也能夠到設置 -> 安全性和位置信息 -> 加密與憑據 -> 信任的憑據 -> 系統 裏找找本身剛安裝的證書(不一樣手機路徑可能不同)。
其餘抓包工具也是如法炮製,除此還有一種成本更高的方法:「二次打包APK」,不過如今反編譯愈來愈難,不必定能打包成功,固然也說說流程:
- ① 經過apktool反編譯apk;
- ② 在res/xml目錄中建立文件network_security_config.xml;
- ③ AndroidManifest.xml添加android:networkSecurityConfig屬性;
- ④ 從新打包並自簽名APK
說回正題,抓包,證實消息卡片不是請求後臺獲取到的:
在點擊消息卡片以及到保存這一步,只下載了頭圖,而非卡片圖,大概能猜想到:
打開頁面時傳入標題,內容簡介,文章頭圖,連接等,而後生成消息卡片。
而view生成截圖的方式有兩種:分別是調用View的 getDrawingCache() 和 draw() 方法,不過前者已經Deprecated(過期),點開源碼能夠看到這樣的註釋:
繼續僞裝不知道原理,接着把佈局給摳出來。
僞裝不會實現這個背景圖,使用「Apktool」反編譯一波apk,直接拿資源文件,工具包可本身百度或 公號回覆001 獲取,若是反編譯出現以下錯誤信息:
可嘗試更新一波apktool.jar的版本,到:bitbucket.org/iBotPeaches… 下載最新版的Jar包替換便可。接着鍵入下述命令反編譯apk:
apktool.bat d -f xxx.apk
複製代碼
坐等編譯成功:
接着打開編譯後的項目的drawable目錄,搜索:bg_,能夠看到:
盲猜bg_message_card.xml,打開看看:
複製到工程中,稍微調整下,看下預覽效果:
能夠的,接着把用到的圖片素材找出來,複製到工程中,接着咱們來堆砌佈局。
先來了解佈局的層次,最簡單的方法:經過adb命令來查看:
adb shell dumpsys activity top > info.txt
複製代碼
上述命令會導出activity的堆棧信息到info.txt文件中,搜索應用包名,可定位到當前顯示的Activity:
往下一點,能夠看到View的層次結構:
從這裏就能夠看到當前這個頁面都是由哪些控件堆砌而成的, 不過可能不是很直觀,接着安利一個Android調試工具:「開發者助手」(可到酷安搜索或 公號回覆002獲取),須要Root權限!!!直接能夠看到包名,版本,當前Activity,Fragment,界面資源分析等信息。
點下界面資源分析,頁面組成一清二楚。
知道佈局是由哪些控件堆砌而成的,是遠遠不夠的,咱們還須要知道控件具體是怎麼堆的。即:寬高多少,margin和padding多少,字體多大,顏色值,是否加粗等等這些信息。一種比較低效的方法是:
手機截圖,發送到電腦,用PxCook之類的工具打開,而後用尺子去量尺寸,取色工具取色。
能夠是不能夠,不過有點撈啊,有沒有更便捷的方法呢?答案確定是有,再安利一個調試工具:UETool,一個移動端頁面調試工具:
不過這個工具,只能 在本身的項目中集成,並不能用來調教別人的APP,須要上擴展版:VirtualUETool
Tips:Virtual App 是著名的黑產神器,App虛擬化引擎,能夠在其中建立虛擬空間,而後在虛擬空間裏安裝運行卸載APP,最多見的使用場景就是應用分身,以前是開源的,不過看README.md貌似開始商業化了...
VirtualUETool 用法比較簡單,「捕捉控件」能夠看到控件的一些屬性信息,「相對位置」能夠看控件寬高和與其餘控件的間距,「網格柵欄」能夠用來看控件是否對齊,「佈局層級」以3D模式查看層級。使用示例以下:
邊框,參數這些都有了,堆佈局就不是什麼大問題了~
這裏沒有直接用它的佈局文件,而是參照着本身另外寫了一個,利用前面獲取的一些邊角料。
佈局文件:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF333333"
tools:context=".MainActivity">
<android.support.constraint.ConstraintLayout
android:id="@+id/cly_share_bar"
android:layout_width="0dp"
android:layout_height="98dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:background="#FFEEEEEE">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/iv_share_wx"
android:layout_width="0dp"
android:layout_height="44dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/iv_share_pyq"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/tv_share_wx"
android:scaleType="fitCenter"
android:src="@drawable/share_wechat"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_share_wx"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintLeft_toLeftOf="@id/iv_share_wx"
app:layout_constraintRight_toRightOf="@id/iv_share_wx"
app:layout_constraintTop_toBottomOf="@id/iv_share_wx"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="12sp"
android:textColor="#8A000000"
android:text="微信"/>
<android.support.v7.widget.AppCompatImageView
android:id="@+id/iv_share_pyq"
android:layout_width="0dp"
android:layout_height="44dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@id/iv_share_wx"
app:layout_constraintRight_toLeftOf="@id/iv_share_qq"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/tv_share_pyq"
android:scaleType="fitCenter"
android:src="@drawable/share_circle"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_share_pyq"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintLeft_toLeftOf="@id/iv_share_pyq"
app:layout_constraintRight_toRightOf="@id/iv_share_pyq"
app:layout_constraintTop_toBottomOf="@id/iv_share_pyq"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="12sp"
android:textColor="#8A000000"
android:text="朋友圈"/>
<android.support.v7.widget.AppCompatImageView
android:id="@+id/iv_share_qq"
android:layout_width="0dp"
android:layout_height="44dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@id/iv_share_pyq"
app:layout_constraintRight_toLeftOf="@id/iv_share_wb"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/tv_share_qq"
android:scaleType="fitCenter"
android:src="@drawable/share_qq"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_share_qq"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintLeft_toLeftOf="@id/iv_share_qq"
app:layout_constraintRight_toRightOf="@id/iv_share_qq"
app:layout_constraintTop_toBottomOf="@id/iv_share_qq"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="12sp"
android:textColor="#8A000000"
android:text="QQ"/>
<android.support.v7.widget.AppCompatImageView
android:id="@+id/iv_share_wb"
android:layout_width="0dp"
android:layout_height="44dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@id/iv_share_qq"
app:layout_constraintRight_toLeftOf="@id/iv_share_save"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/tv_share_wb"
android:scaleType="fitCenter"
android:src="@drawable/share_weibo"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_share_wb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintLeft_toLeftOf="@id/iv_share_wb"
app:layout_constraintRight_toRightOf="@id/iv_share_wb"
app:layout_constraintTop_toBottomOf="@id/iv_share_wb"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="12sp"
android:textColor="#8A000000"
android:text="微博"/>
<android.support.v7.widget.AppCompatImageView
android:id="@+id/iv_share_save"
android:layout_width="0dp"
android:layout_height="44dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@id/iv_share_wb"
app:layout_constraintRight_toLeftOf="@id/iv_share_other"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/tv_share_save"
android:scaleType="fitCenter"
android:src="@drawable/share_save"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_share_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintLeft_toLeftOf="@id/iv_share_save"
app:layout_constraintRight_toRightOf="@id/iv_share_save"
app:layout_constraintTop_toBottomOf="@id/iv_share_save"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="12sp"
android:textColor="#8A000000"
android:text="保存"/>
<android.support.v7.widget.AppCompatImageView
android:id="@+id/iv_share_other"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintLeft_toRightOf="@id/iv_share_save"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/tv_share_other"
android:scaleType="fitCenter"
android:src="@drawable/share_others"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_share_other"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintLeft_toLeftOf="@id/iv_share_other"
app:layout_constraintRight_toRightOf="@id/iv_share_other"
app:layout_constraintTop_toBottomOf="@id/iv_share_other"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="12sp"
android:textColor="#8A000000"
android:text="保存"/>
</android.support.constraint.ConstraintLayout>
<android.support.v4.widget.NestedScrollView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="2dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/cly_share_bar"
android:background="@drawable/bg_message_card">
<android.support.constraint.ConstraintLayout
android:id="@+id/cly_content"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
android:id="@+id/cly_card"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginEnd="14dp"
android:layout_marginTop="26dp"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:background="@drawable/shape_bg_content">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/iv_avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="22dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/tv_level"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
app:layout_constraintLeft_toRightOf="@id/iv_avatar"
app:layout_constraintTop_toTopOf="@id/iv_avatar"
android:textSize="16sp"
android:drawablePadding="5dp"
android:textColor="#FF1C1C1E"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="@id/tv_level"
app:layout_constraintBottom_toBottomOf="@id/iv_avatar"
android:textSize="12sp"
android:textColor="#FF8A9AA9"/>
<android.support.v7.widget.AppCompatImageView
android:id="@+id/iv_article_hover"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginTop="22dp"
app:layout_constraintLeft_toLeftOf="@id/iv_avatar"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_avatar"
android:adjustViewBounds="true"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_article_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:layout_constraintLeft_toLeftOf="@id/iv_article_hover"
app:layout_constraintRight_toRightOf="@id/iv_article_hover"
app:layout_constraintTop_toBottomOf="@id/iv_article_hover"
android:textSize="20sp"
android:textStyle="bold"
android:lineSpacingExtra="4dp"
android:textColor="#FF1C1C1E"
android:text=""/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_article_summary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="14dp"
android:layout_marginRight="14dp"
android:layout_marginTop="9dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_article_title"
android:textSize="16sp"
android:lineSpacingExtra="4dp"
android:textColor="#FF1C1C1E"
android:text=""/>
<android.support.v7.widget.AppCompatImageView
android:id="@+id/iv_article_qrcode"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginTop="18dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_article_summary"
android:scaleType="fitCenter"
android:src="@drawable/ic_qr_code"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_article_tips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="12dp"
android:paddingBottom="12dp"
app:layout_constraintLeft_toLeftOf="@id/iv_article_qrcode"
app:layout_constraintRight_toRightOf="@id/iv_article_qrcode"
app:layout_constraintTop_toBottomOf="@id/iv_article_qrcode"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="11sp"
android:textColor="#FF1C1C1E"
android:text="長按識別二維碼"/>
</android.support.constraint.ConstraintLayout>
<android.support.v7.widget.AppCompatImageView
android:id="@+id/iv_adaptive_logo"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginTop="14dp"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tv_slogan"
app:layout_constraintTop_toBottomOf="@id/cly_card"
android:scaleType="fitCenter"
android:src="@drawable/adaptive_logo"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_slogan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@+id/iv_adaptive_logo"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/iv_adaptive_logo"
app:layout_constraintBottom_toBottomOf="@id/iv_adaptive_logo"
android:textSize="12dp"
android:textColor="#FFFCFCFC"
android:text="掘金 · 一個幫助開發者成長的技術社區"/>
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_host"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="1dp"
android:layout_marginBottom="24dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_slogan"
app:layout_constraintBottom_toBottomOf="parent"
android:textSize="12dp"
android:textColor="#FFFCFCFC"
android:text="juejin.im"/>
</android.support.constraint.ConstraintLayout>
</android.support.v4.widget.NestedScrollView>
</android.support.constraint.ConstraintLayout>
複製代碼
界面文件:MainActivity.kt
package com.coderpig.kttest
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.bumptech.glide.request.RequestOptions
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private val authorAvatarUrl = "https://user-gold-cdn.xitu.io/2019/5/10/16a9fc6bbb83e12e?imageView2/0/h/110/q/110"
private val authorNickName = "coder-pig"
private val authorDesc = "網管@摳腚網咖"
private val authorLevel = 1
private val articleCoverUrl =
"https://user-gold-cdn.xitu.io/2019/7/24/16c22e09ebcc9819?w=1918&h=1067&f=jpeg&s=601259"
private val articleTitle = "Kotlin刨根問底——你真的瞭解Kotlin中的空安全嗎?"
private val articleSummary =
"每一個人的時間都是有限的,一旦作出學習某塊知識的選擇,意味着付出了暫時沒法學習其餘知識的機會成本,須要取捨。不可能等什麼都學會了再去面試,學完得猴年馬月,並且技術,是學不完的… 初次接觸Kotlin已經是三年前,在上家公司用Kotlin重構了平板的應用市場和電臺APP。說來慚愧,至…"
private val articleUrl = "https://juejin.im/entry/5d38086f6fb9a07f00531d24"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Glide.with(this).load(authorAvatarUrl).apply(RequestOptions.bitmapTransform(CircleCrop())).into(iv_avatar)
tv_level.text = authorNickName
val rightDrawable = when (authorLevel) {
1 -> getDrawable((R.drawable.ic_user_lv1))
2 -> getDrawable((R.drawable.ic_user_lv2))
3 -> getDrawable((R.drawable.ic_user_lv3))
4 -> getDrawable((R.drawable.ic_user_lv4))
5 -> getDrawable((R.drawable.ic_user_lv5))
6 -> getDrawable((R.drawable.ic_user_lv6))
7 -> getDrawable((R.drawable.ic_user_lv7))
8 -> getDrawable((R.drawable.ic_user_lv8))
else -> null
}
rightDrawable?.setBounds(0, 0, rightDrawable.minimumWidth, rightDrawable.minimumHeight)
tv_level.setCompoundDrawables(null, null, rightDrawable, null)
tv_desc.text = authorDesc
tv_article_title.text = articleTitle
tv_article_summary.text = articleSummary
Glide.with(this).load(articleCoverUrl).into(iv_article_hover)
}
}
複製代碼
運行效果圖以下:
哈哈,像不像,真的不是截掘金哈,替換一波文章相關的信息,運行下看看:
細心的你應該能發現這個模糊的二維碼(直接用的截圖),以及右上角缺失了的文章標籤,僞裝不知道二維碼能夠用zxing實現,看看掘金是怎麼作的?
在開發者助手那裏能夠看到「未知加固」,通常就是沒有加固,不用脫殼美滋滋,Jadx反編譯一波源碼(jadx直接反編譯apk很容易直接卡死,筆者寫了個Python的批處理腳本,取需:github.com/coder-pig/C…),反編譯後用Android Studio打開反編譯後的項目,記得順帶把前面apktool反編譯出來的res資源文件夾也丟進去!
接着全局搜索文件CommonActivity.java,代碼裏搜下setContentView,能夠看到:
這裏和沉浸式狀態欄有關,兼容Android 5.0如下,佈局以下大同小異:
自定義了一個StatusBarView狀態欄和一個幀佈局容器FixInsetsFrameLayout,中間塞個toolbar,這裏主要關注這個容器,佈局id:fragment_container,八九不離十是用來塞Fragment的,搜下:
getSupportFragmentManager().beginTransaction().replace
複製代碼
能夠看到:
噢,兩種建立方法耶:
- 一、直接Intent傳FRAGMENT_NAME建立
- 二、經過ARouter建立
第一種見得多了,第二種用的是阿里的ARouter路由,點進去**ServiceFactory.getInstance().getFragment()**方法:
就是try裏面包着的這一句,去ARouter的Github倉庫就能夠翻到混淆前的樣子是:
Fragment fragment = (Fragment) ARouter.getInstance().build("/xxx/xxxfragment").navigation();
複製代碼
嘖嘖嘖,怪不得開發者助手那裏沒法檢測當前Fragment,行吧,咱們須要找到這個fragment的路徑,在CommonActivity.java這個文件中顯然是很難繼續下去的:
解固然也是有解的,動態調試smali或者xposed寫個簡單插件打印日誌,不過有點繁瑣,換種姿式吧。先明確下如今的目標:
找到消息卡片的佈局!!!
em...發現卡片底部有一句掘金的slogan:一個幫助開發者成長的技術社區,全局搜下?
23333,直指 fragment_entry_pin_card.xml佈局,打開看看:
圈住的部分分別是二維碼對應的控件和右上角標籤,接着全局搜R.layout.fragment_entry_pin_card
定位到了PreviewEntryPinCardFragment這個類,就是消息卡片對應的Fragment,接着搜顯示二維碼控件的id:iv_qrcode:
定位到BitmapUtils類的**create2DCode()**方法:
導包處能夠看到用到了google的zxing庫:
複製粘貼,轉一波Kotlin,這裏Hashtable須要明確傳入類型:
接着顯示二維碼的控件調用 setImageBitmap() 設置一波,結果以下:
能夠,很舒服,你可能對這裏的**-16777216**有疑問,其實就是一個十進制的顏色值,代碼轉下十六進制:
print(String.format("%08x",-16777216))
複製代碼
打印:ff000000,即不透明黑色,對應屬性Color.Black,偷二維碼生成代碼任務完成!
接着到右上角的文章標籤了,在佈局文件裏看到這個自定義控件im.juejin.android.base.views.labelview.LabelView,搜下LabelView.java文件,開偷,若是你有必定的Kotlin語法基礎和自定義View基礎,偷起來仍是非常不難的,留意下這個東西:
就是自定義屬性,反編譯後的attrs.xml裏是找不到這個LabelView的,須要本身自定義一個,定義declare-styleable標籤,把相關屬性從反編譯後的attrs.xml中選擇性複製,好比這裏:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LabelView">
<attr name="font" format="reference"/>
<attr name="fontWeight" format="integer"/>
<attr name="foregroundInsidePadding" format="boolean"/>
<attr name="il_max_length" format="integer"/>
<attr name="il_hint" format="string"/>
<attr name="lv_text" format="string"/>
<attr name="lv_text_color" format="color"/>
<attr name="lv_text_size" format="dimension"/>
<attr name="lv_text_bold" format="boolean"/>
<attr name="lv_text_all_caps" format="boolean"/>
<attr name="lv_background_color" format="color"/>
<attr name="lv_min_size" format="dimension"/>
<attr name="lv_padding" format="dimension"/>
<attr name="lv_gravity">
<enum name="BOTTOM_LEFT" value="83"/>
<enum name="BOTTOM_RIGHT" value="85"/>
<enum name="TOP_LEFT" value="51"/>
<enum name="TOP_RIGHT" value="53"/>
</attr>
<attr name="lv_fill_triangle" format="boolean" />
</declare-styleable>
</resources>
複製代碼
接着甚至連實現原理都不用去看,無腦複製代碼進項目中,自動Java轉Kotlin,處理一波語法問題和刪除無關代碼,調整後的LabelView代碼以下:
package com.coderpig.kttest
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.View
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt
class LabelView : View {
private var mBackgroundColor: Int = 0
private var mBackgroundPaint: Paint = Paint(1)
private var mFillTriangle: Boolean = false
private var mGravity: Int = 0
private var mMinSize: Float = 0.0f
private var mPadding: Float = 0.0f
private var mPath: Path = Path()
private var mTextAllCaps: Boolean = false
private var mTextBold: Boolean = false
private var mTextColor: Int = 0
private var mTextContent: String = ""
private var mTextPaint: Paint = Paint(1)
private var mTextSize: Float = 0.0f
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs, 0) {
obtainAttributes(context, attrs)
this.mTextPaint.textAlign = Paint.Align.CENTER
}
private fun obtainAttributes(context: Context, attributeSet: AttributeSet?) {
val obtainStyledAttributes = context.obtainStyledAttributes(attributeSet, R.styleable.LabelView)
this.mTextContent = obtainStyledAttributes.getString(R.styleable.LabelView_lv_text) as String
this.mTextColor = obtainStyledAttributes.getColor(
R.styleable.LabelView_lv_text_color, Color.parseColor("#FFFFFF")
)
this.mTextSize = obtainStyledAttributes.getDimension(R.styleable.LabelView_lv_text_size, sp2px(11.0f).toFloat())
this.mTextBold = obtainStyledAttributes.getBoolean(R.styleable.LabelView_lv_text_bold, true)
this.mTextAllCaps = obtainStyledAttributes.getBoolean(R.styleable.LabelView_lv_text_all_caps, true)
this.mFillTriangle = obtainStyledAttributes.getBoolean(R.styleable.LabelView_lv_fill_triangle, false)
this.mBackgroundColor =
obtainStyledAttributes.getColor(R.styleable.LabelView_lv_background_color, Color.parseColor("#FF4081"))
this.mMinSize = obtainStyledAttributes.getDimension(
R.styleable.LabelView_lv_min_size,
if (this.mFillTriangle) dp2px(35.0f).toFloat() else dp2px(50.0f).toFloat()
)
this.mPadding = obtainStyledAttributes.getDimension(R.styleable.LabelView_lv_padding, dp2px(3.5f).toFloat())
this.mGravity = obtainStyledAttributes.getInt(R.styleable.LabelView_lv_gravity, 51)
obtainStyledAttributes.recycle()
}
fun setTextColor(i: Int) { this.mTextColor = i; invalidate() }
fun setText(str: String) { this.mTextContent = str; invalidate() }
fun setTextSize(f: Float) { this.mTextSize = sp2px(f).toFloat(); invalidate() }
fun setTextBold(z: Boolean) { this.mTextBold = z; invalidate() }
fun setFillTriangle(z: Boolean) { this.mFillTriangle = z; invalidate() }
fun setTextAllCaps(z: Boolean) { this.mTextAllCaps = z; invalidate() }
fun setBgColor(i: Int) { this.mBackgroundColor = i; invalidate() }
fun setMinSize(f: Float) { this.mMinSize = dp2px(f).toFloat(); invalidate() }
fun setPadding(f: Float) { this.mPadding = dp2px(f).toFloat(); invalidate() }
fun setGravity(i: Int) { this.mGravity = i }
fun getText(): String = this.mTextContent
fun getTextColor() = this.mTextColor
fun getTextSize() = this.mTextSize
fun isTextBold() = this.mTextBold
fun isFillTriangle() = this.mFillTriangle
fun isTextAllCaps() = this.mTextAllCaps
fun getBgColor() = this.mBackgroundColor
fun getMinSize() = this.mMinSize
fun getPadding() = this.mPadding
fun getGravity() = this.mGravity
public override fun onDraw(canvas: Canvas) {
val height = height
this.mTextPaint.color = this.mTextColor
this.mTextPaint.textSize = this.mTextSize
this.mTextPaint.isFakeBoldText = this.mTextBold
this.mBackgroundPaint.color = this.mBackgroundColor
val descent = this.mTextPaint.descent() - this.mTextPaint.ascent()
if (!this.mFillTriangle) {
val sqrt = (this.mPadding * 2.0f + descent).toDouble() * sqrt(2.0)
when {
this.mGravity == 51 -> {
this.mPath.reset()
this.mPath.moveTo(0.0f, (height.toDouble() - sqrt).toFloat())
this.mPath.lineTo(0.0f, height.toFloat())
this.mPath.lineTo(height.toFloat(), 0.0f)
this.mPath.lineTo((height.toDouble() - sqrt).toFloat(), 0.0f)
this.mPath.close()
canvas.drawPath(this.mPath, this.mBackgroundPaint)
drawText(height, -45.0f, canvas, descent, true)
}
this.mGravity == 53 -> {
this.mPath.reset()
this.mPath.moveTo(0.0f, 0.0f)
this.mPath.lineTo(sqrt.toFloat(), 0.0f)
this.mPath.lineTo(height.toFloat(), (height.toDouble() - sqrt).toFloat())
this.mPath.lineTo(height.toFloat(), height.toFloat())
this.mPath.close()
canvas.drawPath(this.mPath, this.mBackgroundPaint)
drawText(height, 45.0f, canvas, descent, true)
}
this.mGravity == 83 -> {
this.mPath.reset()
this.mPath.moveTo(0.0f, 0.0f)
this.mPath.lineTo(0.0f, sqrt.toFloat())
this.mPath.lineTo((height.toDouble() - sqrt).toFloat(), height.toFloat())
this.mPath.lineTo(height.toFloat(), height.toFloat())
this.mPath.close()
canvas.drawPath(this.mPath, this.mBackgroundPaint)
drawText(height, 45.0f, canvas, descent, false)
}
this.mGravity == 85 -> {
this.mPath.reset()
this.mPath.moveTo(0.0f, height.toFloat())
this.mPath.lineTo(sqrt.toFloat(), height.toFloat())
this.mPath.lineTo(height.toFloat(), sqrt.toFloat())
this.mPath.lineTo(height.toFloat(), 0.0f)
this.mPath.close()
canvas.drawPath(this.mPath, this.mBackgroundPaint)
drawText(height, -45.0f, canvas, descent, false)
}
}
} else if (this.mGravity == 51) {
this.mPath.reset()
this.mPath.moveTo(0.0f, 0.0f)
this.mPath.lineTo(0.0f, height.toFloat())
this.mPath.lineTo(height.toFloat(), 0.0f)
this.mPath.close()
canvas.drawPath(this.mPath, this.mBackgroundPaint)
drawTextWhenFill(height, -45.0f, canvas, true)
} else if (this.mGravity == 53) {
this.mPath.reset()
this.mPath.moveTo(height.toFloat(), 0.0f)
this.mPath.lineTo(0.0f, 0.0f)
this.mPath.lineTo(height.toFloat(), height.toFloat())
this.mPath.close()
canvas.drawPath(this.mPath, this.mBackgroundPaint)
drawTextWhenFill(height, 45.0f, canvas, true)
} else if (this.mGravity == 83) {
this.mPath.reset()
this.mPath.moveTo(0.0f, height.toFloat())
this.mPath.lineTo(0.0f, 0.0f)
this.mPath.lineTo(height.toFloat(), height.toFloat())
this.mPath.close()
canvas.drawPath(this.mPath, this.mBackgroundPaint)
drawTextWhenFill(height, 45.0f, canvas, false)
} else if (this.mGravity == 85) {
this.mPath.reset()
this.mPath.moveTo(height.toFloat(), height.toFloat())
this.mPath.lineTo(0.0f, height.toFloat())
this.mPath.lineTo(height.toFloat(), 0.0f)
this.mPath.close()
canvas.drawPath(this.mPath, this.mBackgroundPaint)
drawTextWhenFill(height, -45.0f, canvas, false)
}
}
private fun drawText(i: Int, f: Float, canvas: Canvas, f2: Float, z: Boolean) {
canvas.save()
canvas.rotate(f, i.toFloat() / 2.0f, i.toFloat() / 2.0f)
canvas.drawText(
if (this.mTextAllCaps) this.mTextContent.toUpperCase() else this.mTextContent,
(paddingLeft + (i - paddingLeft - paddingRight) / 2).toFloat(),
(i / 2).toFloat() - (this.mTextPaint.descent() + this.mTextPaint.ascent()) / 2.0f + if (z) -(this.mPadding * 2.0f + f2) / 2.0f else (this.mPadding * 2.0f + f2) / 2.0f,
this.mTextPaint
)
canvas.restore()
}
private fun drawTextWhenFill(i: Int, f: Float, canvas: Canvas, z: Boolean) {
canvas.save()
canvas.rotate(f, i.toFloat() / 2.0f, i.toFloat() / 2.0f)
canvas.drawText(
if (this.mTextAllCaps) this.mTextContent.toUpperCase() else this.mTextContent,
(paddingLeft + (i - paddingLeft - paddingRight) / 2).toFloat(),
(i / 2).toFloat() - (this.mTextPaint.descent() + this.mTextPaint.ascent()) / 2.0f + if (z) (-i / 4).toFloat() else (i / 4).toFloat(),
this.mTextPaint
)
canvas.restore()
}
/* access modifiers changed from: protected */
public override fun onMeasure(i: Int, i2: Int) {
val measureWidth = measureWidth(i)
setMeasuredDimension(measureWidth, measureWidth)
}
private fun measureWidth(i: Int): Int {
val mode = MeasureSpec.getMode(i)
val size = MeasureSpec.getSize(i)
if (mode == 1073741824) {
return size
}
val paddingLeft = paddingLeft + paddingRight
this.mTextPaint.color = this.mTextColor
this.mTextPaint.textSize = this.mTextSize
var measureText =
((paddingLeft + this.mTextPaint.measureText(this.mTextContent + "").toInt()).toDouble() * sqrt(2.0)).toInt()
if (mode == Integer.MIN_VALUE) {
measureText = min(measureText, size)
}
return max(this.mMinSize.toInt(), measureText)
}
private fun dp2px(f: Float) = (resources.displayMetrics.density * f + 0.5f).toInt()
private fun sp2px(f: Float) = (resources.displayMetrics.scaledDensity * f + 0.5f).toInt()
}
複製代碼
佈局直接添加這個控件:
<com.coderpig.kttest.LabelView
xmlns:lv="http://schemas.android.com/apk/res-auto"
android:layout_width="60dp"
android:layout_height="60dp"
android:paddingLeft="10dip"
android:paddingRight="10dip"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
lv:lv_text=" 文章 "
lv:lv_text_color="#ffffffff"
lv:lv_text_size="11.199982sp"
lv:lv_background_color="#ffdfdfdf"
lv:lv_gravity="TOP_RIGHT"
lv:lv_fill_triangle="false"/>
複製代碼
運行下看下效果(對比掘金生成的和我實現的效果):
行吧,就剩下最後生成信息卡片截圖的代碼咯,在上面的PreviewEntryPinCardFragment.java文件中並無找到底下這個分享欄。分享欄應該是寫到另外一個佈局文件中了,這裏用一個取巧的操做,直接全局搜保存按鈕的文件名:share_save,記得勾選xml類型,能夠更快定位到:
打開fragment_message_card.xml:
行吧,就是咱們想要的內容,全局搜:R.layout.fragment_message_card,勾選java文件:
直指 ActivityShareFragment.java,接着搜save,定位到點擊 ,
猜想這裏作了兩個操做:
- 一、調用BitmapUtils類的saveBitmap2file()方法保存截圖;
- 二、調用FileUtils類的notifyGallery()通知圖庫更新;
接着打開BitmapUtils類,定位到 saveBitmap2file() 方法:
這段代碼只是:把Bitmap保存爲JPEG圖片而已,而後,前面的Bitmap哪來的?對應參數r1:
經過CommonMessageCardFragment類的getBitmap()方法得到,打開類定位到getBitmap()方法:
抽象類和抽象方法???看下前面的PreviewEntryPinCardFragment是否是繼承了這個類:
果真,這裏把佈局視圖做爲參數傳入 ViewExKt.e() 方法,跟:
臥槽,水到渠成啊,圖片的生成過程一清二楚啊!接着把 圖片路徑生成規律 和 通知圖庫 的部分也摳出來把。
SD.getGalleryDir():得到圖庫路徑:
MD5Util.encrypt():MD5加密下連接:
最後FileUtils.notifyGallery():發送廣播通知圖庫更新。
嘖嘖嘖,材料齊全,開始組裝偷來的代碼,整合後的代碼以下:
工具類:Utils.kt
package com.coderpig.kttest
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Environment
import java.io.FileOutputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
// 把Bitmap保存爲圖片
fun saveBitmap2file(bitmap: Bitmap, str: String): Boolean {
val compressFormat = Bitmap.CompressFormat.JPEG
return try {
val fileOutputStream = FileOutputStream(str)
val compress = bitmap.compress(compressFormat, 100, fileOutputStream)
fileOutputStream.close()
compress
} catch (e: Exception) {
e.printStackTrace()
false
}
}
// 得到圖庫路徑
fun getGalleryDir(): String {
val externalStoragePublicDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
try {
externalStoragePublicDirectory.mkdirs()
} catch (e: Exception) {
}
return externalStoragePublicDirectory.absolutePath
}
// 得到MD5字符串
fun encrypt(str: String?): String {
var str = str
val str2 = ""
if (str == null) {
str = ""
}
try {
val instance = MessageDigest.getInstance("MD5")
instance.update(str.toByteArray())
val digest = instance.digest()
val stringBuffer = StringBuffer("")
for (i in digest.indices) {
var b = digest[i]
if (b < 0) {
b = (b + 256).toByte()
}
if (b < 16) {
stringBuffer.append("0")
}
stringBuffer.append(Integer.toHexString(b.toInt()))
}
return stringBuffer.toString()
} catch (e: NoSuchAlgorithmException) {
return str2
}
}
// 廣播通知圖庫更新
fun notifyGallery(context: Context, str: String) {
context.sendBroadcast(Intent("android.intent.action.MEDIA_SCANNER_SCAN_FILE", Uri.parse("file://$str")))
}
複製代碼
頁面MainActivity.java 新增:
iv_share_save.setOnClickListener {
val sb = StringBuilder()
sb.apply { append(getGalleryDir()).append("/").append(Test.encrypt(entryUrl)).append(".jpg") }
val picPath = sb.toString().replace("ffffff", "")
saveBitmap2file(createViewBitmap(cly_content), picPath)
notifyGallery(this, picPath)
Toast.makeText(this, "已經保存到:$picPath" +"", Toast.LENGTH_SHORT).show()
}
private fun createViewBitmap(view: View): Bitmap {
val createBitmap = Bitmap.createBitmap(view.width, view.height, Config.RGB_565)
view.draw(Canvas(createBitmap))
return createBitmap
}
複製代碼
最後的運行效果圖以下:
這裏有個小坑我糾結了許久,就是生成的md5字符串一直和掘金的不同,後來發現加密的字符串不是文章的連接,而是:juejin.im/entry/xxx 哈哈。行吧,到此,掘金消息卡片的代碼總算收入囊中,完整的代碼,公號回覆003取需。
偷代碼只是開開玩笑,畢竟是別人的勞動結晶!尊重他人勞動成果,限於咱們本身的閱歷,或者沒有大神帶,有些功能以本身當前水平沒辦法寫出來, 此時借鑑別人的代碼,也不失爲一個好的方法。並且研究別人寫的代碼挺有趣的,一層一層刨開,揣摩做者的意圖,用到了什麼技巧,怎麼用到本身的項目中,等等,耗時,但獲益良多。最後說一句,僅用於技術研究學習之用,請勿用於商業用途!破壞計算機信息系統罪瞭解下?很是鄙視那種二次打包別人APP,而後塞廣告或者病毒的人。
(PS:公號回覆00x返回對應資源,沒別的意思,只是方便本身和羣友,有些人看了個人文章,反手就問:那個東西去哪裏下?資源失效了?有那個XX嗎?等等這些問題,而我又要去打開文章,而後想一想資源在哪,從新傳一下,而後又回頭把幾個平臺的文章改一下,好煩咯!So,丟公號去了!別吐槽我啊,關鍵字和官網啥的我都有給,能夠本身百度!)
行吧,就說這麼多,若是紕漏或建議,歡迎在評論區指出,謝謝~
參考文獻:
若是本文對你有所幫助,歡迎
留言,點贊,轉發
素質三連,謝謝😘~