Android 上玩转 DeepLink:如何最大程度的向 App 引流 (1),移动端 h5 开发 html 头部模版
实际上它在处理这个注解的时候生成了一个建造者模式里的 builder,然后向 管理中心 注册,说 自己(ArticleActivity)能处理/acticel/xxx
的子域名。
Scheme 的选择很重要:URL Scheme 唤醒
上面简述原理的时候说道了 Manifest 的声明,我们只声明了 android:scheme="http"
和 android:scheme="http"
, 但是实际上很多 App 还会用特定 scheme
的方式来唤起 App,例如在 iOS 早期没有 UniversalLink 的时候,大家这样来唤起。
像淘宝就会用 tbopen
的 scheme,例如 tbopen://item.taobao.com/item.htm?id=xxxx
,当你在网页点击链接以后,页面会创建一个隐藏的 iframe,用它来打开自定义 scheme 的 URL,浏览器无法响应时,向系统发送一个 Action 为 android.intent.action.VIEW
、Data 为 tbopen://item.taobao.com/item.htm?id=xxxx
的 Intent,如果 App 已经按照上述章节改造,那么系统将唤起 RouterActivity 并将 Intent 传递过去。
所以问题就来了:如何选取一个 URL Scheme 使得“浏览器无法响应”,所以你的 scheme 最好满足以下两个条件:
区别于其他应用:唯一性
区别于浏览器已经能处理的 scheme:特殊性
在我们上述假设的新闻 App 里,我们可以定义 scheme 为 zljnews
,那么在 URL Scheme 发送的 URL 将会是这样:
指向 id=123456 的新闻详情页:zljnews://news.zhoulujue.com/article/123456/指向 id=123457 的新闻专题页:zljnews://news.zhoulujue.com/story/123457/指向 id=123456 的新闻讨论页:zljnews://news.zhoulujue.com/article/123456/comments/
为了避免某些应用会预处理 scheme 和 host,我们还需要将 URL Scheme 的 Host 也做相应 更改:
指向 id=123456 的新闻详情页:zljnews://zljnews/article/123456/指向 id=123457 的新闻专题页:zljnews://zljnews/story/123457/指向 id=123456 的新闻讨论页:zljnews://zljnews/article/123456/comments/
这样的我们的 Manifest 里 RouterActivity 的声明要改为:
<activityandroid:name=".RouterActivty"android:theme="@android:style/Theme.Translucent.NTitleBar"><intent-filter android:autoVerify="true"><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><dataandroid:host="news.zhoulujue.com"android:pathPattern="/."android:scheme="http" /><dataandroid:host="news.zhoulujue.com"android:pathPattern="/."android:scheme="https" /></intent-filter><intent-filter><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="zljnews" /><data android:host="zljnews" /><data android:pathPattern="/.*" /></intent-filter></activity>
App Links 与 Universal Links,来自官方的方式
我们假设一个用例:用户在印象笔记里写了一篇笔记,笔记里有一个链接: `http://news.z
《Android 学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
houlujue.com/article/123456/`。 那么问题来了:用户点击以后,将会发生什么?
答案是:很大的可能是系统弹出一个对话框,列出若干个 App,问你想用哪一个打开。

这样体验其实不够好,因为用户路径变长了,转化率 将下降。所以我们应该尽可能去掉这个 对话框,其实上述章节说到了一个方法:将 http://news.zhoulujue.com/article/123456/
改为 zljnews://zljnews/article/123456/
,原理是我们选取了看起来"唯一性"的 scheme, 但是如果用户没有安装你的 App,这个体验就相当糟糕了,用户在点击以后将没有任何反应。
此时就需要 AppLinks 和 UniversalLinks 了,一言以蔽之,就是域名持有者向系统证明自己 拥有 news.zhoulujue.com
这个域名并且 App 属于自己,这样系统就会直接将 App 唤起 并把 intent 传递给 App。
如何配置 AppLinks 就不在赘述了,参考官方的教程。
App Links 实现的另一种方式
Facebook 在 2014 年的 F8 开发者大会上公布了 AppLinks 协议,在 Android 的 AppLinks 之前(Google I/O 15), 也是一种可行的“链接跳转 App”的方式。 这里也不在赘述细节,可以参考 Facebook 官方的介绍来实现,也特别简单:

非自己的代码怎么办
上面说了很多在网页中唤醒 App 的方式,但是这些都是建立在我们可以改页面 JS 等代码的前提下, 如果页面由第三方提供,举个例子,由广告主提供,表现方式是广告主提供一个落地页放在你的 App 里, 推动第三方去按照你的要求去改动他们的代码,可能比较困难,但是如果只是修改一下跳转链接就可以达到 唤起 App 的效果,这样性价比就比较高了。这个时候就需要 chrome 推荐的 intent scheme 了:
<a href="intent://zljnews/recipe/100390954#Intent;scheme=zljnews;package=com.zhoulujue.news;end"> Intent scheme </a>
如代码所示,scheme 填写的是我们上面假设的 scheme:zljnews
,保持一致。 package 填写 App 包名:com.zhoulujue.news
,参考Chrome官方 Intent 编写规范
微信里怎么办
众所周知,微信是限制唤起 App 的行为的,坊间流传着各种微信唤起的 hack,但总是不知道什么时候就被封禁了,这里介绍 微信官方的 正规 搞法:微下载链接:

