1. 预备工作
用例 APK 用例 APK 版本:某艺TV版 v12.1.0
原 APK 链接:pan.baidu.com/s/1zNKk662T…
修正 APK 链接:pan.baidu.com/s/16xo6FN_o…
自己测验环境
- 测验机 Pixel 2(Android 10)
- frida v15.1.27(objection 插件 v1.11.0)
- 开发者帮手 v1.2.1
- MT管理器 v13.3
- jadx-gui v1.4.4
- fiddler v5.0.20211.51073
2. 去除广告思路
2.1 Android 广告
2.1.1 Android 中的广告方式
广告的表现方式很多,可能是一个界面(activity),可能是部分在上方或下方的一个区域视图(view)等。以下是常见广告方式:
- 嵌入式广告:将广告直接嵌入到应用程序中,一般呈现在应用程序的底部、顶部或侧边栏。
- 插页式广告:应用程序的某个时刻点弹出的广告,一般会覆盖整个屏幕。插页式广告一般在应用程序的特定事情之后呈现,例如游戏中的关卡完毕或应用程序的主菜单页面。
- 横幅广告:在应用程序的顶部或底部显现的广告,一般以图画或文本的方式呈现。横幅广告一般比嵌入式广告小,不会占用应用程序的太多空间。
- 视频广告:在应用程序中播映的广告,一般以全屏或插页式的方式呈现。一般需求观看一段时刻才干越过或封闭。
2.1.2 Android 广告来历
- Push 推送广告:经过推送音讯到用户设备告诉栏上展现广告。
- 第三方 SDK 广告:很多应用都会集成第三方广告渠道,比方 AdMob、Facebook Audience Network、Unity Ads 等等,应用程序能够用第三方广告 SDK 来从其他公司的广告库中获取广告并在应用程序中展现。
2.2 Android 去除广告思路
无论怎样方式、怎样来历的广告,在本地一定需求展现出来,展现就需求广告内容载体,如界面、视图等,关于这些容器,即能够利用静态的布局,也能够动态生成布局。假如能移除这些容器、或许破坏容器生成条件就能够达到去广告的地步。
- 关于静态布局的广告:广告图片视频都是保存在apk里的,只需求直接从装备清单 xml 文件,或相应的布局xml文件下手,修正容器的布局或许删除相应的代码,就可去除广告。
- 关于动态导入第三方SDK的广告:咱们就需求从代码逻辑上下手。找到它动态导入广告的当地,测验修正判别条件,然后使导入广告失败,或许让广告无法显现,然后去除广告。
本次事例是来自于第三方 SDK 软件的广告投进,经过发送恳求包,然后获取相对应的广告 ID 与资源,关于这种情况,咱们能够经过定位 SDK 的初始化、广告恳求、广告展现等代码,来剖析其逻辑,然后找到突破点。
3. 剖析开屏广告
3.1 剖析过程
3.1.1 剖析广告页面
首先对开屏广告页面进行剖析,经过 MT 管理器发现该广告是处在 WelcomeActivity 类中,咱们直接 hook 类,得到其函数调用栈。
3.1.2 剖析发动时函数调用栈
能够猜测 showHomePage() 便是展现咱们的主页了,咱们逐条剖析广告发生前的函数:
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();
}
3.1.3 修正 if 判别
能够看到当当时 Activity 数量不等于1时,就直接调 showHomePage 函数,咱们能够将这个判别改为永真,让其直接显现主页。
重打包编译签名,运转程序,已去除开屏广告:
3.2 总结
关于开屏广告,咱们能够观察应用发动的 Acitivity 次序 (先从主进口切入Main),寻找其函数调用次序,找到其广播广告的页面,将其逻辑更改,就能够屏蔽掉开屏广告。
4. 剖析播映视频广告
4.1 剖析过程
4.1.1 剖析广告页面
首先对视频广告页面进行剖析,有暂停键、静音键、概况键、持续时刻、会员封闭提示…,咱们能够想到:
- 剩余时刻:获取广告时长,并设置计时器(可能会有判别时刻归零,完毕视频)
- 了解概况:获取广告 ID,设置按钮监听,保存广告概况 url
- 暂停键:保留当时广告播映方位
……
4.1.2 剖析持续时刻
自己挑选剩余时刻作为破解进口,经过开发者帮手查到显现时刻的资源 ID 是 R.id.account_ads_time_pre_ad
,查找资源ID可得三处引证该资源。
经过 hook 剖析发现在视频发动时的广告,调用的是 aux 类的函数:
剖析 aux 类里运用了R.id.account_ads_time_pre_ad
的办法,找到三处,别离剖析:
榜首、二处均用在 Xi()
函数中,该函数首要设置广告装备及布置广告界面。
private void Vz() {
......
this.bPB = (TextView) findViewById(R.id.account_ads_time_pre_ad);
}
private void Xi() {
...
// 获取当时广告播映器的状况
BaseState currentState = this.mAdInvoker.getCurrentState();
// 获取了广告播映器的UI战略
int adUIStrategy = this.mAdInvoker.getAdUIStrategy();
// 打印日志
com.iqiyi.video.qyplayersdk.g.aux.i("PLAY_SDK_AD_ROLL", "{GPhoneRollAdView}", " show ad UI, current state = ", currentState, ", adUiStrategy: ", Integer.valueOf(adUIStrategy));
// 设置视图的背景,依据当时广告播映器的状况来挑选不同的背景资源
this.bPy.setBackgroundResource(currentState.isOnPaused() ? R.drawable.qiyi_sdk_play_ads_player : R.drawable.qiyi_sdk_play_ads_pause);
// 获取了当时广告的交给类型
int i = this.mDeliverType;
boolean z = i == 3 || i == 7 || i == 4;
// 获取广告播映器装备
QYPlayerADConfig adConfig = this.mAdInvoker.getAdConfig();
int i2 = 8;
// 依据UI战略的不同值,来设置一些视图的可见性或执行一些办法,8不可见,0可见
if (adUIStrategy == 1) {
this.bPA.setVisibility(8);
this.bPy.setVisibility(8);
this.bPF.setVisibility(8);
this.bPz.setVisibility(8);
} else if (adUIStrategy == 2) {
this.bPA.setVisibility(8);
this.bPy.setVisibility(8);
this.bPz.setVisibility(8);
this.bSv.setVisibility(8);
this.bSq.setVisibility(8);
this.bSq.setOnTouchListener(null);
} else if (adUIStrategy == 3) {
this.bPA.setVisibility(8);
this.bPF.setVisibility(8);
boolean isMute = isMute(); // 检查广告是否处于静音状况
this.bPL = isMute;
setAdMute(isMute, false);
} else {
this.bPF.setVisibility(0);
TextView textView = this.bPA;
if (!this.mIsLand) {
i2 = 0;
}
textView.setVisibility(i2);
boolean isMute2 = isMute();
this.bPL = isMute2;
setAdMute(isMute2, false);
Xk();
}
if (this.mDeliverType != 6) {
this.bPB.setVisibility(0); // 设置时刻视图可显
}
this.bPB.setText(String.valueOf(this.mAdInvoker.getAdDuration())); // 给时刻视图赋值
}
第三处坐落 Xc()
函数中,依据 hook 到的函数调用栈,剖析其运转过程:
public void Xc() {
// 获取广告播映时长
int adDuration = this.mAdInvoker.getAdDuration();
String str = adDuration + "";
...
jv(adDuration); // 判别能不能越过广告
if (XE()) {
XH();
}
TextView textView = this.bPB; // 设置剩余时刻
if (textView != null) {
textView.setText(str); // 显现非VIP持续时刻
}
int i = this.mDeliverType;
if (i == 3 || i == 7) { // 假如交给类型是3或7 (VIP广告),广告持续时刻小于1,调用dz(false)
if (adDuration < 1) {
dz(false);
} else {
this.bSA.setText(str); // 显现VIP持续时刻
}
}
if (this.mDeliverType == 2) { // 答应越过的广告
int Xp = Xp(); // 广告可越过的剩余时刻
if (Xp < 1) { // 答应越过
Xl(); // 显现越过按钮
} else {
this.bSG.setText(this.mContext.getString(R.string.trueview_accountime, Integer.valueOf(Xp)));
}
}
// 省流:依据不同的交给类型,为不同类型的广告进行时刻装备与视图是否可显操作
...
}
// 处理广告的交互时刻约束逻辑
private void jv(int i) {
// 判别是否为触摸广告,是否支撑点击跳转,而且是否现已被点击过
if (!this.bOR.isTouchAd() || this.bOR.getClickThroughType() != 0 || this.bTn) {
return; // 是,直接回来
}
// 获取广告的预览信息
PreAD creativeObject = this.bOR.getCreativeObject();
// getInterTouchTime()是广告中点击交互的时刻间隔,回来 10,表明用户需求等待至少 10 秒之后才干进行一次点击交互。小于0,说明能够点击。
// 后边一个条件是指当时时刻加上最早答应交互的时刻点,假如超过广告总时长,则不答应交互,比方总时长120秒,getInterTouchTime() 回来 40,当时时刻为100秒,大于总时长,不答应交互。
if (creativeObject.getInterTouchTime() <= -1 || i + creativeObject.getInterTouchTime() > this.bTp) {
return;
}
// 重置广告界面,持续播映
this.bSq.reset();
Wu();
}
// 判别当时广告是创意广告
private boolean XE() {
CupidAD<PreAD> cupidAD = this.bOR;
if (cupidAD == null || cupidAD.getCreativeObject() == null) {
return false;
}
return this.bOR.getDeliverType() == 10 || this.bOR.getDeliverType() == 11;
}
// 计算广告可越过的剩余时刻
private int Xp() {
if (this.bOR.getDeliverType() != 2) {
return 0;
}
return (this.bOR.getSkippableTime() / 1000) - ((this.bOR.getDuration() / 1000) - this.mAdInvoker.getAdDuration());
}
上面两个函数都是对布局文件进行操作,设置其 text 或许是否可显,并没有判别去掉广告的当地,咱们还有持续寻找。
比照两个函数发现,获取持续时刻的函数是 getAdDuration(),咱们去寻找该函数声明,发现在 com.iqiyi.video.qyplayersdk.player.QYMediaPlayerProxy
类中:
public int getAdDuration() {
com.iqiyi.video.qyplayersdk.core.com1 com1Var = this.mPlayerCore;
if (com1Var == null) {
return 0;
}
return com1Var.getAdsTimeLength();
}
// 坐落 com.iqiyi.video.qyplayersdk.core.QYBigCorePlayer 类中
public int getAdsTimeLength() {
com8 com8Var = this.pumaPlayer;
if (com8Var != null) {
return Math.round(com8Var.GetADCountDown() / 1000.0f); // 转成整数
}
return 0;
}
// com.mcto.player.nativemediaplayer.NativeMediaPlayer 类中
public int GetADCountDown() {
int GetADCountDown;
if (IsCalledInPlayerThread()) { // 判别是否在播映器线程中调用
return this.native_media_player_bridge.GetADCountDown(); // 获取广告持续时刻
}
synchronized (this) {
if (!this.native_player_valid) { // 判别播映器是否合法
throw new MctoPlayerInvalidException(puma_state_error_msg);
}
GetADCountDown = this.native_media_player_bridge.GetADCountDown();
}
return GetADCountDown;
}
// com.mcto.player.nativemediaplayer.NativeMediaPlayerBridge 类中
public int GetADCountDown() {
// 调用了一个指定ID为43的办法,该办法回来一个JSON格式的字符串,其间包含有关广告信息的数据
String InvokeMethod = InvokeMethod(43, "{}");
if (InvokeMethod.isEmpty()) { // 回来的字符串为空,则表明当时没有广告,办法回来0。
return 0;
}
try {
// 回来的字符串不为空,则将其转换为JSONObject目标,并获取其间名为ad_count_down的值
return new JSONObject(InvokeMethod).getInt("ad_count_down");
} catch (JSONException unused) {
return 0;
}
}
跟进到 com.mcto.player.nativemediaplayer.NativeMediaPlayerBridge 咱们就能够发现,该软件是在Native层利用 mediaplay 获取视频时刻信息。到这儿获取剩余时刻的 Java 层剖析就差不多能够了。咱们能够看到的是在 NativeMediaPlayerBridge
这个类中调用了很多 native 办法去获取广告的各种信息供后续操作,可是将一切的办法全修正一遍不太实际,咱们需求寻找判别是否显现广告界面的当地。
4.1.3 剖析 QYMediaPlayerProxy 署理类
依据 hook 上层类的办法调用发现,QYMediaPlayerProxy
类中存在一些可能是与加载广告界面相关的函数。
几个重要的函数剖析:
// 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; // 不需求进行网络阻拦
}
咱们重点剖析 performBigCorePlayback
函数:
// 执行播映器的中心播映功用
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);
}
}
...
}
咱们能够发现这个函数便是判别是否显现广告界面的函数,能够猜测只有当是VIP账户时,播映数据(playData)才为空,才会使 i = 0(广告ID为0)。
4.1.4 修正 if 判别
到这儿咱们就能够测验进行破解了,将 if 判别修正,使之进入 i=0 的分支中。
重打包编译签名,运转程序,已去除视频广告:
4.2 总结
4.2.1 破解广告技巧
- 关于破解视频广告或其他广告,都能够经过获取广告的相关控件,剖析函数的调用逻辑次序,定位到要害类,剖析类,找到要害函数。
- 针对杂乱客户端,尽量不采用要害字查找的方式去破解,由于杂乱的客户端代码都是有规划思维的,而且大概率做了混淆,无法轻易经过要害字符串进行定位,能够测验经过资源 ID 进行定位。
- 进步英文水平,如 player 代表播映器、Ad Duration 代表广告持续时刻等,破解的首要任务便是看懂代码,特别是关于混淆过的代码,那些没有混淆过的函数名、变量名便是破解的要害,只有看懂才有机会能猜到要害点。
4.2.2 扩展:proxy署理类
剖析代码后发现,广告的生成、调用、装备大部分都是在 QYMediaPlayerProxy
类中完结的,而且播映器的中心功用也有一部分在署理类中调用。
关于第三方SDK动态导入视频广告,一般会经过网络恳求向广告服务器发送恳求以获取广告,流程参阅下方 android 广告 SDK 原理流程图,常用办法使经过动态署理,经过动态署理这样的办法有一定的好处:
- 能够过滤和操控广告流量,例如阻挠一些恶意或不受欢迎的广告,以及进步广告拜访速度和可靠性。
- 在特别情况下,广告服务器可能会要求运用特定的署理服务器或 IP 地址进行广告恳求。这时候,动态署理就能够被用来完成这些特别的网络拜访要求,保证广告恳求能够成功发送和接收。
进一步剖析,咱们能够想到广告不太会是在软件刚出来时就加上,一定是后续附加上去的功用。后续除了广告之外肯定也会陆续附加其他功用,如何做到这些功用扩展呢?这就能够用 proxy 署理类了,将播映器中心功用(播映视频)融入到署理类中,让其负责对中心功用进行扩展(如在播映视频之前添加广告)。这样既方便后续软件更新,也会使逻辑更加清晰、出错时能快速定位。
android 广告 SDK 原理流程图
[参阅链接]: Android反编译实战-去广告_安卓反编译去除广告_sam.li的博客 android广告SDK原理详解