ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android广告SDK原理与核心源码解析:从请求链路到缓存策略

Android广告SDK原理与核心源码解析:从请求链路到缓存策略 广告SDK这个东西圈外人听着可能觉得就是个“往App里塞广告的库”但真正在Android端做过商业化或聚合SDK的人都知道它其实是一个非常精巧的客户端-服务端联动系统。早期很多开发者自己接广告就是调一个SDK的load方法拿到广告就show拿不到就拉倒但一旦你开始关心填充率、展示率、eCPM、超时策略、缓存池命中率这些东西就会发现广告SDK的底层设计直接决定了你的变现天花板。这篇我就从Android广告SDK的原理出发把这些年实际调试、接入、甚至自己封装聚合层时踩过的坑一起讲清楚核心的类设计和源码片段会贴在对应位置。如果你正准备自研一套广告SDK或者只是想把主流的穿山甲、AdMob等SDK吃得透一点这篇内容应该能帮你在原理层面把链路捋顺。1. 广告SDK的功能拆解与模块设计1.1 广告SDK在App里到底扮演什么角色市面上所有广告SDK不管穿山甲还是AdMob本质上都是在做一件事情把App的流量变成广告平台的库存再把广告平台的预算变成开发者的收入。但这件事听起来简单做起来却涉及网络请求、渲染、事件上报、计费、防作弊、隐私合规等一大堆环节。从一个Android工程的角度看广告SDK是以aar依赖的形式集成进App的它需要做的第一件事就是隐藏掉“广告平台”和“App宿主”之间的复杂度。对App开发者来说SDK只暴露几行API比如AdManager.init、BannerAdView.loadAd、InterstitialAd.show但对SDK内部来说每次加载广告都要完成拼接请求参数、发起网络请求、解析服务端返回的竞价结果、按类型创建广告对象、渲染到对应的容器、注册生命周期回调、上报曝光和点击。这里有个很多人忽略的点广告SDK并不是一个纯粹的前端组件它其实是一个“瘦客户端胖服务端”的系统。客户端SDK的职责是采集信息、发起请求、渲染和上报真正的广告匹配、出价、频控、反作弊绝大多数逻辑都在服务端完成。客户端做得太重包体积和崩溃率都会上去客户端做得太轻又没法保证广告加载速度和展示成功率。所以SDK内部的模块划分本质上是在平衡“端上体验”和“服务端决策”的边界。1.2 核心模块职责划分从源码角度拆解一个完整的Android广告SDK通常会分成以下几个核心模块入口与初始化模块负责读取AndroidManifest中的配置、初始化上下文、拉取远程配置、注册生命周期监听通常以Application的attachBaseContext或者ContentProvider作为初始化触发点。广告位管理模块把开发者配置的广告位IDslotId和广告类型Banner、插屏、激励视频、原生、开屏绑定在一起管理广告位的状态机。网络请求模块负责组装请求参数、做签名、加密、序列化然后通过OkHttp或自研网络栈发送到广告服务端。这里通常还会做超时控制和错误重试。竞价与排序模块在聚合SDK里特别重要负责把多个渠道SDK返回的价格eCPM做排序决定最终展示哪一家。渲染模块针对不同类型的广告位创建对应的View或Activity容器把素材图片、视频、标题、描述渲染出来。事件上报模块曝光、点击、关闭、计费回调等事件的收集和上报通常要支持批量上报和离线补偿。缓存模块为了提升加载速度SDK会预加载广告并在内存或磁盘里缓存缓存命中后直接秒开。结构清楚了下面我挑几个最核心的环节展开讲源码层面也会贴上简化版本的实现思路。2. 广告请求链路一次完整请求是怎么被组装和发出的2.1 请求参数的组装与协议设计广告请求的发起是SDK最敏感的一环因为这一环直接决定了服务端能不能给你匹配到合适的广告。客户端能提供的参数包括设备信息Android ID、OAID、GAID、设备型号、系统版本、网络状态运营商、网络类型、IP、上下文信息包名、版本号、广告位ID、页面名称、用户行为信息近期点击过的广告位、历史展示次数。很多人问为什么服务端能精准到“知道我这个用户就是不喜欢看游戏广告”其实靠的就是这套请求参数的长期积累和用户画像建模。从源码角度看一次广告请求本质上就是组装一个包含上述信息的结构化对象然后再序列化。早期很多SDK直接用JSON明文传输但后来因为安全和防作弊的要求普遍改成Protocol Buffers加上加密签名。我这里给一个简化版的请求参数构造代码示例public class AdRequestBuilder { private String slotId; private String appId; private String deviceId; // 原始设备标识内部加密后上报 private String oaid; private int adType; // 1:Banner 2:插屏 3:激励视频 4:原生 5:开屏 private int width; private int height; private String customs; // 开发者自定义参数比如内容分类 public String buildParam() { JSONObject json new JSONObject(); try { json.put(slot_id, slotId); json.put(app_id, appId); json.put(device_id, EncryptUtil.encrypt(deviceId)); json.put(oaid, oaid); json.put(ad_type, adType); json.put(ad_size, width x height); json.put(customs, customs); json.put(timestamp, System.currentTimeMillis()); json.put(sign, genSign(json)); } catch (JSONException e) { e.printStackTrace(); } return json.toString(); } }注意这里的设备标识都是先加密再上报一方面是为了防篡改另一方面也是隐私合规的要求。sign的生成一般是把请求参数按字典序排序后拼接盐值做MD5或HMAC服务端会校验签名签名不通过直接拒绝请求。2.2 服务端决策与流量分配逻辑请求发到服务端之后服务端做的事情远比客户端复杂。首先是流量识别判断这个请求来自哪个App、哪个广告位、什么样的用户然后是库存筛选从广告池里找出所有符合条件的广告计划最后是竞价或排序决定返回哪条广告以及出价多少。这里涉及到一个关键的机制Waterfall瀑布流和实时竞价Bidding。很多开发者对Waterfall不陌生它的逻辑就是服务端预先配置好一组广告源的优先级顺序按照优先级从高到低依次请求前面的广告源没有填充就请求下一个。这种方式实现简单、稳定但对服务端的时效性要求不高很多历史广告SDK都是这么做的。而Bidding也就是业内人士常说的In-App Bidding或实时竞价则是在请求到达服务端之后直接把这次请求同时广播给多个广告源让它们各自出价价高者得。Bidding的好处是让每一次流量都能卖到最高价不会因为优先级配置不合理而把高价值的请求浪费在低eCPM的广告源上。从SDK源码角度”请求-返回”的数据模型一般长这样public class AdResponse { private String requestId; // 服务端生成的请求ID用于后续追踪 private int code; // 0表示拉取成功非0表示失败原因 private String msg; // 失败原因描述 private ListAdMaterial ads; // 广告物料列表 private long expireTime; // 广告过期时间戳 } public class AdMaterial { private String adId; // 广告创意ID private String title; private String desc; private String imageUrl; private String videoUrl; private String clickUrl; // 点击跳转地址 private String impressionUrl; // 曝光上报地址 private int price; // 价格eCPM private String schema; // 唤起App的schemadeeplink用 }AdResponse是整个广告SDK的“命根子”后续所有操作——展示、上报、计费——都围绕这个响应对象展开。2.3 请求回调模型为什么有Load和Show两个阶段广告SDK和普通网络请求最大的区别在于一次网络请求成功不代表广告可以立刻展示。这里面有预加载、缓存、正确时机判断等复杂逻辑。所以SDK设计的回调模型通常分为两类加载回调Load回调onAdLoaded表示广告已经准备好随时可以展示。这个回调的触发时机一般是SDK内部拿到AdResponse并成功解析、校验完物料之后。展示回调Show回调onAdShown表示广告真正出现在用户屏幕上。对于插屏、激励视频这种需要手动show的广告展示回调一般在调用show方法、广告视图成功attach到Window之后才触发。为什么要拆开因为一个广告从“拉取成功”到“成功展示”中间可能出现很多意外比如用户退出了页面、容器被回收、视频文件加载失败、广告过期等等。如果SDK在拉取成功那一刻就把曝光上报、计费回调全部触发那广告主就亏大了。所以广告行业的规则是只有真正展示到了用户眼前才计算曝光和费用。这个设计在源码里的体现就是广告对象内部有个状态机public abstract class BaseAd { public static final int STATE_IDLE 0; public static final int STATE_LOADING 1; public static final int STATE_LOADED 2; public static final int STATE_SHOWING 3; public static final int STATE_CLOSED 4; private int state STATE_IDLE; public final void load() { changeState(STATE_LOADING); doLoad(); } protected abstract void doLoad(); public final void show() { if (state ! STATE_LOADED) { throw new IllegalStateException(广告未加载完成不能展示); } changeState(STATE_SHOWING); doShow(); } protected abstract void doShow(); private void changeState(int newState) { this.state newState; dispatchStateEvent(newState); } }这样的状态机设计是广告SDK源码里最重要的骨架后面所有模块都是在给这个状态机添加细节。3. 核心源码实现加载、缓存、渲染与生命周期3.1 SDK入口与初始化机制一个广告SDK能不能稳定工作初始化这步非常关键。常见的初始化触发方式有两种显式初始化开发者在Application的onCreate里调用AdManager.init(context, appId)。这种方式优点是开发者对初始化时机可控缺点是容易忘。自动初始化SDK通过Manifest里注册ContentProvider利用App启动时机自动完成初始化开发者不用写一行代码。缺点是会增加启动耗时。现在主流的SDK普遍采用“双保险”策略既提供显示初始化API也提供一个懒加载AutoInitProvider如果开发者调用了初始化就用开发者的如果没调用就在首次加载广告时用默认配置初始化。初始化到底做了什么源码层面核心就几件事public class AdManager { private static volatile AdManager instance; private Context appContext; private AdConfig config; private AdLoader loader; private AdCache cache; private Tracker tracker; private AdManager() { } public static AdManager get() { if (instance null) { synchronized (AdManager.class) { if (instance null) { instance new AdManager(); } } } return instance; } public void init(Context context, AdConfig config) { this.appContext context.getApplicationContext(); this.config config; // 初始化网络模块、缓存模块、上报模块 this.loader new AdLoader(appContext, config); this.cache new AdCache(appContext); this.tracker new Tracker(appContext); // 读取远程配置例如广告位超时时间、缓存策略 RemoteConfig.getInstance().load(); // 注册前后台切换监听清理过期缓存 AppLifecycleObserver.getInstance().register(appContext); } }注意这里有个很重要的细节缓存的是ApplicationContext不是Activity的Context。用Activity的Context初始化的SDK一旦发生内存泄漏整页Activity都回收不掉这个问题在早期很多自研SDK里特别常见。3.2 广告加载器与缓存池设计广告加载器AdLoader是SDK里最核心的执行器它负责根据广告位ID找到对应的广告源配置组装请求参数并发送请求拿到响应之后做解析、校验、创建广告对象把广告对象放入缓存池或者直接回调给开发者。为了提高广告展示速度几乎所有商业SDK都会做预加载和缓存。常见的缓存策略是在开发者创建广告位对象的那一刻SDK后台就自动发出一次请求把返回的广告对象缓存起来等开发者真正调用load的时候先从缓存池取缓存没有命中再发网络请求。这里有一个值得借鉴的设计缓存池分两类一类是位级缓存per-slot cache一类是类型级缓存type-level cache。位级缓存的意思是每个广告位ID独立缓存比如开屏广告位A和开屏广告位B各缓存各的类型级缓存则允许跨广告位共享比如Banner广告位A缓存了广告Banner广告位B也可以复用。类型级缓存利用率高但容易串场所以现在主流做法是位级缓存为主、类型级缓存为辅。缓存池的过期时间也要特别谨慎。广告物料里通常带一个expireTime有的甚至只有几分钟过期之后再展示会报“广告过期”错误。缓存池会启动一个定时任务定期把过期广告清理掉避免脏数据占内存。public class AdCache { private final MapString, BaseAd slotCache new ConcurrentHashMap(); private final Handler handler new Handler(Looper.getMainLooper()); private static final long CACHE_CLEAN_INTERVAL 60 * 1000L; public void put(String slotId, BaseAd ad) { if (ad ! null ad.isValid()) { slotCache.put(slotId, ad); } } public BaseAd take(String slotId) { BaseAd ad slotCache.remove(slotId); if (ad ! null ad.isValid()) { return ad; } return null; } public void scheduleClean() { handler.postDelayed(new Runnable() { Override public void run() { long now System.currentTimeMillis(); IteratorMap.EntryString, BaseAd iterator slotCache.entrySet().iterator(); while (iterator.hasNext()) { BaseAd ad iterator.next().getValue(); if (ad.getExpireTime() now) { iterator.remove(); } } handler.postDelayed(this, CACHE_CLEAN_INTERVAL); } }, CACHE_CLEAN_INTERVAL); } }3.3 渲染模块与广告类型适配广告SDK的渲染模块是直接面向用户的部分也是最容易出现兼容性问题的部分。不同类型的广告渲染容器完全不一样Banner广告是一个View嵌入到App的布局里通常需要指定宽高如320x50SDK返回的是一段HTML或者原生View组件。插屏广告是全屏或者半屏的Dialog或ActivityApp调用show时弹出来。激励视频广告是全屏视频播放完可以给用户奖励一般通过全屏Activity承载SDK内部管理播放器。原生广告素材图片、标题、描述和View分离开发者在RecyclerView的Adapter里自己绑数据SDK只提供数据和控件模板。开屏广告本质上是App闪屏页上的一个半透明容器SDK拿到素材后渲染在这个容器上通常3到5秒后自动关闭或点击跳过。渲染模块的源码设计通常也是模板方法模式每个广告类型继承同一个BaseAdRenderpublic abstract class BaseAdRender { protected Context context; protected AdMaterial material; protected View adView; public BaseAdRender(Context context, AdMaterial material) { this.context context; this.material material; } // 子类实现具体的视图构建逻辑 public abstract View createAdView(ViewGroup parent); // 子类实现点击、曝光等事件的绑定 public abstract void bindAdAction(View view); protected void registerImpression(View view) { view.getViewTreeObserver().addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { Override public void onGlobalLayout() { // 通过可见性判断是否真正曝光 if (view.isShown() view.getVisibility() View.VISIBLE) { notifyImpression(); view.getViewTreeObserver().removeOnGlobalLayoutListener(this); } } } ); } }这里有个很多人忽略的细节曝光检测不能用view.isShown()这一个条件就判断完成因为还有遮挡、透明度的场景。严格一点的SDK会计算曝光面积比例比如View的可见区域超过50%且持续超过1秒才算有效曝光这个逻辑往往在SDK的检测线程里定时扫描。3.4 生命周期管理与内存泄漏防治广告SDK的生命周期管理和普通页面不太一样。因为插屏和激励视频的show可能开启一个新的Activity所以SDK必须监听Activity的生命周期在onPause、onResume、onDestroy时做相应的处理。最常见的坑有两个第一个是Activity泄漏。早期接入广告SDK时如果一个插屏广告正在展示用户直接按Home键退回桌面这时候Activity可能被系统回收但广告对象还持有它的引用导致泄漏。解决办法是广告容器不要直接持有Activity的强引用而是通过Application层级的ActivityLifecycleCallbacks来管理。第二个是页面销毁后回调仍触发。很多开发者遇到过这种场景用户从一个页面跳到下一个页面上一个页面里的Banner广告还在加载加载完成后回调onAdLoaded但此时页面的View已经销毁如果SDK直接往这个View里塞广告轻则抛异常重则崩溃。所以商业SDK都会在回调之前做一次View有效性校验private boolean isViewAttached(View view) { return view ! null view.getParent() ! null view.getContext() ! null view.isAttachedToWindow(); }这个判断在源码里出现了很多次我建议你在自己写广告相关组件时也加上。4. 事件上报、计费与防作弊机制4.1 曝光点击上报链路广告计费模型里有两个核心事件曝光Impression和点击Click。曝光决定了一次广告展示是否计费CPM计费模式下点击则决定了是否产生点击费用CPC计费模式下。SDK的上报链路设计基本是客户端采集事件 - 批量打包 - 服务端校验 - 入库计费。为了降低网络IO压力SDK一般不会每个事件立刻上报而是先存在本地攒到一定数量例如10条或者每隔固定时间例如30秒批量上报一次。如果App退到后台会把未上报的事件先持久化到文件里等下次启动再补报。源码层面的实现大概是public class Tracker { private final ListEvent eventQueue new ArrayList(); private final ScheduledExecutorService executor Executors.newSingleThreadScheduledExecutor(); public void track(Event event) { eventQueue.add(event); if (eventQueue.size() BATCH_LIMIT) { flush(); } } private void flush() { ListEvent batch; synchronized (eventQueue) { batch new ArrayList(eventQueue); eventQueue.clear(); } // 发送到服务端失败则重新入队等待下次上报 boolean success sendEvents(batch); if (!success) { saveToDisk(batch); } } private void startPeriodicalFlush() { executor.scheduleWithFixedDelay(this::flush, 30, 30, TimeUnit.SECONDS); } }一个关键点是曝光上报必须在广告真正展示到屏幕上时触发而不是在SDK内部创建广告对象时触发否则会产生大量无效曝光影响广告主对渠道质量的评估严重的还会被平台判定为作弊。4.2 常见的防作弊手段广告行业最怕的就是作弊流量。客户端SDK层面能做的防作弊措施通常有这几类设备指纹收集设备硬件信息、系统属性、传感器列表生成一个稳定的设备指纹用于识别同一台设备是否在大量刷广告。点击行为校验记录点击坐标、点击间隔、滑动轨迹、陀螺仪数据判断点击是否来自真实用户。如果某个设备在非常短的时间内频繁点击广告SDK会直接丢弃后面的点击并标记异常。时间频控限制同一广告位在一定时间内的请求次数比如同一设备一小时内最多请求100次超出后由服务端直接拒绝。防二次打包校验App签名与广告平台记录的是否一致防止开发者改包刷量。源码层面通常用PackageManager拿到签名然后与服务端下发配置做对比。客户端SDK能做的其实有限真正强力的反作弊在服务端它会结合IP、设备指纹、行为序列做风控。但客户端SDK至少要做到“不给作弊素材留后门”比如数据加密、传输签名、禁止模拟器环境下加载真实广告测试模式除外。4.3 隐私合规与广告标识符这两年隐私合规是广告SDK必须处理好的底线问题。Android端有两个关键的广告标识符GAIDGoogle Advertising ID在海外版本的Google Play服务里提供用户可以在系统设置里“重置广告ID”或“退出个性化广告”。OAID匿名设备标识符国内Android生态的替代方案由中国信通院牵头制定移动安全联盟提供SDK。广告SDK拿到广告标识符后一般只用于广告定向和频控不能用来做用户画像关联。从源码角度SDK在初始化时会异步获取这两个ID获取不到时降级为使用加密后的设备指纹但绝不能直接去读IMEIAndroid 10之后也没权限了。合规这块我的建议是SDK内部不要无条件采集所有信息而是做两层开关。第一层是全局隐私开关App可以关闭个性化推荐第二层是字段级别的动态配置服务端可以远程下发“哪些字段可以采集哪些字段必须脱敏”。现在主流SDK的远程配置模块都是这个思路。5. 聚合SDK与竞价客户端如何管理多个广告源5.1 聚合SDK要解决的核心问题很多开发者的App不是只接一家广告平台而是接了好几家穿山甲、优量汇、AdMob、Mintegral……每一家SDK的包体积、初始化方式、回调协议都不一样。如果广告位要按优先级依次请求难道要让开发者自己在业务层写一堆if-else吗这就催生了聚合SDKMediation Platform的需求。聚合SDK的核心能力是用一个统一的广告位管理多个广告源自动完成请求、排序、回退和报表归因。聚合SDK在源码层面做得最漂亮的一件事是Adapter模式。每个广告源SDK对应一个AdapterAdapter实现统一的接口隐蔽掉各家SDK的差异。比如Banner广告的Adapter接口长这样public interface IBannerAdapter { void loadBannerAd(Context context, String slotId, int width, int height, AdCallback callback); void showBannerAd(ViewGroup container); void destroy(); }然后穿山甲有穿山甲的AdapterAdMob有AdMob的Adapter每个Adapter内部转调各家SDK自己的API。这样聚合层就能统一管理生命周期、缓存和回调开发者完全不用关心底层切换。5.2 Waterfall和Bidding在客户端的表现在聚合SDK内部一次广告请求的流程是这样的开发者调用聚合SDK的load方法聚合SDK先检查本地的Waterfall配置服务端动态下发确定当前广告位的广告源加载顺序按顺序依次触发各家Adapter的load方法第一个返回成功的广告源胜出回调给开发者如果全部失败则判定填充失败。换成Bidding模式则不同。聚合SDK会把这次请求同时送给所有接入的Bidding广告源让它们在服务端各自出价然后客户端SDK会对比各家Adapter返回的价格选择eCPM最高的那家展示。在源码层面Bidding模式关键是价格比较和字段透传。各家SDK返回的广告对象里都带一个price字段比如穿山甲返回2.5元、AdMob返回3.0元聚合SDK就会选择AdMob的广告去展示。这个过程必须保证公平公正否则广告源会有意见所以这里的核心代码会有大量的加锁和同步处理毕竟并发竞价的结果必须实时且一致。5.3 Adapter层的异常隔离聚合SDK还有一个隐藏的价值是异常隔离。使用聚合SDK的App不会因为某一家广告SDK的崩溃导致整个App崩溃。实现方式是在Adapter调用外面包一层try-catch同时用独立的HandlerThread执行各家SDK的回调。这样即使优量汇SDK在load时抛了个未捕获异常聚合层也能接住把它标记为失败再试下一家。这里有一个实际踩过的坑有一次线上App崩溃率飙升最终定位到是某家广告SDK在弱网环境下回调了两次onAdLoaded导致聚合SDK的排队逻辑里同一个广告被展示了两次。后来我们在Adapter层的回调入口做了去重判断if (callbackInvoked.getAndSet(true)) { return; // 防止重复回调 }这个技巧后来也被我用到了其他异步场景里算是广告SDK源码里顺带学到的一个通用经验。6. 接入与调试从联调到上线的完整指南6.1 集成步骤与初始化配置接入广告SDK对移动端工程师来说是家常便饭但真要做好还是有很多细节。第一步是引入依赖通常是用Gradleimplementation com.example.adsdk:adsdk-core:2.1.0 implementation com.example.adsdk:adsdk-banner:2.1.0第二步是配置AndroidManifest。除了必要的权限声明还要特别注意一点很多广告SDK要求App里配置Provider或者Activity如果忘记配置或者配置了错误的包路径运行时直接Crash。第三步是初始化。以我们自己的MiniAdSDK为例AdConfig config new AdConfig.Builder() .setAppId(your_app_id) .setDebug(BuildConfig.DEBUG) .setPersonalizedAdEnabled(true) // 是否允许个性化推荐 .setTestMode(true) // 联调环境开启上线前必须关闭 .build(); AdManager.get().init(this, config);初始化完成之后所有广告位的加载都是异步的所以要保证在Application里尽早初始化否则用户第一秒看到页面广告时可能还没初始化完。6.2 测试模式与调试技巧在联调阶段所有正规广告SDK都会提供测试模式。测试模式的好处是返回的广告都是测试素材不会产生真实计费可以在日志里看到详细的请求过程和失败原因可以模拟各种错误场景比如无填充、网络超时、素材解析失败。调试时最常用的手段是看SDK日志。商业SDK一般都会把关键事件用带TAG的Log输出比如AdSDK-Request、AdSDK-Render、AdSDK-Tracker根据这些日志可以定位到具体环节的问题。我个人的习惯是写一个小工具类把广告SDK的关键回调打印出来AdCallback callback new AdCallback() { Override public void onAdLoaded() { Log.d(TAG, onAdLoaded, time System.currentTimeMillis()); } Override public void onAdFailed(int errorCode, String message) { Log.d(TAG, onAdFailed, code errorCode , msg message); } Override public void onAdShown() { Log.d(TAG, onAdShown, time System.currentTimeMillis()); } Override public void onAdClicked() { Log.d(TAG, onAdClicked, time System.currentTimeMillis()); } };6.3 常见问题与排查思路速查结合过往我接广告SDK以及后来自己写广告SDK的经验整理一份高频问题排查表问题现象可能原因排查手段广告加载加载失败错误码401广告位ID校验失败检查AppId、SlotId是否有拼写错误加载成功但展示为空广告物料过期查看SDK返回的expireTime检查预加载时间点填充率极低设备测试模式未关闭确认测试模式已关闭切换到正式环境点击无反应广告源被风控检查clickUrl是否能正常访问排查是否触发频控App启动变慢初始化影响主线程确认是否在热启动路径上做了耗时初始化建议使用ContentProvider自动初始化放入后台线程插屏广告弹出后白屏素材渲染时机过早检查是否在onAdLoaded后立刻调用show建议延迟到渲染完成回调后再展示同一广告多次展示回调重复触发在Adapter层做去重保护视频广告播放失败网络弱场检查预加载素材大小是否过大考虑用SDK提供的视频降级开关还有一个非常容易被忽视的问题广告SDK的ProGuard/R8混淆规则。很多SDK内部用了反射和注解混淆后拿不到类名导致广告无法展示运行时还会报ClassNotFoundException。接入时一定要把SDK文档里的keep规则复制进proguard-rules.pro千万别省这一步。注意上线前务必走一遍“正式环境验证流程”在没开启测试模式的前提下用一个真实设备过一遍所有广告位的加载、展示、点击、关闭确认没有任何异常再提交审核。7. 从源码到实践我的一些额外心得写了这么多最后再分享一点我实际开发广告SDK时的个人体会这些不是文档里能看到的但踩过坑的都懂。第一个体会是广告SDK的代码风格必须保守。因为广告SDK运行在别人的App进程里它的稳定性直接影响到宿主App的用户体验。比如你SDK内部出现了主线程卡顿用户不会怪广告SDK只会说这个App卡。所以自研广告SDK时一定要严格限制主线程操作所有网络、解析、渲染尽量放到子线程只在回调给上层时切回主线程。第二个体会是回调时机必须用主线程统一管理。Android UI操作只能在主线程做但如果你的广告SDK底层从线程池回调onAdLoaded开发者在回调里创建View就会崩溃。所以我们自研SDK时会定一个铁律所有对外回调都post到主线程Handler再发出去。这个设计牺牲了一点性能但极大提升了接入方的稳定性。第三个体会是异常捕获要分模块做。广告SDK涉及网络、渲染、音视频播放、View树操这么多环节一个全局try-catch会掩盖问题但每个模块都try-catch又会影响代码可读性。折中方案是每个模块最外层入口加try-catch捕获后打日志、上报、标记模块降级同时保证主流程不受影响。第四个体会和收益相关广告的预加载和缓存策略比任何优化都对eCPM影响大。很多开发者的App填充率看起来正常但展示率很低原因就是广告加载到展示之间经过太久很多物料过期后不能用了。后来我们调整策略在页面onCreate时就预加载用户滑动到广告位附近时再取缓存展示率提升了将近20%。所以接入广告SDK时不要只看load和show这两个接口多研究一下缓存和预加载的时机收益是实实在在的。这篇文章里我讲的源码都是基于主流广告SDK的通用设计思路整理的简化版本真正的商用SDK在代码结构上会更加复杂比如会加入动态加载、多进程、插件化等机制但核心思想不会变一套稳定、高效、合规的广告SDK本质上就是对网络、资源、生命周期和用户体验的极致管理。希望这篇内容能帮你在接入或者自研广告SDK时少走一些弯路。
返回列表