[原创]某艺TV版 apk 破解去广告及源码分析
2023-4-7 12:59:15 Author: bbs.pediy.com(查看原文) 阅读量:25 收藏

无论怎样形式、怎样来源的广告,在本地一定需要展示出来,展示就需要广告内容载体,如界面、视图等,对于这些容器,即可以利用静态的布局,也可以动态生成布局。如果能移除这些容器、或者破坏容器生成条件就可以达到去广告的地步。

本次案例是来自于第三方 SDK 软件的广告投放,通过发送请求包,从而获取相对应的广告 ID 与资源,对于这种情况,我们可以通过定位 SDK 的初始化、广告请求、广告展示等代码,来分析其逻辑,从而找到突破点。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

private void checkPermission() {

    if (lpt2.br(InitHelper.getInstance().checkInitPermission(this))) {

        jumpToMain();

        return;

    }

    List<String> checkInitPermission = InitHelper.getInstance().checkInitPermission(this);

    androidx.core.app.aux.a(this, (String[]) checkInitPermission.toArray(new String[checkInitPermission.size()]), 1);

}

// 检查初始化权限

public List<String> checkInitPermission(Context context) {

    ArrayList<String> arrayList = new ArrayList();

    ArrayList arrayList2 = new ArrayList();

    arrayList.add("android.permission.INTERNET");   // 访问网络的权限

    if (!org.qiyi.speaker.u.con.bMX()) {

        arrayList.add("android.permission.READ_PHONE_STATE");    // 取手机状态的权限

    }

    arrayList.add("android.permission.WRITE_EXTERNAL_STORAGE");   // 写入外部存储设备的权限

    arrayList.add("android.permission.ACCESS_NETWORK_STATE");    // 访问网络状态的权限

    ....

}

private void jumpToMain() {

    Log.e("gzy", "size:" + SpeakerApplication.getInstance().getCurrentActivitySize());

        // 用户是否给软件授权

    if (!org.qiyi.speaker.o.con.bLa()) {

        org.qiyi.speaker.o.con.a(this, this.mLisenceCallback);  // 显示免责声明并进行用户许可

        // 加载splash启动页动画(没有后台进程)

    } else if (GuideController.INSTANCE.needShowSplashGuide()) {

        showGuidePage();

    } else {

                //

        launchMain(false);

    }

}

// 首次打开,启动应用程序主界面

public void launchMain(final boolean z) {

        // 如果当前Activity数量不等于1,那么显示主页。

    if (SpeakerApplication.getInstance().getCurrentActivitySize() != 1) {

        showHomePage(z);

        return;

    }

        // 注册一个启动画面的回调,请求广告并下载,当启动画面结束后, 显示广告。

    com.qiyi.video.g.con.aXh().registerSplashCallback(new ISplashCallback() { // from class: com.qiyi.video.speaker.activity.WelcomeActivity.2

        @Override // org.qiyi.video.module.api.ISplashCallback

        public void onAdAnimationStarted() {

        }

        @Override // org.qiyi.video.module.api.ISplashCallback

        public void onAdCountdown(int i) {

        }

        @Override // org.qiyi.video.module.api.ISplashCallback

        public void onAdOpenDetailVideo() {

        }

        @Override // org.qiyi.video.module.api.ISplashCallback

        public void onAdStarted(String str) {

        }

        @Override // org.qiyi.video.module.api.ISplashCallback

        public void onSplashFinished(int i) {

            WelcomeActivity.this.showHomePage(z);

            JobManagerUtils.a(new Runnable() { // from class: com.qiyi.video.speaker.activity.WelcomeActivity.2.1

                @Override // java.lang.Runnable

                public void run() {

                    com.qiyi.video.qysplashscreen.ad.aux.aUv().aUE();

                    ((ISplashScreenApi) ModuleManager.getModule(IModuleConstants.MODULE_NAME_SPLASH_SCREEN, ISplashScreenApi.class)).requestAdAndDownload();

                }

            }, 500, PageAutoScrollUtils.HANDLER_SWITCH_NEXT_TIPS_DELAY, "splashAD_requestad", WelcomeActivity.TAG);

        }

    });

    launchAppGuide();

}

对于开屏广告,我们可以观察应用启动的 Acitivity 顺序 (先从主入口切入Main),寻找其函数调用顺序,找到其播送广告的页面,将其逻辑更改,就可以屏蔽掉开屏广告。

