大家好,今天小编来为大家解答以下的问题,关于深入解析微信插件安装技巧:Xposed系列教程篇(二),这个很多人还不知道,现在让我们一起来看看吧!
图片旧版本实现效果
准备工作
Root相关
TWRP Recovery:强大的第三方恢复,具有官方Recovery无法实现的功能。安装Magisk Manager需要使用Magisk Manager:一款功能强大的root工具,可以替代SuperSU进行root权限管理。如果您使用VirtualXposed或Taichi或其他虚拟环境Xposed,则不需要Root。如果您使用Xposed安装程序,则需要Root权限。
关于这三者的信息请参见上一篇文章:Xposed系列Demo入门指南及源码分析(一)
反编译相关
apktool:主要用于反编译APK查看资源文件dex2jar:反编译APK获取Java源代码Jar包jd-gui:一般与dex2jar配合使用,将反编译得到的jar包拖入gui中,方便查看源代码jadx :功能强大的反编译工具,可以直接查看Java代码和资源文件。它还支持查看Smali。推荐使用jadx工具反编译。例如,从微信官网下载的32位版本的微信,会默认下载上面的weixin7016android1700_arm64.apk。
如果下载的是arm64版本,有些x86架构的模拟器不支持微信,无法正常使用,所以32位版本主要是为了方便在模拟器上调试。
图像
然后将下载的weixin7016android1700.apk拖入JadxGUI中。反编译结果如下:
图像
并且我们还可以将其保存为Gradle项目并在Android Studio或IDEA中查看。操作入口如下:
image
Hook分析相关
adb命令
adb 命令请参考https://github.com/xbdcc/CCommand。这里简单介绍一下这里需要用到的东西:
adb shell dumpsys Activity top Activity_top.txt 当然,如果你已经熟悉adb命令,想要快速输入,也可以在环境变量中设置alisa。
在控制台输入vim ~/.bash_profile,添加一行将adb shell dumpsys Activity top Activity_top.txt的别名设置为activity_top,如下:alias Activity_top="adb shell dumpsys Activity top Activity_top.txt" 然后按esc键输入:wq返回退出并保存,然后输入source ~/.bash_profile使环境变量生效。最后直接在控制台输入activity_top,回车即可。效果和刚才一长串命令是一样的。该命令可以输出当前Activity的信息,例如该命令捕获的变更页面Activity。信息如下:
TASK com.tencent.mm id=184
活动com.tencent.mm/.plugin.wallet.balance.ui.WalletBalanceManagerUI 1fcb6708 pid=1326
本地活动27bbdf64 State:
mResumed=true mStopped=false mFinished=false
mLoadersStarted=true
mChangingConfigurations=false
mCurrentConfig={1.0 ?mcc?mnc zh_CN ?layoutDir sw360dp w360dp h622dp 480dpi nrml 长端口手指-keyb/v/h -nav/h s.5mThemeChanged=0mThemeChangedFlags=0mFlipFont=0}
247455f2: 中的活动片段
#0: ReportFragment{22e48443 #0 android.arch.lifecycle.LifecycleDispatcher.report_fragment_tag}
mFragmentId=#0 mContainerId=#0 mTag=android.arch.lifecycle.LifecycleDispatcher.report_fragment_tag
mState=5 mIndex=0 mWho=android:fragment:0 mBackStackNesting=0
mAdded=true mRemoving=false mResumed=true mFromLayout=false mInLayout=false
mHidden=false mDetached=false mMenuVisible=true mHasMenu=false
mRetainInstance=false mRetaining=false mUserVisibleHint=true
mFragmentManager=FragmentManager{WalletBalanceManagerUI 中的247455f2{27bbdf64}}
mActivity=com.tencent.mm.plugin.wallet.balance.ui.WalletBalanceManagerUI@27bbdf64
子FragmentManager{ReportFragment 中的a6aeac0{22e48443}}:
FragmentManager 杂项state:
mActivity=com.tencent.mm.plugin.wallet.balance.ui.WalletBalanceManagerUI@27bbdf64
mContainer=android.app.Fragment$1@138ca3f9
mParent=ReportFragment{22e48443 #0 android.arch.lifecycle.LifecycleDispatcher.report_fragment_tag}
mCurState=5 mStateSaved=false mDestroyed=false
添加了片段:
#0: ReportFragment{22e48443 #0 android.arch.lifecycle.LifecycleDispatcher.report_fragment_tag}
FragmentManager 杂项state:
mActivity=com.tencent.mm.plugin.wallet.balance.ui.WalletBalanceManagerUI@27bbdf64
mContainer=android.app.Activity$1@386ddf3e
mCurState=5 mStateSaved=false mDestroyed=false
查看Root:
mAdded=true mRemoved=false
mConsumeBatchedInputScheduled=false
mConsumeBatchedInputImmediatelyScheduled=false
mPendingInputEventCount=0
mProcessInputEventsScheduled=false
mTraversalScheduled=false
android.view.ViewRootImpl$NativePreImeInputStage: mQueueLength=0
android.view.ViewRootImpl$ImeInputStage: mQueueLength=0
android.view.ViewRootImpl$NativePostImeInputStage: mQueueLength=0
编舞:
mFrameScheduled=false
mLastFrameTime=16770070(150010 毫秒前)
查看层次结构:
com.android.internal.policy.impl.PhoneWindow$DecorView{162d5b8c V.ED. R.. 0,0-1080,1920}
com.tencent.mm.ui.widget.SwipeBackLayout{15900669 VFE. 0,0-1080,1920 #7f09245b app:id/g2s}
com.tencent.mm.ui.statusbar.b{303be18f V.ED. 0,0-1080,1920}
android.widget.LinearLayout{1d1aa478 V.E. 0,54-1080,1920}
android.view.ViewStub{2f44c751 G.E..I. 0,0-0,0 #1020373}
android.widget.FrameLayout{ea9bbb6 V.E. 0,0-1080,1866}
android.support.v7.widget.ActionBarOverlayLayout{1e0ddd42 V.E. 0,0-1080,1866 #7f090a81 app:id/b8w}
android.support.v7.widget.ContentFrameLayout{6698a90 V.E. 0,130-1080,1866 #1020002 android:id/content}
com.tencent.mm.ui.LayoutListenerView{941b6fc V.E.0,0-1080,1736 #7f0917b0 app:id/dp5}
android.widget.ScrollView{cc9bba6 VFED.. 0,0-1080,1736}
android.widget.RelativeLayout{3887e994 V.E. 0,0-1080,1736}
android.widget.TextView{3781bc3d G.ED..I. 0,0-0,0 #7f09289c app:id/guw}
android.widget.LinearLayout{15c40b00 V.E. 0,0-1080,1736}
android.widget.ImageView{1d2df39 V.ED. 453,130-626,303 #7f09034d app:id/w5}
android.widget.TextView{25e2997e V.ED. 448,389-632,451 #7f09259c app:id/ga5}
android.widget.LinearLayout{21b3eedf V.E.0,451-1080,721}
android.widget.RelativeLayout{bded72c V.E. 0,43-1080,157}
com.tencent.mm.plugin.wallet_core.ui.view.WcPayMoneyLoadingView{b7935f5 V.E. 375,0-705,114 #7f0928aa app:id/gv_}
com.robinhood.ticker.TickerView{21299fb V.ED. 0,0-330,114 #7f091794 app:id/dod}
android.widget.ProgressBar{1ff83a18 G.ED..I. 0,0-0,0 #7f092905 app:id/gxq}
android.widget.LinearLayout{36ce5a56 G.E..I. 0,0-0,0 #7f09033f app:id/vr}
android.widget.TextView{29e6dad7 V.ED..ID 0,0-0,0 #7f090340 app:id/vs}
android.widget.ImageView{742dfc4 V.ED..ID 0,0-0,0 #7f09033e app:id/vq}
android.widget.LinearLayout{d3f2ead G.E..I. 0,0-0,0 #7f09034b app:id/w3}
com.tencent.mm.pluginsdk.ui.applet.CdnImageView{28544d73 V.ED..I. 0,0-0,0 #7f09034a app:id/w2}
com.tencent.mm.wallet_core.ui.WalletTextView{35703430 V.ED..ID 0,0-0,0 #7f090349 app:id/w1}
com.tencent.mm.pluginsdk.ui.applet.CdnImageView{3dcd08a9 V.ED.I. 0,0-0,0 #7f090348 app:id/w0}
android.widget.Space{111c4dcf I.ED..I. 0,721-1080,1101}
android.widget.LinearLayout{25dc635c V.E. 0,1187-1080,1295}
android.widget.Button{3a448665 VFED.C. 291,0-788,108 #7f0919c4 app:id/e3i}
android.widget.Button{b7a5948 GFED.C.I. 0,0-0,0 #7f09289d app:id/gux}
android.widget.LinearLayout{363e27c7 G.E..I. 0,0-0,0 #7f09141b app:id/d1c}
android.widget.TextView{137fc1f4 V.ED..ID 0,0-0,0 #7f09141c app:id/d1d}
android.widget.ImageView{31361d1d G.ED..I。 0,0-0,0 #7f09153c app:id/d96}
android.widget.LinearLayout{12576b92 V.E. 464,1554-616,1606}
android.widget.TextView{36eb7963 V.ED.C. 0,0-152,52 #7f09289f app:id/guz}
android.view.View{181c6e19 G.ED.I. 0,0-0,0 #7f09289e app:id/guy}
android.widget.TextView{2dfcaede G.ED.I. 0,0-0,0 #7f09289b app:id/guv}
android.widget.TextView{2f9248bf V. 0,1628-1080,1671 #7f092951 app:id/gzs}
android.widget.Button{39a3e185 GFED.C.ID 0,0-0,0 #7f0917c6 app:id/dpq}
android.support.v7.widget.ActionBarContainer{1e880f89 V.ED. 0,0-1080,130 #7f090059 app:id/bp}
android.support.v7.widget.Toolbar{1a08b8e V.E. 0,0-1080,130 #7f090057 app:id/bn}
android.widget.LinearLayout{88b4a54 V.E. 0,0-810,130 #7f09005b app:id/br}
android.widget.LinearLayout{24ec6dfd V.E.C. 0,0-108,130 #7f0900a0 app:id/dm}
com.tencent.mm.ui.widget.imageview.WeImageView{10dc1af2 V.ED. 22,0-86,130 #7f0900a1 app:id/dn}
android.widget.LinearLayout{1e96ecf9 G.E..I. 0,0-0,0 #7f090098 app:id/de}
com.tencent.mm.ui.widget.AlbumChooserView{40cf43e V.E.C.I. 0,0-0,0 #7f09008a app:id/d1}
android.widget.RelativeLayout{13558fec V.E..I. 0,0-0,0}
android.widget.TextView{9d05fb5 V.ED..ID 0,0-0,0 #7f090120 app:id/h3}
android.widget.FrameLayout{25e0164a V.E..I. 0,0-0,0}
com.tencent.mm.ui.widget.imageview.WeImageView{2e7039bb V.ED..ID 0,0-0,0 #7f09011f app:id/h2}
android.widget.LinearLayout{1901bed8 V.E..I. 108,0-108,130 #7f0925d1 app:id/gbk}
android.widget.LinearLayout{20f44231 V.E..ID 0,0-0,130}
android.widget.ImageView{17114d16 G.ED..I。 0,0-0,0 #7f0925d0 app:id/gbj}
android.widget.TextView{cdeb697 V.ED..ID 0,34-0,96 #1020014 android:id/text1}
android.widget.ProgressBar{263ff084 G.ED..I。 0,0-0,0 #7f091c5e app:id/eki}
android.widget.TextView{364d24a2 G.ED..I。 0,0-0,0 #1020015 android:id/text2}
android.support.v7.widget.ActionMenuView{c9308ac V.E..... ........ 810,0-1080,130} android.widget.LinearLayout{25f3ae17 V.E..... ........ 0,0-270,130} android.widget.ImageButton{24965204 GFED..C. ......I. 0,0-0,0 #7f09007c app:id/cn} android.widget.TextView{141653ed V.ED..CL ........ 0,0-270,130 #7f090079 app:id/ck} android.widget.LinearLayout{357b14b3 G.E..... ......I. 0,0-0,0 #7f090158 app:id/il} android.widget.ImageView{3b288a70 V.ED.... ......ID 0,0-0,0} android.widget.Button{d1141e9 GFED..CL ......I. 0,0-0,0 #7f090076 app:id/ch} android.widget.RelativeLayout{1908490f V.E..... ......ID 270,65-270,65} com.tencent.mm.ui.widget.imageview.WeImageView{3f63dd9c G.ED.... ......I. 0,0-0,0 #7f090078 app:id/cj} android.widget.ImageView{2db313a5 G.ED.... ......I. 0,0-0,0 #7f090b6f app:id/beb} android.support.v7.widget.ActionBarContextView{18591b45 G.E..... ......I. 0,0-0,0 #7f090065 app:id/c1} Looper (main, tid 1) {1f1b79df} Message 0: { when=+6m42s348ms what=26 target=com.tencent.mm.sdk.platformtools.ao$2 } Message 1: { when=+26m42s267ms what=23 target=com.tencent.mm.sdk.platformtools.ao$2 } (Total messages: 2, idling=false, quitting=false) Local FragmentActivity 27bbdf64 State: mCreated=true mResumed=true mStopped=false FragmentManager misc state: mHost=android.support.v4.app.FragmentActivity$a@1273f2ec mContainer=android.support.v4.app.FragmentActivity$a@1273f2ec mCurState=4 mStateSaved=false mStopped=false mDestroyed=falsemonitor
image可以查看布局元素和trace信息,分析布局可以查看之前文章Android通过辅助功能实现抢微信红包原理简单介绍 image,分析trace方法调用栈,例如下: imageimage,生成生成html格式的trace,可以分析卡顿丢帧等问题,如果有打Trace也可以在上面看出来。可以在Chrome里打开查看,如下: image再介绍三种方便查找id值的方法
假如我们要查看id为dod的值:通过activity_top查看
在里面找到元素对应的值,为十六进制值。如上面结果中有这样一行,可以看到id为"dod"的值为十六进制值7f091794 com.robinhood.ticker.TickerView{21299fb V.ED.... ........ 0,0-330,114 #7f091794 app:id/dod}通过apk查看
可以把apk拖进AS中,在resources.arsc下选择你要找的id,得到的值为十六进制 image通过jadx查看
双击resources.arsc,可以在里面搜索id,得到的值为十进制。如id为dod的值为十进制值2131302292 imageHook分析
通过activity_top得知: 支付页面:com.tencent.mm/.plugin.mall.ui.MallIndexUI钱包页面:com.tencent.mm/.plugin.mall.ui.MallWalletUI零钱页面:com.tencent.mm/.plugin.wallet.balance.ui.WalletBalanceManagerUI分析布局
首先打开支付页面我们通过monitor的Dump View Hierarchy,得知显示钱包的金额的控件id为dod 支付页面View视图然后我们通过activity_top找到这个id的地方知道它其实是com.robinhood.ticker.TickerView这个控件, 嗯这个一看就是不是腾讯的自定义View而是用的第三方库,Github上一搜,可以知道它用的是ticker这个库, 后面可以看到钱包页面和零钱页面显示金额的也是用的这个控件。 知道了这个库我们可以看下TickerView这个类的代码,这里再推荐一个Chrome插件octotree比较方便在GitHub网页上切换文件, 如下,可以看到这里有个setText方法,里面执行了columnManager.setText(targetText);代码,而columnManager最后执行了columnManager.draw(canvas, textPaint);把文字绘制到Canvas上了, setContentDescription(text);设置了contentDescription的值,所以这就是我们能看到描述和显示金额的值一样,但是它的text属性值却为空的原因了。 image分析支付页面MallIndexUI
首先我们观察到每次进入支付页面它的金额旁边是有个loading的,并且从钱包页面返回到支付页面也都会loading一下,那么可以猜想它可能是在onResume里面做了什么操作 (其实直接看它代码一下就能看出来,假设我们还没看代码先简单猜下)。那么我们看下它的onResume方法调用栈如下: image看到了吗?里面主要就执行了MallIndexBaseUI(MallIndexUI的父类)的onResume和自己的dbb方法,这个时候如果你不想看源码继续分析的话其实就已经可以尝试Hook跑起来看看效果了, 本着保险起见我们还是先继续看看它的源码 public final void dbb() { AppMethodBeat.i(66131); ac.i("MicorMsg.MallIndexUI", "updateBalanceNum"); ak akVar = new ak(); if (akVar.erV()) { this.uCo.setText((String) g.agR().agA().get(ah.a.USERINFO_WALLET_RELEAY_NAME_BALANCE_CONTENT_STRING_SYNC, (Object) getString(R.string.eex))); this.uCo.setVisibility(0); this.uDf.setVisibility(8); this.uDg.setVisibility(8); AppMethodBeat.o(66131); return; } if (akVar.erX()) { ac.i("MicorMsg.MallIndexUI", "show balance amount"); long longValue = ((Long) ((com.tencent.mm.plugin.wxpay.a.a) g.ad(com.tencent.mm.plugin.wxpay.a.a.class)).getWalletCacheStg().get(ah.a.USERINFO_NEW_BALANCE_LONG_SYNC, (Object) 0L)).longValue(); if (this.uDf != null) { nQ(akVar.erZ()); if (this.uDf.getVisibility() == 0) { this.uDf.setMoney(com.tencent.mm.wallet_core.ui.e.C(com.tencent.mm.wallet_core.ui.e.a(String.valueOf(longValue), "100", 2, RoundingMode.HALF_UP).doubleValue())); AppMethodBeat.o(66131); return; } } else { ac.w("MicorMsg.MallIndexUI", "moneyLoadingView is null"); } } AppMethodBeat.o(66131); }可以看出ac.i应该就是打印的log方法,根据它的日志updateBalanceNum,可以知道这个方法主要是更新余额的,那么我们是不是可以手动拦截这个方法替换为自己设置的呢?我们来试试, 拿到方法的对象转为Activity,然后通过findViewById找到显示金额的控件,通过反射拿到setText并且调用赋值,或者通过XposedHelpers.callMethod(view, "setText", money) private fun hookPayPage() { XposedHelpers.findAndHookMethod("com.tencent.mm.plugin.mall.ui.MallIndexUI", classLoader, "dbb", object : XC_MethodReplacement() { override fun replaceHookedMethod(param: MethodHookParam?): Any { param?.let { val activity = param.thisObject as Activity var view = activity.findViewById(0x7f091794) //id:dod的id值为0x7f091794分析钱包页面WalletBalanceManagerUI
前面分析支付页面知道WcPayMoneyLoadingView中有setFirstMoney和setNewMoney,其实我们就可以直接Hook替换这两个方法, 而且该方法通用三个页面都有效,如下: private fun hookMoney() { val hookClass = classLoader.loadClass(wechatMoneyLoadingView) ?: return XposedHelpers.findAndHookMethod(hookClass, "setFirstMoney", String::class.java, replaceStr) XposedHelpers.findAndHookMethod(hookClass, "setNewMoney", String::class.java, replaceStr) } private val replaceStr = object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam?) { param?.let { val view = param.thisObject as View when(view.context.javaClass.name) { wechatWalletActivity ->param.args[0] = "¥$money" wechatPayActivity, wechatChangeActivity ->param.args[0] = money } } } }分析零钱页面WalletBalanceManagerUI
首先我们看下零钱页面WalletBalanceManagerUI里面声明的对象,再根据View Hierarchy我们知道AZo是在TickerView外面的WcPayMoneyLoadingView, 继续看this.AZo.cc,可以发现它调用的之前支付页面分析的WcPayMoneyLoadingView中的tk方法,所以我们可以直接Hook这个方法,如下: private fun hookChangePage() { XposedHelpers.findAndHookMethod("com.tencent.mm.plugin.wallet.balance.ui.WalletBalanceManagerUI", classLoader, "tk", Boolean::class.java, object : XC_MethodReplacement() { override fun replaceHookedMethod(param: MethodHookParam?): Any { param?.let { val activity = param.thisObject as Activity var view = activity.findViewById(0x7f091794) //id:dod的id值为0x7f091794 XposedHelpers.callMethod(view, "setText", money) xlog("Hook method tk of WalletBalanceManagerUI class and set money.") } return "" } }) }结语
其实分析的时候可能有点复杂,并且要善于用多种工具一起分析,但是最后实现的代码很简单,整理后代码如下: class WechatHook : IXposedHookLoadPackage { private val packageName = "com.tencent.mm" private lateinit var classLoader: ClassLoader private val wechatPayActivity = "com.tencent.mm.plugin.mall.ui.MallIndexUI" private val wechatWalletActivity = "com.tencent.mm.plugin.mall.ui.MallWalletUI" private val wechatChangeActivity = "com.tencent.mm.plugin.wallet.balance.ui.WalletBalanceManagerUI" private val wechatMoneyLoadingView = "com.tencent.mm.plugin.wallet_core.ui.view.WcPayMoneyLoadingView" private var money = "100000000.00" override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { if (packageName == lpparam.packageName) { xlog("Load Wechat app.") classLoader = lpparam.classLoader hookMoney() // hookPayPage() // hookPayPage2() // // hookChangePage() } } /** * 改变自定义的文本控件设置文本方法,终极boss,三个都可以 */ private fun hookMoney() { val hookClass = classLoader.loadClass(wechatMoneyLoadingView) ?: return XposedHelpers.findAndHookMethod(hookClass, "setFirstMoney", String::class.java, replaceStr) XposedHelpers.findAndHookMethod(hookClass, "setNewMoney", String::class.java, replaceStr) } /** * 支付页面改变文本 */ private fun hookPayPage() = XposedHelpers.findAndHookMethod( wechatPayActivity, classLoader, "dbb", replaceViewText ) /** * 支付页面改变文本另一种方法 */ private fun hookPayPage2() { XposedHelpers.findAndHookMethod(wechatMoneyLoadingView, classLoader, "cc", String::class.java, Boolean::class.java, object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam?) { param?.let { val view = param.thisObject as View param.args[0] = money } } }) } /** * 零钱页面改变文本 */ private fun hookChangePage() = XposedHelpers.findAndHookMethod( wechatChangeActivity, classLoader, "tk", Boolean::class.java, replaceViewText ) /** * 在方法调用前手动修改值来改变最后显示的金额 */ private val replaceStr = object : XC_MethodHook() { override fun beforeHookedMethod(param: MethodHookParam?) { param?.let { xlog("") val view = param.thisObject as View when (view.context.javaClass.name) { wechatWalletActivity ->param.args[0] = "¥$money" wechatPayActivity, wechatChangeActivity ->param.args[0] = money } } } } /** * 通过找到显示金额的控件反射拿到它的赋值方法并调用 */ private val replaceViewText = object : XC_MethodReplacement() { override fun replaceHookedMethod(param: MethodHookParam?): Any { param?.let { val activity = param.thisObject as Activity var view = activity.findViewById(0x7f091794) //id:dod的id值为0x7f091794 XposedHelpers.callMethod(view, "setText", money) xlog("find view and set text.") } return "" } }END,本文到此结束,如果可以帮助到大家,还望关注本站哦!
【深入解析微信插件安装技巧:Xposed系列教程篇(二)】相关文章:
用户评论
好耶!期待第二期揭秘。
有5位网友表示赞同!
看标题就感觉很有趣,我要试试这个“装X”功能!
有7位网友表示赞同!
我好久没使用过Xposed框架了,这篇指南能帮我回顾一下吗?
有15位网友表示赞同!
微信最近更新了哪些新功能啊?这篇文章会不会教我怎么利用这些新功能来装X?
有11位网友表示赞同!
之前用过Xposed框架弄一些小功能,感觉很厉害!
有14位网友表示赞同!
微信的官方功能太少了吧,这东西可以让我体验一下不同的玩法?
有7位网友表示赞同!
这篇文章会不会包含哪些“装X”技巧?比如伪装自己的在线状态?
有7位网友表示赞同!
我比较好奇使用Xposed框架会导致什么安全性问题?
有15位网友表示赞同!
现在很多小软件都提供类似的功能,Xposed框架还值得一用吗?
有7位网友表示赞同!
这篇文章会不会教我怎么安装和配置Xposed框架?
有12位网友表示赞同!
感觉这个“装X”指南很有趣,分享一下你体验后,哪些功能最实用吧!
有11位网友表示赞同!
想问问,使用Xposed框架会不会影响微信的稳定性呢?
有16位网友表示赞同!
如果这个方法风险比较高,会不会有更好的替代方案?
有9位网友表示赞同!
学习一个新的技能,感觉有点刺激!要跟着这篇指南一起玩玩~
有6位网友表示赞同!
有没有什么安全小技巧可以让我们使用Xposed框架时更加安全?
有16位网友表示赞同!
这篇文章写得怎么样?有什么值得推荐的吗?
有8位网友表示赞同!
听起来很有趣,我决定去试试这个“装X”指南!
有6位网友表示赞同!
好奇一下,哪些微信用户比较需要这种“装X”功能?
有17位网友表示赞同!
分享一下你使用Xposed框架装X之后的故事吧,一定会很精彩!
有9位网友表示赞同!
最近想尝试一些新的手机使用方法,这篇指南正好赶上了~
有10位网友表示赞同!