Android Dynamic Action隔披,簡稱DA洒疚,是一種簡便征冷、可變Action的實現(xiàn)方案择膝。DA框架的初衷是為了取代Context.startActivity的調(diào)用方式,使用建造者模式(Builder Pattern)構(gòu)建交互參數(shù)资盅,使程序更優(yōu)美调榄。DA框架能夠?qū)θ魏我粋€已經(jīng)存在的Action修改,動態(tài)改變原有的跳轉(zhuǎn)邏輯呵扛。值得一提的是每庆,DA框架不僅友好地實現(xiàn)了與H5間的跳轉(zhuǎn)交互,也解決了Activity在插件化項目的交互問題今穿。
項目地址:https://github.com/benniaobuguai/android-dynamic-action
DA的URI基本結(jié)構(gòu)
在DA框架下缤灵,Activity是一個有趣的概念實體,每一個Activity都可視作DA框架下的一種資源。對于一個客戶端而言腮出,每個Activity都是全局唯一可訪問的資源帖鸦,因此每個Activity都有統(tǒng)一資源標(biāo)識符(URI)。
URI的基本結(jié)構(gòu):
scheme://com.example.project:8888/path/etc?id=1024
\-----/ \------------------/\--/ \------/\-------/
scheme host port path query parameter
\---------------------/
authority
DA框架基于標(biāo)準(zhǔn)的URI胚嘲,定制了更符合Android Activity交互的URI結(jié)構(gòu)作儿。
定制后的URI基本結(jié)構(gòu):
scheme://packageId$ActionName?data={"id":"1024"}
\-----/ \------------------/\-----------------/
scheme host query parameter
基于以上協(xié)議,定義屬于自己的scheme馋劈,每個Activity將具有一個可被訪問的URI攻锰,你就能夠像訪問網(wǎng)頁一樣訪問Activity啦!<宋怼娶吞!
DA的配置文件
DA框架的“動態(tài)可變性”體現(xiàn)在配置文件上,DA框架遵循“約定優(yōu)于配置”的原則械姻,使用更少的配置達(dá)到目的妒蛇。配置文件非常簡單,僅包含scheme以及包名的映射關(guān)系楷拳。
配置文件示例:
<?xml version="1.0" encoding="UTF-8"?>
<DynamicAction xmlns:opencdk="http://www.opencdk.com/dynamicaction"
opencdk:version="1.0.0" >
<constant name="DA.devMode" value="true" />
<constant name="DA.scheme" value="opencdk" />
<constant name="DA.appscheme" value="opencdkexample" />
<package id="0" name="com.opencdk.da.ui" >
</package>
<package id="1" name="com.opencdk.da.ui.user" >
</package>
<package id="2" name="com.opencdk.da.ui.video" >
<!-- 用H5登錄界面來修復(fù)有BUG的原生登錄 -->
<action name="Login" from="home_login_click" to="0$Browser?title=H5登錄&url=http://www.opencdk.com/login.html" />
</package>
</DynamicAction>
參數(shù)說明:
- DA.devMode:開發(fā)模式绣夺,開關(guān)打開后,后臺將可看到更多日志
- DA.scheme:DA框架內(nèi)部使用的scheme唯竹,建議根據(jù)自己的業(yè)務(wù)定義唯一的scheme
- DA.appscheme:非DA框架直接使用乐导,是App提供給第三方App的scheme
- package:包名的映射關(guān)系,文章最后有完整配置
- package>id:包id浸颓,與包名一一對應(yīng)
- package>name:包名
- action:動態(tài)修復(fù)的Action
- action>name:Action的名稱
- action>from:事件源,觸發(fā)此事件的源頭
- action>to:目標(biāo)地址(DA框架標(biāo)準(zhǔn)的URI數(shù)據(jù)結(jié)構(gòu))
- action>to>title:Activity標(biāo)題
- action>to>url:網(wǎng)址
DA的代碼實現(xiàn)
假設(shè)在com.opencdk.da.ui包下有LoginActivity旺拉,訪問的scheme可表示為:
opencdk://1$Login
使用DA框架后产上,啟動LoginActivity則變得非常容易:
new DA.Builder(Context)
.setHost("1$Login")
.go();
或:
new DA.Builder(Context)
.setUriString("opencdk://1$Login")
.go();
或:
new DA.Builder(Context)
.setPackageId("1")
.setActionName("Login")
.go();
非常簡單的實現(xiàn)方式,為動態(tài)運營奠定了基礎(chǔ)蛾狗〗粒基于DA框架,開發(fā)同學(xué)可以輕松構(gòu)建項目規(guī)范沉桌,編寫更優(yōu)雅的代碼谢鹊;測試同學(xué)編寫測試用例也變得更容易了。
DA的數(shù)據(jù)交互
DA框架不但支持原生Activity間的數(shù)據(jù)交互留凭,而且也支持Activity與H5間的數(shù)據(jù)交互佃扼。保證數(shù)據(jù)協(xié)議的一致性,DA框架統(tǒng)一使用JSON進(jìn)行數(shù)據(jù)交互(推薦使用fastjson)蔼夜。
URI表示如下:
opencdk://1$Login?data={"username":"benniaobuguai"}
代碼調(diào)用:
new DA.Builder(Context)
.setHost("1$Login")
.setData("{\"username\":\"benniaobuguai\"}")
.go();
DA支持Activity回調(diào)(Context.startActivityForResult)
URI表示如下:
opencdk://1$Login?data={"username":"benniaobuguai"}&requestCode=10000
代碼調(diào)用:
new DA.Builder(Context)
.setHost("1$Login")
.setData("{\"username\":\"benniaobuguai\"}")
.setRequestCode(10000)
.go();
PS: 建議在Activity間交互時使用事件總線兼耀,如:EventBus,otto等。
DA動態(tài)修改默認(rèn)跳轉(zhuǎn)
- 用一個Activity替換另外一個Activity
<package id="2" name="com.opencdk.da.ui.video" >
<!--- 隨機(jī)推薦視頻列表界面修改成免費視頻列表界面 -->
<action name="VideoRandomList" from="home_video_random_click" to="2$VideoFreeList" />
</package>
- 用一個H5界面替換一個Activity
<package id="2" name="com.opencdk.da.ui.video" >
<!-- 隨機(jī)推薦視頻列表界面修改成H5的免費視頻推薦界面 -->
<action name="VideoRandomList" from="home_video_random_click" to="0$Browser?title=精彩免費視頻推薦&url=http%3a%2f%2fwww.iqiyi.com%2fdianying%2ffree.html" />
</package>
注意:
- xml文件不支持直接使用&瘤运、<窍霞、>等特殊字符,應(yīng)該使用其對應(yīng)的轉(zhuǎn)義字符拯坟。具體可參考:XML和HTML常用轉(zhuǎn)義字符
- url地址需要進(jìn)行URL編碼但金,避免特殊字符對URI的解析造成影響。在線URL編碼
DA啟用攔截器功能
往往有些時候郁季,我們需要對某些界面進(jìn)行訪問控制冷溃。
已經(jīng)發(fā)布的版本,任何用戶都可訪問VIP視頻列表巩踏。臨時需求變更秃诵,只有登錄的用戶才能訪問VIP視頻列表界面,可修改配置下發(fā):
<interceptors>
<interceptor
name="LoginInterceptor"
class="com.opencdk.da.interceptor.LoginInterceptor" >
</interceptor>
<interceptor
name="TestInterceptor"
class="com.opencdk.da.interceptor.TestInterceptor" >
</interceptor>
<actionInterceptor>
<accept name="2$VideoVIPList" >
<interceptor-ref>LoginInterceptor</interceptor-ref>
</accept>
</actionInterceptor>
</interceptors>
H5與原生程序的交互
引入DA框架后塞琼,在H5界面也可前往任意的原生界面菠净,代碼也非常簡單。
H5代碼片斷:
<p><a href="opencdk://1$Login">Login</a></p>
<p><a href="opencdk://2$VideoPlay">Play Video</a></p>
<p><a href="opencdk://0$Browser?url=http://www.opencdk.com">http://www.opencdk.com</a></p>
重寫WebView WebViewClient的方法shouldOverrideUrlLoading(WebView view, String url)彪杉,支持自定義的scheme毅往。
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url != null && url.startsWith(DALoader.getScheme())) {
new DA.Builder(mContext)
.setUriString(url)
.go();
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
});
DA框架解決的核心問題
- 一個地址可達(dá)任意Activity
- 任意一個Activity可被動態(tài)修改為另一個Activity
- DA框架數(shù)據(jù)交互扁平化,使用JSON便于與H5交互
- 任意Activity可替換成H5派近,快速修復(fù)原生突發(fā)BUG
- 插件化項目中也能滿足以上需求
DA框架所遵循的一些約定
DA框架默認(rèn)遵循以下規(guī)則:
- 配置優(yōu)先級>Java代碼優(yōu)先級攀唯,保證通過配置可修復(fù)代碼編碼的缺陷。
- 查找動態(tài)配置時渴丸,先根據(jù)name+from進(jìn)行精確查找侯嘀,其次根據(jù)name去查找。
示例說明
為了更好地體現(xiàn)動態(tài)Action谱轨,示例中在assets目錄下放了多個配置文件戒幔,加載不同的配置文件就相當(dāng)于網(wǎng)絡(luò)下發(fā)了新的配置文件。
- dynamic_action.cfg土童,默認(rèn)的配置文件诗茎,僅包含包結(jié)構(gòu)映射關(guān)系
- dynamic_action_transfer.cfg,配置一個動態(tài)Action
- dynamic_action_interceptor.cfg献汗,配置一個攔截器
進(jìn)階與思考
- 客戶端與客戶端交互
客戶端與客戶端之間的交互是不安全的敢订,對于暴露給第三方的入口都需要進(jìn)行校驗。就DA框架而言罢吃,主要是為了解決內(nèi)部跳轉(zhuǎn)的統(tǒng)一協(xié)議而確定的scheme(opencdk://)楚午,提供外部使用的scheme(opencdkexample://)。opencdk://僅供內(nèi)部使用刃麸,認(rèn)為是可信任的醒叁、安全的。opencdkexample://是外部協(xié)議,必須經(jīng)過校驗方可拉起我們的客戶端把沼。
需要提供外部入口啊易,必須要在AndroidManifest.xml里面定義,
<activity
android:name="com.opencdk.da.ui.SplashActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/DA.Theme.NoTitleBar" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<!-- 定義給外部調(diào)用 的scheme -->
<data android:scheme="opencdkexample" />
</intent-filter>
</activity>
新建一個項目饮睬,執(zhí)行外部調(diào)用代碼片斷:
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_DEFAULT);
Uri data = Uri.parse("opencdkexample://0$Browser?url=http://v.qq.com/cover/r/rm3tmmat4li8uul/w0019k37ecc.html");
intent.setData(data);
startActivity(intent);
- iOS對本協(xié)議的支持
iOS并無包名概念租谈,如果希望使用同一個URI來跳轉(zhuǎn)至同一個界面,iOS在對URI的處理時捆愁,應(yīng)當(dāng)直接過濾包名后再使用割去。
登錄配置如下:
opencdk://1$Login?data={"username":"benniaobuguai"}
解析【host】協(xié)議時,直接取【ActionName】昼丑,忽略前面的 "1$" 即可呻逆。
- 對于簡單的項目,Android把所有Activity放在一個包名下(不建議這么做)菩帝,也可與iOS保持URI同一處理邏輯咖城。
URI表示如下:
opencdk://Login?data={"username":"benniaobuguai"}
PS: DA框架當(dāng)前不支持無包名的實現(xiàn)方式。
注意點
- 避免循環(huán)對Action進(jìn)行中轉(zhuǎn)
- 通過反射訪問目標(biāo)Activity呼奢,ActionName的大小寫敏感
- 避免過多的包名映射宜雀,Activity所在的包不宜過多,包越多維護(hù)成本越大握础。
- Activity不能被混淆
其他
- 運營的靈活性辐董,增強(qiáng)運營配置的自由度
- 界面可替換性,任意Activity可替換成H5禀综,提供快速使用H5修復(fù)BUG的能力
- 插件訪問簡單化简烘,宿主程序是無法直接獲取插件的Activity對象,DA框架的作用尤其明顯定枷。
- DA框架最大的問題是全局可訪問任意Activity夸研,如何保證被訪問者的安全,業(yè)務(wù)不受到影響就顯示尤為重要了
- DA框架與傳統(tǒng)的Context.startActivity最大的區(qū)別在于:交互協(xié)議標(biāo)準(zhǔn)化依鸥、靈活、運營能力強(qiáng)悼沈,動態(tài)修復(fù)能力強(qiáng)
Q&A:
1.為什么需要制定兩個scheme?
答:隔離
2.配置from to過多時, 性能如何?
答:配置from和to時贱迟,屬于修復(fù)BUG的行為,原則上并不會太多絮供。通過HashMap查找衣吠,速度很快。
3.配置文件過大時, 性能如何?
答:未驗證壤靶。
完整的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<DynamicAction xmlns:opencdk="http://www.opencdk.com/dynamicaction"
opencdk:version="1.0.0" >
<constant name="DA.devMode" value="true" />
<constant name="DA.scheme" value="opencdk" />
<constant name="DA.appscheme" value="opencdkexample" />
<package id="0" name="com.opencdk.da.ui" >
</package>
<package id="1" name="com.opencdk.da.ui.user" >
</package>
<package id="2" name="com.opencdk.da.ui.video" >
<action name="VideoRandomList" from="home_video_random_click" to="2$VideoFreeList" />
</package>
<interceptors>
<interceptor
name="LoginInterceptor"
class="com.opencdk.da.interceptor.LoginInterceptor" >
</interceptor>
<interceptor
name="TestInterceptor"
class="com.opencdk.da.interceptor.TestInterceptor" >
</interceptor>
<actionInterceptor>
<accept name="2$VideoVIPList" >
<interceptor-ref>LoginInterceptor</interceptor-ref>
</accept>
</actionInterceptor>
</interceptors>
</DynamicAction>