某艺TV APK 破解去广告及源码分析

5,845 阅读15分钟

1. 准备工作

用例 APK 用例 APK 版本:某艺TV版 v12.1.0

iqiyi

原 APK 链接:pan.baidu.com/s/1zNKk662T…

修改 APK 链接:pan.baidu.com/s/16xo6FN_o…

本人测试环境

  1. 测试机 Pixel 2(Android 10)
  2. frida v15.1.27(objection 插件 v1.11.0)
  3. 开发者助手 v1.2.1
  4. MT管理器 v13.3
  5. jadx-gui v1.4.4
  6. fiddler v5.0.20211.51073

2. 去除广告思路

2.1 Android 广告

2.1.1 Android 中的广告形式

广告的表现形式很多,可能是一个界面(activity),可能是局部在上方或下方的一个区域视图(view)等。以下是常见广告形式:

  1. 嵌入式广告:将广告直接嵌入到应用程序中,通常出现在应用程序的底部、顶部或侧边栏。
  2. 插页式广告:应用程序的某个时间点弹出的广告,通常会覆盖整个屏幕。插页式广告通常在应用程序的特定事件之后出现,例如游戏中的关卡结束或应用程序的主菜单页面。
  3. 横幅广告:在应用程序的顶部或底部显示的广告,通常以图像或文本的形式出现。横幅广告通常比嵌入式广告小,不会占用应用程序的太多空间。
  4. 视频广告:在应用程序中播放的广告,通常以全屏或插页式的形式出现。通常需要观看一段时间才能跳过或关闭。

2.1.2 Android 广告来源

  1. Push 推送广告:通过推送消息到用户设备通知栏上展示广告。
  2. 第三方 SDK 广告:很多应用都会集成第三方广告平台,比如 AdMob、Facebook Audience Network、Unity Ads 等等,应用程序可以用第三方广告 SDK 来从其他公司的广告库中获取广告并在应用程序中展示。

 

2.2 Android 去除广告思路

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

  • 对于静态布局的广告:广告图片视频都是保存在apk里的,只需要直接从配置清单 xml 文件,或相应的布局xml文件入手,修改容器的布局或者删除相应的代码,就可去除广告。
  • 对于动态导入第三方SDK的广告:我们就需要从代码逻辑上入手。找到它动态导入广告的地方,尝试修改判断条件,从而使导入广告失败,或者让广告无法显示,从而去除广告。

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

3. 分析开屏广告

3.1 分析步骤

3.1.1 分析广告页面

ad_layout

首先对开屏广告页面进行分析,通过 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可得三处引用该资源。

resource

resource

通过 hook 分析发现在视频启动时的广告,调用的是 aux 类的函数:

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 到的函数调用栈,分析其运行过程:

Xc

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 类中存在一些可能是与加载广告界面相关的函数。

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 的分支中。

修改if判断

重打包编译签名,运行程序,已去除视频广告:

去除广告

4.2 总结

4.2.1 破解广告技巧

  • 对于破解视频广告或其他广告,都可以通过获取广告的相关控件,分析函数的调用逻辑顺序,定位到关键类,分析类,找到关键函数。
  • 针对复杂客户端,尽量不采用关键字搜索的方式去破解,因为复杂的客户端代码都是有设计思想的,并且大概率做了混淆,无法轻易通过关键字符串进行定位,可以尝试通过资源 ID 进行定位。
  • 提高英文水平,如 player 代表播放器、Ad Duration 代表广告持续时间等,破解的首要任务就是看懂代码,特别是对于混淆过的代码,那些没有混淆过的函数名、变量名就是破解的关键,只有看懂才有机会能猜到关键点。

4.2.2 扩展:proxy代理类

分析代码后发现,广告的生成、调用、配置大部分都是在 QYMediaPlayerProxy 类中完成的,并且播放器的核心功能也有一部分在代理类中调用。

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

  1. 可以过滤和控制广告流量,例如阻止一些恶意或不受欢迎的广告,以及提高广告访问速度和可靠性。
  2. 在特殊情况下,广告服务器可能会要求使用特定的代理服务器或 IP 地址进行广告请求。这时候,动态代理就可以被用来实现这些特殊的网络访问要求,确保广告请求能够成功发送和接收。

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

android 广告 SDK 原理流程图

SDK

[参考链接]: Android反编译实战-去广告_安卓反编译去除广告_sam.li的博客 android广告SDK原理详解