本人选择剩余时间作为破解入口,通过开发者助手查到显示时间的资源 ID 是 R.id.account_ads_time_pre_ad,搜索资源ID可得三处引用该资源。

对比两个函数发现,获取持续时间的函数是 getAdDuration(),我们去寻找该函数声明,发现在 com.iqiyi.video.qyplayersdk.player.QYMediaPlayerProxy 类中:

跟进到 com.mcto.player.nativemediaplayer.NativeMediaPlayerBridge 我们就可以发现,该软件是在Native层利用 mediaplay 获取视频时间信息。到这里获取剩余时间的 Java 层分析就差不多可以了。我们可以看到的是在 NativeMediaPlayerBridge 这个类中调用了众多 native 方法去获取广告的各种信息供后续操作,但是将所有的方法全修改一遍不太现实,我们需要寻找判断是否显示广告界面的地方。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

// setVVCollector():设置VVCollector,收集播放器的VV统计信息。

// video view (VV),意思为视频播放次数,根据广告播放次数,统计盈利。

public void setVVCollector(com.iqiyi.video.qyplayersdk.module.a.f.con conVar) {

    com.iqiyi.video.qyplayersdk.module.a.aux auxVar = this.mStatistics;

    if (auxVar != null) {

        auxVar.setVVCollector(conVar);

    }

}

// init(): 初始化播放器界面

// 获取了mControlConfig中的一些配置信息,例如编解码类型、是否自动跳过片头片尾、色盲模式等,然后调用prn.aux构造方法创建一个prn对象,并设置这些配置信息,最后通过a()方法将prn对象和mPassportAdapter对象一起传入a方法中,完成播放器的初始化。

public void init() {

    this.mPlayerCore.a(new prn.aux(this.mControlConfig.getCodecType())

            .eH(this.mControlConfig.isAutoSkipTitle())

            .eI(this.mControlConfig.isAutoSkipTrailer())

            .kR(this.mControlConfig.getColorBlindnessType())

            .lX(this.mControlConfig.getExtendInfo())

            .lY(this.mControlConfig.getExtraDecoderInfo())

            .aie(), com.iqiyi.video.qyplayersdk.core.data.aux.a(this.mPassportAdapter));

}

// 检查 RC 策略是否需要执行

// RC 策略是指在不同的地理位置或网络环境下,根据不同的版权限制或合作协议,播放不同的内容或提供不同的服务。

public PlayData checkRcIfRcStrategyNeeded(PlayData playData) {

    if (playData == null) {

        com.iqiyi.video.qyplayersdk.g.aux.d(TAG, "QYMediaPlayerProxy checkRcIfRcStrategyNeeded source == null!");

        return playData;

    }

    int rCCheckPolicy = playData.getRCCheckPolicy();

    com.iqiyi.video.qyplayersdk.g.aux.d(TAG, "QYMediaPlayerProxy checkRcIfRcStrategyNeeded strategy == " + rCCheckPolicy);

    if (this.mPlayerRecordAdapter == null) {

        this.mPlayerRecordAdapter = new PlayerRecordAdapter();

    }

        // 根据 RCCheckPolicy (即 RC 策略) 的值。

        // 如果值为 2,直接返回 playData;如果值为 1 0,,则调用 PlayerRecordAdapter 的 retrievePlayerRecord 方法,获取播放记录,

    return rCCheckPolicy == 2 ? playData : (rCCheckPolicy == 1 || rCCheckPolicy == 0) ?

        com.iqiyi.video.qyplayersdk.player.data.b.con.a(playData, this.mPlayerRecordAdapter.retrievePlayerRecord(playData)) : playData;

}

// 获取登录用户信息

void login() {

    IPassportAdapter iPassportAdapter;

        // mPlayerCore 是播放器核心,mPassportAdapter 是用户身份验证适配器。

    if (this.mPlayerCore == null || (iPassportAdapter = this.mPassportAdapter) == null) {

        return;

    }

        // 判断是不是VIP用户,并获取相应用户信息

    this.mPlayerCore.login(com.iqiyi.video.qyplayersdk.core.data.aux.a(iPassportAdapter));

}

// 准备播放器重要核心配置