如上图,知乎就使用了微下载来向知乎的 App 导流,这种方式 Android iOS 都是通用的,具体实现方式参考腾讯微信官方的文档。
优化 1:从网页到 App 的无缝体验
假设一个场景,用户访问 http://news.zhoulujue.com
阅读新闻时,被推荐下载了 App,此时安装完毕后打开 App 后,最好 的体验当然是帮用户打开他没有看完新闻,直接跳转到刚刚在网页版阅读的文章。 最佳实践是:在用户点击下载时,把当前页面的 URL 写到 APK 文件的 ZIP 文件头里,待用户下载安装完毕后,启动时去读取这个 URL,然后结合上面说到过的 Router,路由到新闻详情页。下面跟我来一步一步实现吧。
在网页上下载 APK 时:将路径写如 APK 的 ZIP 文件头里
将下面的 Java 代码保存为 WriteAPK.java
并用 javac 编译好。
import java.io.ByteArrayOutputStream;import java.io.File;import java.io.IOException;import java.io.RandomAccessFile;import java.nio.ByteBuffer;import java.nio.ByteOrder;import java.util.zip.ZipFile;
/**
Created by michael on 16/9/8.*/public class WriteApk {
public static void main(String[] args) {for (int i = 0; i < args.length; i++) {System.out.println(args[i]);}if (args.length < 2) {System.out.println("Wrong parameters! Usage : WriteApk path comment\n");}String path = args[0];String comment = args[1];writeApk(new File(path), comment);System.out.println("Complete! File lies in " + path);try {ZipFile zipFile = new ZipFile(new File(path));System.out.println("Zip file comment = " + zipFile.getComment());} catch(IOException e) {e.printStackTrace();System.out.println("Zip file comment read failed!");}}
public static void writeApk(File file, String comment) {ZipFile zipFile = null;ByteArrayOutputStream outputStream = null;RandomAccessFile accessFile = null;try {zipFile = new ZipFile(file);String zipComment = zipFile.getComment();if (zipComment != null) {return;}
byte[] byteComment = comment.getBytes();outputStream = new ByteArrayOutputStream();
outputStream.write(byteComment);outputStream.write(short2Stream((short) byteComment.length));
byte[] data = outputStream.toByteArray();
accessFile = new RandomAccessFile(file, "rw");accessFile.seek(file.length() - 2);accessFile.write(short2Stream((short) data.length));accessFile.write(data);} catch (IOException e) {e.printStackTrace();} finally {try {if (zipFile != null) {zipFile.close();}if (outputStream != null) {outputStream.close();}if (accessFile != null) {accessFile.close();}} catch (Exception e) {
}
}}
/**
字节数组转换成 short(小端序)*/private static byte[] short2Stream(short data) {ByteBuffer buffer = ByteBuffer.allocate(2);buffer.order(ByteOrder.LITTLE_ENDIAN);buffer.putShort(data);buffer.flip();return buffer.array();}}
然后使用下面的命令对 APK 写入 URL:
$java WriteAPK /path/to/your/APK http://news.zhoulujue.com/article/12345/
用户首次打开时:读取 URL 并打开
在 App 首次打开的时候读取 ZIP 文件头里你写入的 URL,读取代码如下:
public static String getUnfinishedURL(Context context) {//获取缓存的 APK 文件 File file = new File(context.getPackageCodePath());byte[] bytes;RandomAccessFile accessFile = null;// 从指定的位置找到 WriteAPK.java 写入的信息 try {accessFile = new RandomAccessFile(file, "r");long index = accessFile.length();bytes = new byte[2];index = index - bytes.length;accessFile.seek(index);accessFile.readFully(bytes);int contentLength = stream2Short(bytes, 0);bytes = new byte[contentLength];index = index - bytes.length;accessFile.seek(index);accessFile.readFully(bytes);return new String(bytes, "utf-8");} catch (IOException e) {e.printStackTrace();} finally {if (accessFile != null) {try {accessFile.close();} catch (IOException ignored) {ignored.printStackTrace();}}}return null;
}
接着只要将getUnfinishedURL
返回值交给 Router 去处理,从而将用户导向没有阅读完毕的新闻详情页。
优化 2:有控制的允许流量的导出
上面的内容都是在讲如何尽可能地把用户导进 App 里来,从另外一个角度,为了提高用户转化率我们要降低用户的跳出率,也就是说尽量避免用户从我们的 App 里被带跑了。
很多情况下,如果我们运营一个 UGC 的社区,我们无法控制用户创建内容的时候会填写哪些 URL,当然作为一个开放的平台我们肯定希望用户能够更高地利用各种工具将他们所专注的任务完成。
但是如果平台出现了一些人不受限制的发广告,或者利用你的平台运营竞争对手的产品,这种方式对成长中的产品打击有可能将是毁灭性的。
最后我想说
为什么很多程序员做不了架构师?1、良好健康的职业规划很重要,但大多数人都忽略了 2、学习的习惯很重要,持之以恒才是正解。3、编程思维没能提升一个台阶,局限在了编码,业务,没考虑过选型、扩展 4、身边没有好的架构师引导、培养。所处的圈子对程序员的成长影响巨大。
金九银十面试季,跳槽季,整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为 Android 高级工程师的系统且全面的学习资料。涵盖了 Android 初级——Android 高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司 Android 面试真题(含 BAT、小米、华为、美团、滴滴)和我自己整理 Android 复习笔记(包含 Android 基础知识点、Android 扩展知识点、Android 源码解析、设计模式汇总、Gradle 知识点、常见算法题汇总。)

里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
评论