1. 预备工作

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

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

原 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 剖析广告页面

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

首先对开屏广告页面进行剖析,经过 MT 管理器发现该广告是处在 WelcomeActivity 类中,咱们直接 hook 类,得到其函数调用栈。

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

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 函数,咱们能够将这个判别改为永真,让其直接显现主页。

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

重打包编译签名,运转程序,已去除开屏广告:

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

3.2 总结

关于开屏广告,咱们能够观察应用发动的 Acitivity 次序 (先从主进口切入Main),寻找其函数调用次序,找到其广播广告的页面,将其逻辑更改,就能够屏蔽掉开屏广告。

4. 剖析播映视频广告

4.1 剖析过程

4.1.1 剖析广告页面

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

首先对视频广告页面进行剖析,有暂停键、静音键、概况键、持续时刻、会员封闭提示…,咱们能够想到:

  • 剩余时刻:获取广告时长,并设置计时器(可能会有判别时刻归零,完毕视频)
  • 了解概况:获取广告 ID,设置按钮监听,保存广告概况 url
  • 暂停键:保留当时广告播映方位

……

4.1.2 剖析持续时刻

自己挑选剩余时刻作为破解进口,经过开发者帮手查到显现时刻的资源 ID 是 R.id.account_ads_time_pre_ad,查找资源ID可得三处引证该资源。

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

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

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

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

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

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

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

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

几个重要的函数剖析:

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

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

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

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

4.2 总结

4.2.1 破解广告技巧

  • 关于破解视频广告或其他广告,都能够经过获取广告的相关控件,剖析函数的调用逻辑次序,定位到要害类,剖析类,找到要害函数。
  • 针对杂乱客户端,尽量不采用要害字查找的方式去破解,由于杂乱的客户端代码都是有规划思维的,而且大概率做了混淆,无法轻易经过要害字符串进行定位,能够测验经过资源 ID 进行定位。
  • 进步英文水平,如 player 代表播映器、Ad Duration 代表广告持续时刻等,破解的首要任务便是看懂代码,特别是关于混淆过的代码,那些没有混淆过的函数名、变量名便是破解的要害,只有看懂才有机会能猜到要害点。

4.2.2 扩展:proxy署理类

剖析代码后发现,广告的生成、调用、装备大部分都是在 QYMediaPlayerProxy 类中完结的,而且播映器的中心功用也有一部分在署理类中调用。

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

  1. 能够过滤和操控广告流量,例如阻挠一些恶意或不受欢迎的广告,以及进步广告拜访速度和可靠性。
  2. 在特别情况下,广告服务器可能会要求运用特定的署理服务器或 IP 地址进行广告恳求。这时候,动态署理就能够被用来完成这些特别的网络拜访要求,保证广告恳求能够成功发送和接收。

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

android 广告 SDK 原理流程图

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

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