private void prepareBigCorePlayback(PlayData playData) {

    boolean z;

    org.qiyi.android.coreplayer.d.com7.beginSection("QYMediaPlayerProxy.prepareBigCorePlayback");

        // 检查是否需要预加载

        com.iqiyi.video.qyplayersdk.h.con conVar = this.mPreload;

    if (conVar != null) {

        conVar.aoj();

    }

        // 根据播放数据和控制配置,选择一个播放策略,根据策略选择对应操作

    int a2 = com.iqiyi.video.qyplayersdk.player.data.b.nul.a(playData, this.mContext, this.mControlConfig);

    com.iqiyi.video.qyplayersdk.g.aux.e("PLAY_SDK", "vplay strategy : " + a2);

    switch (a2) {

        case 1:

            performBigCorePlayback(playData);

            break;

        case 2:

            z = true;

            doVPlayBeforePlay(playData, z);

            break;

        case 3:

            doVPlayFullBeforePlay(playData);

            break;

        case 4:

            doVPlayAfterPlay(playData);

            break;

        case 5:

            if (com.iqiyi.video.qyplayersdk.g.aux.isDebug()) {

                throw new RuntimeException("address & tvid & ctype are null");

            }

            com.iqiyi.video.qyplayersdk.g.aux.e("PLAY_SDK", "address & tvid & ctype are null");

            break;

        case 6:

            z = false;

            doVPlayBeforePlay(playData, z);

            break;

    }

    org.qiyi.android.coreplayer.d.com7.endSection();

}

// 视频播放结束后,继续获取视频的相关信息。

public void doVPlayAfterPlay(final PlayData playData) {

    performBigCorePlayback(playData);

    lpt6 lpt6Var = this.mTaskExecutor;

    if (lpt6Var != null) {

        lpt6Var.q(new Runnable() { // from class: com.iqiyi.video.qyplayersdk.player.QYMediaPlayerProxy.1

            @Override // java.lang.Runnable

            public void run() {

                QYMediaPlayerProxy.this.requestVplayInfo(playData);

            }

        });

    }

}

// 在获取视频源前获取一些与视频相关的信息

private void doVPlayBeforePlay(PlayData playData, boolean z) {

    VPlayParam a2 = com.iqiyi.video.qyplayersdk.player.data.b.con.a(playData, VPlayHelper.CONTENT_TYPE_PLAY_CONDITION, this.mPassportAdapter);

    this.mVPlayHelper.cancel();

        // 请求 VPlay 信息

    this.mVPlayHelper.requestVPlay(this.mContext, a2, new aux(this, playData, this.mSigt, z), this.mBigcoreVplayInterceptor);

    sendVPlayRequestPingback(true, playData, this.mSigt);

    com.iqiyi.video.qyplayersdk.b.com3.b(playData);

    com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, " doVPlayBeforePlay needRequestFull=", Boolean.valueOf(z));

}

// 判断是否需要网络拦截

private boolean isNeedNetworkInterceptor(PlayerInfo playerInfo) {

        // 是否需要忽略用户代理的拦截

    if (ignoreNetworkInterceptByUA()) {

        com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, "ignoreNetworkInterceptByUA ");

        return false;

    }

        // 判断当前是否处于离线状态,并且要播放的视频是在线视频

    boolean gW = org.iqiyi.video.l.aux.gW(this.mContext);

    boolean D = com.iqiyi.video.qyplayersdk.player.data.b.nul.D(playerInfo);

    if (gW && D) {

                // 获取当前的错误码版本号,根据不同的版本号来执行不同的逻辑

        int errorCodeVersion = getErrorCodeVersion();

        com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, "isNeedNetworkInterceptor isOffNetWork = ", Boolean.valueOf(gW), " isOnLineVideo = ", Boolean.valueOf(D), " errorCodeVer = " + errorCodeVersion);

                if (errorCodeVersion == 1) {

                        // 自定义错误码为900400的播放器错误

            this.mInvokerQYMediaPlayer.onError(PlayerError.createCustomError(900400, "current network is offline, but you want to play online video"));

            return true;    // 进行网络拦截

        } else if (errorCodeVersion == 2) { 

                        // 返回错误码和错误信息

            org.iqiyi.video.data.com7 bbQ = org.iqiyi.video.data.com7.bbQ();

            bbQ.xC(String.valueOf(900400));

            bbQ.setDesc("current network is offline, but you want to play online video");

            this.mInvokerQYMediaPlayer.onErrorV2(bbQ);

            return true;

        }

    }

    return false;     // 不需要进行网络拦截

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

// 执行播放器的核心播放功能

