一、背景:最近发现微信朋友圈的广告有点多,有时前十条就出现2条广告,还是针对性投放的,于是想着怎么去除广告。

本次研究用的是微信谷歌商店版,版本名是7.0.0,版本号是1363,云盘地址,反编译工具是MT管理器

二、首先,广告和说说,明显的区别就是条目右上角有个广告标识,还可以进行点击,这就好办了。

ad_smallpng

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[0]);
   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()[0],
                                constructor.getParameterTypes()[1], constructor.getParameterTypes()[2],
                                constructor.getParameterTypes()[3], constructor.getParameterTypes()[4],
                                constructor.getParameterTypes()[5], new XC_MethodHook() {
                                    @Override
                                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                                        context = (Context) param.args[0];
                                        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[0])) {
                                    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[0];
                            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));
        }
    }

所有源代码已上传 WeChatSnsAd

最后,腾讯官方的减少个性化广告地址:https://privacy.qq.com/ads/optout.html

在当前页登录及后续操作视频教程: