无论怎样形式、怎样来源的广告,在本地一定需要展示出来,展示就需要广告内容载体,如界面、视图等,对于这些容器,即可以利用静态的布局,也可以动态生成布局。如果能移除这些容器、或者破坏容器生成条件就可以达到去广告的地步。
本次案例是来自于第三方 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 代理类了,将播放器核心功能(播放视频)融入到代理类中,让其负责对核心功能进行扩展(如在播放视频之前添加广告)。这样既方便后续软件更新,也会使逻辑更加清晰、出错时能快速定位。