Xposed去除微信朋友圈广告
本帖最后由 ColoThor 于 2021-3-26 17:40 编辑## 一、背景:最近发现微信朋友圈的广告有点多,有时前十条就出现2条广告,还是针对性投放的,于是想着怎么去除广告。
## 本次研究用的是微信谷歌商店版,版本名是7.0.0,版本号是1363,[云盘地址](https://www.lanzouj.com/i667lej),反编译工具是MT管理器
## 二、首先,广告和说说,明显的区别就是条目右上角有个广告标识,还可以进行点击,这就好办了。
!(https://tccos.myworld.fun/upload/87395009bc3e492b877e3af29cedacd2.png)
### 1、用android sdk里的 monitor 工具进行 dump view hierarchy 分析,发现朋友圈的列表用的是ListView,条目中广告标识的 layout 是 LinearLayout ,resource id 是 eb9
### 2、接着就是MT管理器发挥作用的时候了,对微信的安装包里的 resources.arsc 进行 id 过滤,找到 eb9 对应的16进制id 是 7F111B0F。
### 接下来用MT管理器的 Dex编辑器++ 选择所有的dex文件,搜索 7F111B0F 的16进制整数,结果里 7F111B0F 对应的名称都是 ad_info_ll。
### 于是再对 ad_info_ll 进行代码搜索,发现搜索结果多了一条,在 com.tencent.mm.plugin.sns.ui.bb 里被使用,打开该类并反编译成java代码
```
public bb(View view) {
this.view = view;
ab.i("MicroMsg.TimeLineAdView", "adView init lan " + this.mRP);
this.qGB = (TextView) this.view.findViewById(f.ad_info_tv);
this.qGC = (TextView) this.view.findViewById(f.ad_link_tv);
this.qGD = this.view.findViewById(f.ad_info_tv_arrow);
this.qGE = this.view.findViewById(f.ad_lbs_icon_tv);
this.qGA = (LinearLayout) this.view.findViewById(f.ad_info_ll);
this.qGB.setText(" " + this.view.getResources().getString(j.sns_ad_tip) + " ");
}
```
### 构造函数里对相关 view 进行查找,并且该类还有一个 setVisibility 方法,广告标识要显示,传入的就应该是View.VISIBLE,对应的 int 的值为 0
### 3、编写Xposed模块对 setVisibility 方法进行hook,打印 Log.getStackTraceString(new Throwable()) 的结果
### 发现 com.tencent.mm.plugin.sns.ui.item.BaseTimeLineItem 的 方法 a(BaseViewHolder baseViewHolder, int i, n nVar, TimeLineObject timeLineObject, int i2, au auVar) 有相关代码
```
av avVar = (av) auVar.cky().cim().get(Integer.valueOf(i));
...
...
if (avVar.qkM) {
baseViewHolder.pJU.setVisibility(8);
} else {
ab.i("MicroMsg.BaseTimeLineItem", "adatag " + avVar.qDU);
baseViewHolder.pJU.x(Long.valueOf(avVar.qDK), new com.tencent.mm.plugin.sns.data.b(baseViewHolder.pJU, baseViewHolder.position, avVar.qml, avVar.qDK, avVar.qDQ));
baseViewHolder.pJU.a(avVar.qDT, avVar.qDS);
baseViewHolder.pJU.setVisibility(0);
if (baseViewHolder.qzu != null && baseViewHolder.qzu.getVisibility() == 0) {
if (baseViewHolder.pJU.qGC.getVisibility() != 0) {
obj = 1;
} else {
obj = null;
}
if (obj != null) {
LayoutParams layoutParams = (LayoutParams) baseViewHolder.qzu.getLayoutParams();
layoutParams.setMargins(layoutParams.leftMargin, BackwardSupportUtil.b.b(this.mActivity, 0.0f), layoutParams.rightMargin, layoutParams.bottomMargin);
baseViewHolder.qzu.setLayoutParams(layoutParams);
}
}
}
```
### 上面的第一行代码对方法的最后一个参数进行了一系列调用,返回 com.tencent.mm.plugin.sns.ui.av 类的对象。
### 接着对 av 对象的成员变量 qkM 进行判断,这里 baseViewHolder.pJU 就是 com.tencent.mm.plugin.sns.ui.bb 的对象,对判断的结果分别传 0 和 8 进行显示和隐藏。
### 于是我们判断一个 av 实例对应一个朋友圈条目,而 qkM 变量可以判断当前条目是否是广告
### 4、对代码 av avVar = (av) auVar.cky().cim().get(Integer.valueOf(i)) 进行分析,找到 auVar 应对的类 com.tencent.mm.plugin.sns.ui.au 并进行分析, 发现 cky 方法返回的是成员变量 qBs,
```
public final w cky() {
return this.qBs;
}
```
### 而该变量又是在该类的构造方法进行赋值的,对构造方法进行hook,打印调用栈。发现在类 com.tencent.mm.plugin.sns.ui.a.a 的构造方法
```
public a(MMActivity mMActivity, ListView listView, com.tencent.mm.plugin.sns.ui.d.b bVar, i iVar, String str, b bVar2) {
this.qHT = new au(mMActivity, listView, bVar, iVar, this);
this.qHT.qta = true;
if (bVar2 == null) {
bVar2 = new c();
}
this.qHU = bVar2;
this.qHU.a(mMActivity, this.qHT, str);
b bVar3 = this.qHU;
com.tencent.mm.vending.f.a.i("Vending.ForwardVending", "Vending.setRangeSize(%s)", new Object[]{Integer.valueOf(10)});
bVar3.a = 10;
this.qHU.addVendingDataChangedCallback(this.qHW);
}
```
### 中进行实例化,传入的参数值是 this,即该类变量本身,找到代码 av avVar = (av) auVar.cky().cim().get(Integer.valueOf(i)) 中 cim 方法返回的是成员变量 qHU
```
public final Vending cim() {
return this.qHU;
}
```
### 该变量在上面的构造函数中被赋值,不是参数中类 com.tencent.mm.plugin.sns.ui.a.b.b 的实例 bVar2,就是实例化一个 com.tencent.mm.plugin.sns.ui.a.b.c。
### 而类 com.tencent.mm.plugin.sns.ui.a.a 继承于 BaseAdapter 。
### 上面说过,朋友圈的列表展示用的是ListView,而 BaseAdapter 就是 ListView 的适配器,用于展现条目,而BaseAdapter 的 getView 返回的就是一个条目的视图。
### 分析该方法在 com.tencent.mm.plugin.sns.ui.a.a 中的实现,参数 i 表示列表中的位置,从0开始。其中成员变量 qHT 是 上面分析过的类com.tencent.mm.plugin.sns.ui.au 的一个实例,调用 h 方法返回条目的视图
```
public final View getView(int i, View view, ViewGroup viewGroup) {
return this.qHT.h(i, view);
}
```
### 这下有点思路了,根据类 au 的成员变量 qkM 可以分析是否是广告,而 au 可从代码 av avVar = (av) auVar.cky().cim().get(Integer.valueOf(i)) 中获得,在调用链最后的 get 方法中保存广告条目,在 getView 中判断是否是广告,是广告就返回空视图,不是就继续调用 this.qHT.h(i, view),不就可以去除广告了吗。
### 回到正题,我们分析到了调用链的 cim 方法,方法返回的是成员变量 qHU,找到 get 方法需要找到对应的类。而qHU在上面的构造函数中赋值,不是最后一个参数就是实例化一个c。
### 分析发现,类com.tencent.mm.plugin.sns.ui.a.b.c 继承于 com.tencent.mm.plugin.sns.ui.a.a,而com.tencent.mm.plugin.sns.ui.a.a 实现接口 com.tencent.mm.plugin.sns.ui.a.b.b,在这些类中都未发现 get 方法,继续寻找继承关系,在 com.tencent.mm.vending.base.b 中发现了 get 方法
```
public final <T> T get(int i) {
if (this.c != 0) {
return super.get(Integer.valueOf(i));
}
a.e("Vending.ForwardVending", "mCount is 0, why call get()?", new Object);
return null;
}
```
### 调用父类 com.tencent.mm.vending.base.Vending 的同名方法
```
public <T> T get(_Index _Index) {
return a((Object) _Index);
}
private _Struct a(_Index _Index) {
Looper myLooper = Looper.myLooper();
if (myLooper != this.c && myLooper != this.d) {
throw new IllegalAccessError("Call from wrong looper");
} else if (this.g.get()) {
return null;
} else {
i lock = getLock(_Index);
if (invalidIndex(_Index)) {
return lock.b;
}
if (myLooper == this.c) {
return b(lock, _Index).b;
}
a(lock, (Object) _Index);
return lock.b;
}
}
private boolean a(i<_Struct, _Index> iVar, _Index _Index) {
synchronized (iVar.c) {
if (!iVar.f || iVar.d || iVar.e) {
this.q = true;
Object resolveAsynchronous = resolveAsynchronous(_Index);
this.q = false;
if (iVar.g) {
return false;
}
a((i) iVar, (Object) _Index, resolveAsynchronous);
return false;
}
return true;
}
}
protected abstract _Struct resolveAsynchronous(_Index _Index);
```
### 上面 get 方法调用一个参数的 a 方法,一个参数的 a 方法调用两个参数的 a 方法,最终调用需要子类实现的 resolveAsynchronous 方法。
### 在子类 com.tencent.mm.vending.base.b 中发现了 resolveAsynchronous 方法,调用的是子类的 Cx 方法
```
protected Object resolveAsynchronous(Object obj) {
return Cx(((Integer) obj).intValue());
}
protected abstract _Struct Cx(int i);
```
### 在子类 com.tencent.mm.plugin.sns.ui.a.b.a 中发现了 Cx 方法,
```
public final Object Cx(int i) {
return Cw(i);
}
```
### 该方法调用了同类的 Cw 方法,改方法的代码有点长,就不贴了,最终返回了我们需要的类 av 的实例
### 5、终于可以写代码了,主要逻辑是获取 com.tencent.mm.plugin.sns.ui.a.b.a 的 Cw 方法返回值 av 的实例,判断该实例的成员变量 qkM ,如果为 true 就表示是广告,保存下来。在com.tencent.mm.plugin.sns.ui.a.a 中判断当前条目是否在上面保存的结果中,如果存在就返回空视图,返回继续调用该方法体。这里就不处理缩进了
```
private Context context;
private final Set<Integer> adSet = new HashSet<>();
private void hookA(final XC_LoadPackage.LoadPackageParam lpParam) {
try {
String className = "com.tencent.mm.plugin.sns.ui.a.a";
final Class clazz = lpParam.classLoader.loadClass(className);
if (clazz != null) {
Constructor[] constructors = clazz.getDeclaredConstructors();
for (Constructor constructor : constructors) {
if (constructor.getParameterTypes().length == 6) {
XposedHelpers.findAndHookConstructor(clazz, constructor.getParameterTypes(),
constructor.getParameterTypes(), constructor.getParameterTypes(),
constructor.getParameterTypes(), constructor.getParameterTypes(),
constructor.getParameterTypes(), new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
context = (Context) param.args;
adSet.clear();
}
});
break;
}
}
XposedHelpers.findAndHookMethod(clazz, "getView", int.class, View.class, ViewGroup.class,
new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
if (adSet.contains(param.args)) {
return new View(context);
}
return XposedBridge.invokeOriginalMethod(param.method, param.thisObject,
param.args);
}
});
} else {
LogUtil.e(TAG, "class " + className + " not found");
}
className = "com.tencent.mm.plugin.sns.ui.a.a$1";
final Class dataChangeClazz = lpParam.classLoader.loadClass(className);
if (dataChangeClazz != null) {
XposedHelpers.findAndHookMethod(dataChangeClazz, "cll", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
adSet.clear();
}
});
} else {
LogUtil.e(TAG, "class " + className + " not found");
}
} catch (Throwable e) {
LogUtil.e(TAG, "hookA err:" + Log.getStackTraceString(e));
}
}
private void hookC(final XC_LoadPackage.LoadPackageParam lpParam) {
try {
final String className = "com.tencent.mm.plugin.sns.ui.a.b.a";
final String avClassName = "com.tencent.mm.plugin.sns.ui.av";
final Class clazz = lpParam.classLoader.loadClass(className);
final Class avClazz = lpParam.classLoader.loadClass(avClassName);
if (avClazz == null) {
LogUtil.e(TAG, "class " + className + " not found");
return;
}
final Field isAdField = XposedHelpers.findFieldIfExists(avClazz, "qkM");
if (clazz != null) {
XposedHelpers.findAndHookMethod(clazz, "Cw", int.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Object result = param.getResult();
if (result == null) {
return;
}
boolean isAd = (boolean) isAdField.get(result);
if (isAd) {
int position = (int) param.args;
adSet.add(position);
LogUtil.e(TAG, "position: " + position + " is ad");
}
}
});
} else {
LogUtil.e(TAG, "class " + className + " not found");
}
} catch (Throwable e) {
LogUtil.e(TAG, "hookA err:" + Log.getStackTraceString(e));
}
}
```
## 所有源代码已上传 (https://github.com/ColoThor/WeChatSnsAd)
## 最后,腾讯官方的减少个性化广告地址:https://privacy.qq.com/ads/optout.html
## 在当前页登录及后续操作视频教程:https://tccos.myworld.fun/upload/2019-09-14_15-41-39-169.mp4
## 腾讯官方的减少个性化广告新地址:https://ads.privacy.qq.com/ads/optout.html
Xposed去除微信朋友圈广告
貌似一段时间有用,减少广告也是一时的,tx还是会持续给你推荐,不然怎么赚钱盈利 感觉蛮复杂的 仰望大神中。。。。 腾讯广告手动只能关闭半年的广告,之后还需要再次手动关闭。 好像挺有用的样子 大神真牛,研究了好久,可以借鉴 太好了终于有这个了希望也能来个QQ的 好牛x的感觉,支持一下 腾讯就是喜欢看着你好烦又干不掉我的样子。 感觉有点复杂哦