广大的开发者吃糠咽菜开发了一两款 APP,获取了一些流量后自然就会想到流量变现,一般情况下大家会选择到百度联盟或者 Google Admob 这些广告服务提供商注册开发者账号,在自己的 APP 里面加上几行广告位代码,集成他们的广告 SDK 包。然后谋事在人,成事在天,只有每天看报表查查收益。
也有一些开发者为了能够提高自己的变现水平,同时接入了好几家的 sdk,哪家出的收益高一点我就出哪家的广告,但是这样做也不是没有代价的,一是包的体积会增加不少,对下载成功率影响很大,二是市面上的广告 SDK 也良莠不齐,有些 SDK 在可能在后台会做一些“高级”工作,轻则导致用户流失重则导致开发账号被禁。
本文的目的就是告诉你一款广告 SDK 在我们的系统里面到 底会做什么以及应该做什么,知道了这些的话,聪明的开发者就可以更好的优化自己的广告获取更多的收入,甚至可以根据这些步骤自己开发一款自己的广告 SDK,用于制作自己的广告投放系统。
文末还有福利,介绍一种不用开发和对接就可以管理自己广告资源的方法。
1 SDK 工作流程
一般情况下移动广告 SDK 的大致工作流程如下(贴不了图只能外链): http://www.tietuku.com/bb6486fc59e1ea5a
如上图所示,其实广告 SDK 充当了一个中间代理角色,它负责 App 的事件处理,向服务器发送广告请求,渲染广告创意,上报广告展示事件等等,开发者不需要自己和广告服务器进行交互,不需要自己渲染广告位,不需要自己处理事件,极大的简化了开发的复杂度,下面我们详细讲解每一个步骤:
1.1 广告位展示机会
移动广告位有多种多样的展示形式,应用使用的不同时机可以展示不同形式的广告,一般常用的广告展示机会是用户打开 App 是加载启动页广告,玩游戏暂停的时候进入插屏,在页面顶部或者底部加载横幅广告等。广告位设计的原则是清楚的展示广告创意信息,并且不至于影响用户体验。最近原生广告比较流行,原因就是原生广告可以和应用本身内容的上下文保持一致不会显得非常突兀。
1.2 系统软硬件信息获取
SDK 获取适当系统信息有助于精准投放广告以及广告反作弊和广告计费,比如获取用户手机型号,系统软件版本,IP,MAC,IMEI 等等,有些 SDK 会获取用户的隐私信息,比如获取用户联系人通讯录,账号信息,手机号等等,这些我们是不提倡的。
获取这些适当的信息需要增加一些系统权限: <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.READ_PHONE_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
下面是获得一些系统软硬件系统的代码:
public class DeviceInfoUtil {
private Activity activity;
public DeviceInfoUtil(Activity activity) {
this.activity = activity;
}
private String getIP() {
try {
Context context = activity.getApplicationContext();
NetworkInfo info = ((ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
if (info != null && info.isConnected()) {
if (info.getType() == ConnectivityManager.TYPE_MOBILE) {//当前使用 2G/3G/4G 网络
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
NetworkInterface intf = en.nextElement();
for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
InetAddress inetAddress = enumIpAddr.nextElement();
if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
return inetAddress.getHostAddress();
}
}
}
} else if (info.getType() == ConnectivityManager.TYPE_WIFI) {
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
String ipAddress = intIP2StringIP(wifiInfo.getIpAddress());
return ipAddress;
}
}
} catch (Exception e) {
return "";
}
return "";
}
private String getAppversion() {
PackageManager pm = activity.getApplicationContext().getPackageManager();
try {
PackageInfo pi = pm.getPackageInfo(activity.getPackageName(), 0);
String appVersion = pi.versionName;
return appVersion;
} catch (Exception e) {
return "1.0";
}
}
private String intIP2StringIP(int ip) {
return (ip & 0xFF) + "." +
((ip >> 8) & 0xFF) + "." +
((ip >> 16) & 0xFF) + "." +
(ip >> 24 & 0xFF);
}
private String getCurrentUserAgent() {
try {
String userAgent = System.getProperty("http.agent");
return userAgent;
} catch (Exception e) {
return "";
}
}
private String getPhoneIMEI() {
try {
TelephonyManager mTm = (TelephonyManager) activity.getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
String imei = mTm.getDeviceId();
return imei;
} catch (Exception e) {
return "";
}
}
private String getMacAddress() {
try {
String macAddress = null;
StringBuffer buf = new StringBuffer();
NetworkInterface networkInterface = null;
networkInterface = NetworkInterface.getByName("eth1");
if (networkInterface == null) {
networkInterface = NetworkInterface.getByName("wlan0");
}
if (networkInterface == null) {
return "02:00:00:00:00:02";
}
byte[] addr = networkInterface.getHardwareAddress();
for (byte b : addr) {
buf.append(String.format("%02X:", b));
}
if (buf.length() > 0) {
buf.deleteCharAt(buf.length() - 1);
}
macAddress = buf.toString();
return macAddress;
} catch (Exception e) {
return "";
}
}
private Location getLocation() {
LocationManager locationManager = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE);
String locationProvider;
//获取所有可用的位置提供器
List<String> providers = locationManager.getProviders(true);
if (providers.contains(LocationManager.GPS_PROVIDER)) {
//如果是 GPS
locationProvider = LocationManager.GPS_PROVIDER;
} else if (providers.contains(LocationManager.NETWORK_PROVIDER)) {
//如果是 Network
locationProvider = LocationManager.NETWORK_PROVIDER;
} else {
return null;
}
//获取 Location
Location location = locationManager.getLastKnownLocation(locationProvider);
if (location != null) {
//不为空,显示地理位置经纬度
return location;
} else {
return null;
}
}
private String getAndroidid() {
try {
return Settings.System.getString(activity.getContentResolver(), Settings.System.ANDROID_ID);
} catch (Exception e) {
return "";
}
}
private String getCarrier() {
try {
TelephonyManager telManager = (TelephonyManager) activity.getSystemService(Context.TELEPHONY_SERVICE);
String operator = telManager.getSimOperator();
return operator;
} catch (Exception e) {
return "";
}
}
private Integer getNetwork() {
try {
int network = 0;
ConnectivityManager connectivity = (ConnectivityManager) activity.getApplicationContext()
.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivity == null) {
return network;
}
NetworkInfo networkInfo = connectivity.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isConnected()) {
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
network = 1;
} else if (networkInfo.getType() == ConnectivityManager.TYPE_MOBILE) {
String _strSubTypeName = networkInfo.getSubtypeName();
// TD-SCDMA networkType is 17
int networkType = networkInfo.getSubtype();
switch (networkType) {
case TelephonyManager.NETWORK_TYPE_GPRS:
case TelephonyManager.NETWORK_TYPE_EDGE:
case TelephonyManager.NETWORK_TYPE_CDMA:
case TelephonyManager.NETWORK_TYPE_1xRTT:
case TelephonyManager.NETWORK_TYPE_IDEN: //api<8 : replace by 11
network = 2;
break;
case TelephonyManager.NETWORK_TYPE_UMTS:
case TelephonyManager.NETWORK_TYPE_EVDO_0:
case TelephonyManager.NETWORK_TYPE_EVDO_A:
case TelephonyManager.NETWORK_TYPE_HSDPA:
case TelephonyManager.NETWORK_TYPE_HSUPA:
case TelephonyManager.NETWORK_TYPE_HSPA:
case TelephonyManager.NETWORK_TYPE_EVDO_B:
case TelephonyManager.NETWORK_TYPE_EHRPD:
case TelephonyManager.NETWORK_TYPE_HSPAP:
network = 3;
break;
case TelephonyManager.NETWORK_TYPE_LTE:
network = 4;
break;
default:
//中国移动 联通 电信 三种 3G 制式
if (_strSubTypeName.equalsIgnoreCase("TD-SCDMA") || _strSubTypeName.equalsIgnoreCase("WCDMA") || _strSubTypeName.equalsIgnoreCase("CDMA2000")) {
network = 3;
} else {
network = 0;
}
break;
}
}
}
return network;
} catch (Exception e) {
return 0;
}
}
private String getMD5(String s) {
char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {
byte[] btInput = s.getBytes();
MessageDigest mdInst = MessageDigest.getInstance("MD5");
mdInst.update(btInput);
byte[] md = mdInst.digest();
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private String getPackageList()
{
ArrayList<String> al = new ArrayList<String>();
PackageManager packageManager= activity.getPackageManager();
List<PackageInfo> list=packageManager.getInstalledPackages(0);
for (PackageInfo p:list) {
int flags=p.applicationInfo.flags;
if((flags & ApplicationInfo.FLAG_SYSTEM)==0) {
al.add(p.applicationInfo.packageName);
}
}
StringBuilder sb = new StringBuilder("");
for(String s:al)
{
if(sb.toString().equals(""))
{
sb.append(s);
continue;
}
sb.append(",").append(s);
}
return sb.toString();
}
public String getGaid()
{
String advertisingId ="";
try {
AdvertisingIdClient.AdInfo adInfo = AdvertisingIdClient
.getAdvertisingIdInfo(activity);
advertisingId = adInfo.getId();
return advertisingId;
} catch (Exception e) {
return advertisingId;
}
}
private boolean isGooglePlayAvaiable()
{
PackageManager pm = activity.getPackageManager();
boolean app_installed = false;
try {
PackageInfo info = pm.getPackageInfo("com.android.vending", PackageManager.GET_ACTIVITIES);
String label = (String) info.applicationInfo.loadLabel(pm);
app_installed = (!TextUtils.isEmpty(label) && label.startsWith("Google Play"));
} catch(PackageManager.NameNotFoundException e) {
app_installed = false;
}
return app_installed;
}
}
1.3 广告请求与检索
在获取了系统相关的信息后 SDK 就会去广告检索服务器请求广告了,请求的内容包括:
SDK 会给每个广告请求生成一个全局唯一请求 ID,系统中后续所有的事件的归因都会根据这个全局唯一请求 ID 来进行。
为了检查广告请求的有效性,广告系统会为开发者的应用分配一个密钥,SDK 会使用特定的算法根据这个密钥算出一个加密 Token,服务器在收到广告请求后会根据这个 Token 反算来验证是否是你的正常的广告请求。
SDK 之前收集的系统软硬件信息。
广告请求示例:
{
"version": "2.0",
"time": "1414764477867",
"token": "2c38ec54777641d9687aa8b65e7fa621",
"reqid": "2c38ec54777641d9687aa8bgrefef343",
"appid": "100002",
"adspotid": "100000433",
"impsize": 1,
"ip": "58.53.67.42",
"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 9_2 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13C75",
"lat": 31.167,
"lon": 112.582,
"sw": 1080,
"sh": 1920,
"ppi": 428,
"make": "HUAWEI",
"model": "HUAWEI Y600-U00",
"os": 1,
"osv": "7.1",
"imei": "862033031965161",
"mac": "38:bc:1a:c7:3b:11",
"androidid": "69f2d801e804f908",
"idfa": "E5C21290-B2D4-46D4-B9B7-37B59DD78684",
"carrier": "46000",
"network": 2,
"devicetype": 1,
"res_type": 0,
}
SDK 和广告检索服务器请求以及应答格式,可以是基于 restful 接口通过 json 传输,也可以使用 protobuf 自己定义序列化方式,前一种易读易升级,后一种传输编解码效率高,但是可读性和扩展性会差一些。
广告服务器在收到 SDK 发出的请求后,会根据请求中的软硬件信息,广告主的定向需求等,通过一定的算法选择一个合适的创意返回给 SDK,这是广告精准定向与智能大数据算法的范畴。
广告相应的信息中会包含广告素材信息,广告语,跳转链接,展示上报链接,点击上报链接等等信息。
广告响应示例:
{
"code": 200,
"msg": "SUCCESS",
"imp": [
{
"impid": "60a9b20ce5ec11e5af66a45e60c539c5",
"win_price": 20,
"adid": "103204",
"aduser": "43435",
"compid": "20031",
"crid": "100345",
"image": [
{
"type": 301,
"iurl": "http://js.snmi.cn/images/zufang1.png"
},
{
"type": 101,
"iurl": "http://js.snmi.cn/images/zufang2.png"
}
],
"word": [
{
"type": 1,
"text": "hello world"
},
{
"type": 2,
"text": "hello shuttle"
}
],
"video": [
{
"type": 901,
"duration": 15,
"bitrate": 30.8,
"vurl": "http://xxx.com/video.mp4"
}
],
"link": "http://www.google.com/click",
"deeplink": "http://www.google.com/click",
"action": 1,
"adsource": "广告",
"logo": "http://js.snmi.cn/images/logo.png"
"imptk": ["http://displayreport1", "http://displayreport2"],
"clicktk": ["http://clickreport1", "http://clickreport2"],
"playtk": ["http://playvideoreport1", "http://playvideoreport2"],
"ext": {
}
}
]
}
1.4 广告位渲染
广告返回结果中已经包含了广告创意的素材信息,对于一些非原生的常规的广告位比如:横幅,开屏,插屏等 SDK 会负责渲染,而原生的广告的形式需要和 App 的内容相适应,开发者根据 SDK 提供的接口自己来渲染。
1.5 广告事件处理与事件上报
广告的返回结果中已经包含了相应的事件上报地址数组,在相应的事件发生的时候 SDK 会负责请求相应的地址,比如广告点击后 SDK 就会请求 Clicktk 中的点击事件上报链接,事件服务器收到请求就会记录一次点击。
2 SDK 架构设计
一般情况下 SDK 的架构如以下类图所示(贴不了图只能外链): http://www.tietuku.com/41af563d68efeb3c
SDK 中包含了一系列的广告位类,比如 BannerAdspot,应用 AppActivity 创建广告位时,会实例化相应的广告位类,并且实例化广告位的事件监听器并注入监听器,广告位事件监听器的作用是在广告位状态发生变化时可以在应用中做一些必要的处理逻辑,比如打印日志记录,切换广告位的可视状态等。
BayesBanner bayesBanner = new BayesBanner(this,"10000314","100139","xxxxxxxx", BayesAdSize.SIZE_300x250);
RelativeLayout.LayoutParams rbl = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,RelativeLayout.LayoutParams.WRAP_CONTENT);
rbl.addRule(RelativeLayout.CENTER_HORIZONTAL);
rl.addView(bayesBanner,rbl);
//注入广告监听器
bayesBanner.setListener(new BayesBannerListener() {
@Override
public void onAdReady() {
//开发者可以实现自己的事件逻辑的时机
System.out.println("get Ad Ready");
}
@Override
public void onAdShow() {
System.out.println("get Ad Show");
}
@Override
public void onAdClick() {
System.out.println("get Ad Click");
}
@Override
public void onAdFailed() {
System.out.println("get Ad Failed");
}
@Override
public void onAdShowReportOk() {
System.out.println("get Ad Show Repored OK");
}
@Override
public void onAdShowReportFailed() {
System.out.println("get Ad Show Report failed");
}
@Override
public void onAdClickReportOk() {
System.out.println("get Ad Click Report OK");
}
@Override
public void onAdClickReportFailed() {
System.out.println("get Ad Click Report failed");
}
});
广告位类都会实现 Adspot 接口,Adspot 接口定义了通用的方法。
广告位包含了 Adservice 类实例,它是 SDK 中的核心类,它实现了 SDK 中通用的方法,比如,收集软硬件信息,请求广告,下载图片,下载软件包,上报事件。
在 AdService 类中所有的网络请求都是在子线程中通过异步方式完成的,不然会导致主线程卡死,同时网络请求是不可靠的,所有网络请求都有超时时间限制,超时会释放系统的资源。
网络请求示例:
private void requestAd(JSONObject jsonObject) {
try {
URL url = new URL(BayesSdkConfig.postUrl);
// 建立 http 连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 设置允许输出
conn.setDoOutput(true);
conn.setDoInput(true);
// 设置不用缓存
conn.setUseCaches(false);
// 设置传递方式
conn.setRequestMethod("POST");
// 设置文件字符集:
conn.setRequestProperty("Charset", "UTF-8");
//设置超时时间
conn.setConnectTimeout(1000);
//转换为字节数组
byte[] data = (jsonObject.toString()).getBytes();
// 设置文件长度
conn.setRequestProperty("Content-Length", String.valueOf(data.length));
// 设置文件类型:
conn.setRequestProperty("contentType", "application/json");
// 开始连接请求
conn.connect();
OutputStream out = conn.getOutputStream();
// 写入请求的字符串
out.write((jsonObject.toString()).getBytes());
out.flush();
out.close();
if (conn.getResponseCode() == 200) {
BufferedReader in = null;
String result = "";
in = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
Message msg = new Message();
msg.what = AD_RECEIVED;
msg.obj = result;
handler.sendMessage(msg);
} else {
System.out.println("无广告返回");
System.out.println(conn.getResponseMessage());
Message msg = new Message();
msg.what = AD_REQUEST_ERROR;
msg.obj = conn.getResponseMessage();
handler.sendMessage(msg);
}
} catch (Exception e) {
Message msg = new Message();
msg.what = AD_REQUEST_ERROR;
msg.obj = "广告请求发生异常";
handler.sendMessage(msg);
}
}
3 点击事件处理
当用户点击广告位后,就会触发点击事件处理的逻辑,后续的处理逻辑一般分为两大类,一种是落地页跳转,一种是软件包下载,通用的方法是使用系统默认的链接打开方式:
Intent intent = new Intent();
intent.setData(Uri.parse(link));
intent.setAction(Intent.ACTION_VIEW);
this.appContext.startActivity(intent);
对于下载类的广告可以通过多线程下载器进行下载,可以支持多线程和断点续传,提高下载的成功率:
mDownloadManager = (DownloadManager) mContext.getSystemService(DOWNLOAD_SERVICE);
// apkDownloadUrl 是 apk 的下载地址
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(link));
// 获取下载队列 id
enqueueId = mDownloadManager.enqueue(request);
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
long downloadCompletedId = intent.getLongExtra(
DownloadManager.EXTRA_DOWNLOAD_ID, 0);
// 检查是否是自己的下载队列 id, 有可能是其他应用的
if (enqueueId != downloadCompletedId) {
return;
}
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(enqueueId);
Cursor c = mDownloadManager.query(query);
if (c.moveToFirst()) {
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) {
bs.reportDownloadComplete();
// 获取下载好的 apk 路径
String uriString = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME));
// 提示用户安装
promptInstall(Uri.parse("file://" + uriString));
}
}
}
};
// 注册广播, 设置只接受下载完成的广播
mContext.registerReceiver(receiver, new IntentFilter(
DownloadManager.ACTION_DOWNLOAD_COMPLETE));
4 结论
看到这里,大家已经可以轻松对接任何一家网盟。不过,市面上的广告平台和 SSP 多如牛毛,变现水平层次不齐,每一家都对接测试一遍实在不划算。这里介绍一种更简单方便的办法——直接使用绿色免费第三方工具,比如倍业科技的倍联 Blink 平台 www.bayescom.com ,打包了市面上 30 多家网盟接口,还有直客投放等模块,功能很强大,小伙伴们可以研究下。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.