private void performBigCorePlayback(PlayData playData, PlayerInfo playerInfo, String str) {

    int i;

        // 判断是否有自定义的播放拦截器(mDoPlayInterceptor),如果有且拦截器拦截了播放请求,则不播放视频。

    com.iqiyi.video.qyplayersdk.f.con conVar = this.mDoPlayInterceptor;

    if (conVar != null && conVar.e(playerInfo)) {

        com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, "DoPlayInterceptor is intercept!");

        lpt5 lpt5Var = this.mInvokerQYMediaPlayer;

        if (lpt5Var == null) {

            return;

        }

        lpt5Var.amX();  

        // 没有播放器信息,什么都不做

    } else if (this.mPlayerInfo == null) {

    }

        // 重点

        else {

        org.qiyi.android.coreplayer.d.com7.beginSection("QYMediaPlayerProxy.performBigCorePlayback");

                // 通过判断播放数据(playData)是否为空以及是否存在播放地址,空则i = 0

        if (com.iqiyi.video.qyplayersdk.player.data.b.nul.A(playerInfo) || playData == null) {

            i = 0;

        } else {

                        // 如果有地址,根据该数据生成CupidVvId,并将该ID与广告相关的Ad对象(mAd)绑定。

                        // 所以这里就是去后台获取广告的id

            com.iqiyi.video.qyplayersdk.cupid.data.model.com9 a2 = com.iqiyi.video.qyplayersdk.cupid.util.con.a(playData, playerInfo, false, this.mPlayerRecordAdapter, 0);

            a2.eV(isIgnoreFetchLastTimeSave());

            int generateCupidVvId = CupidAdUtils.generateCupidVvId(a2, playData.getPlayScene());

            com.iqiyi.video.qyplayersdk.cupid.com4 com4Var = this.mAd;

            if (com4Var != null) {

                com4Var.la(generateCupidVvId);    // 更新当前的广告ID

            }

            org.qiyi.android.coreplayer.d.aux.boe();

            i = generateCupidVvId;

        }

                // a3 存储广告信息

        com.iqiyi.video.qyplayersdk.core.data.model.com1 a3 = com.iqiyi.video.qyplayersdk.core.data.a.aux.a(this.mSigt, i, playData, playerInfo, str, this.mControlConfig);

        com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK", TAG, " performBigCorePlayback QYPlayerMovie=", a3);

        this.mPlayerInfo = new PlayerInfo.Builder().copyFrom(playerInfo).extraInfo(new PlayerExtraInfo.Builder().copyFrom(playerInfo.getExtraInfo()).sigt(a3.getSigt()).build()).build();

        // 通知播放器信息已更改(在这里是指开始播放广告)

                notifyPlayerInfoChanged();

                // 判断是否断网

        if (!isNeedNetworkInterceptor(playerInfo)) {

            if (playData == null || (TextUtils.isEmpty(playData.getPlayAddress()) && (TextUtils.isEmpty(playData.getTvId()) || "0".equals(playData.getTvId())))) {

                PlayerExceptionTools.report(0, 0.1f, "1", com.iqiyi.video.qyplayersdk.player.data.b.con.i(playData));

            }

            com.iqiyi.video.qyplayersdk.core.com1 com1Var = this.mPlayerCore;

            if (com1Var != null) {

                com1Var.setVideoPath(a3);    // 设置广告url

                this.mPlayerCore.ahF();

            }

        }

        org.qiyi.android.coreplayer.d.com7.endSection();

    }

}

// 停止视频

public void amX() {

    d dVar = this.mQYMediaPlayer;

    if (dVar != null) {

        dVar.stopPlayback();

    }

}

// 判断是否获取到视频

public static boolean A(PlayerInfo playerInfo) {

    return z(playerInfo) || y(playerInfo);

}

// 获取PlayerExtraInfo对象的播放地址和播放地址类型

public static boolean z(PlayerInfo playerInfo) {

    if (playerInfo == null || playerInfo.getExtraInfo() == null) {

        return false;

    }

    PlayerExtraInfo extraInfo = playerInfo.getExtraInfo();

    String playAddress = extraInfo.getPlayAddress();

    int playAddressType = extraInfo.getPlayAddressType();

    if (TextUtils.isEmpty(playAddress)) {

        return false;

    }

    return playAddressType == 9 || playAddressType == 4 || playAddressType == 8;

}

// 判断是否有视频和专辑ID

public static boolean y(PlayerInfo playerInfo) {

    String s = s(playerInfo);     // 专辑ID

    String u = u(playerInfo);     // 视频ID

    if ((TextUtils.isEmpty(s) || TextUtils.equals(s, "0")) && !((!TextUtils.isEmpty(u) && !TextUtils.equals(u, "0")) || playerInfo == null || playerInfo.getExtraInfo() == null)) {

        // 获取PlayerExtraInfo对象的播放地址和播放地址类型

                PlayerExtraInfo extraInfo = playerInfo.getExtraInfo();

        return !TextUtils.isEmpty(extraInfo.getPlayAddress()) && extraInfo.getPlayAddressType() == 6;

    }

    return false;

}

// 获取专辑ID

public static String s(PlayerInfo playerInfo) {

    String id;

    return (playerInfo == null || playerInfo.getAlbumInfo() == null || (id = playerInfo.getAlbumInfo().getId()) == null) ? "" : id;

}

// 获取视频ID

public static String u(PlayerInfo playerInfo) {

    String id;

    return (playerInfo == null || playerInfo.getVideoInfo() == null || (id = playerInfo.getVideoInfo().getId()) == null) ? "" : id;

}

// 一个广告控制器方法,用于更新当前的CupidvvId

public void la(int i) {

        // col=0,则说明当前没有活跃的vvId,打印日志信息表示要更新当前的vvId

    if (this.col.getAndIncrement() == 0) {

        com.iqiyi.video.qyplayersdk.g.aux.i("PLAY_SDK_AD_MAIN", "{AdsController}", " update current cupid vvId. current doesn't has active vvId.");

    } else {

        com.iqiyi.video.qyplayersdk.g.aux.i("PLAY_SDK_AD_MAIN", "{AdsController}", " update current cupid vvId. but current has active vvId.");

        // 将旧的vvId赋值给coh变量

                this.coh = this.coi;

    }

        // 将当前新的ID赋给coi

    this.coi = i;

    lc(i);

    com5.aux auxVar = this.mQYAdPresenter;

    if (auxVar != null) {

        auxVar.lh(i);    // 为暂停播放函数与继续播放函数传递广告ID

    }

}

/*

        该方法用于注册广告委托和委托JSON,以展示广告

        通过 qYPlayerADConfig3.checkRegister 方法判断是否需要注册广告

        通过 Cupid.registerObjectAppDelegate 方法注册代理

        广告类型包括:

        中插广告(SlotType.SLOT_TYPE_BRIEF_ROLL)、

        viewpoint广告(SlotType.SLOT_TYPE_VIEWPOINT)、

        页面广告(SlotType.SLOT_TYPE_PAGE)等等

        代码过长就不再此展示,需要请自行查看

*/

private void lc(final int i) {

    com.iqiyi.video.qyplayersdk.g.aux.d("PLAY_SDK_AD_CORE", "{AdsController}", "; registerCupidJsonDelegate vvId:", Integer.valueOf(i), "");

        org.qiyi.android.coreplayer.d.aux.wr(com.qiyi.baselib.utils.d.nul.fJ(org.iqiyi.video.mode.com3.enn) ? 2 : 1);

        ...

        QYPlayerADConfig qYPlayerADConfig5 = this.cog;

      if (qYPlayerADConfig5.checkRegister(256, qYPlayerADConfig5.getAddAdPolicy())) {

          QYPlayerADConfig qYPlayerADConfig6 = this.cog;

          if (!qYPlayerADConfig6.checkRegister(256, qYPlayerADConfig6.getRemoveAdPolicy())) {

              Cupid.registerJsonDelegate(i, SlotType.SLOT_TYPE_VIEWPOINT.value(), this.cof);

          }

      }

        ...

}

对于第三方SDK动态导入视频广告,通常会通过网络请求向广告服务器发送请求以获取广告,流程参考下方 android 广告 SDK 原理流程图,常用方法使通过动态代理,通过动态代理这样的方法有一定的好处:

进一步分析,我们可以想到广告不太会是在软件刚出来时就加上,一定是后续附加上去的功能。后续除了广告之外肯定也会陆续附加其他功能,如何做到这些功能扩展呢?这就可以用 proxy 代理类了,将播放器核心功能(播放视频)融入到代理类中,让其负责对核心功能进行扩展(如在播放视频之前添加广告)。这样既方便后续软件更新,也会使逻辑更加清晰、出错时能快速定位。


文章来源: https://bbs.pediy.com/thread-276778.htm
如有侵权请联系:admin#unsafe.sh