Q1: Flutter是如何自定義View?
通過(guò)繼承CustomPainter
,然后實(shí)現(xiàn)paint(Canvas canvas, Size size)
方法拿到canvas
,使用Canvas
來(lái)繪制需要的View, 這個(gè)繪制方法和Android和iOS都基本類似
Q2: 在Flutter中如何使用Android和iOS的原生View?(什么是 platform view谢鹊?)
platform view
就是 AndroidView
和UIKitView
的總稱脚仔,允許將 native view
嵌入到了 flutter widget
體系中轴脐,完成 Datr 代碼對(duì)native view
的控制。
1朗鸠、
在Flutter中使用一個(gè)Widget
包裹platform view
便于使用
@override
Widget build(BuildContext context) {
// 根據(jù)運(yùn)行平臺(tái)判斷執(zhí)行代碼
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
// 在 native 中的唯一標(biāo)識(shí)符笼踩,需要與 native 側(cè)的值相同
viewType: "platform_text_view",
// 在創(chuàng)建 AndroidView 的同時(shí)穷娱,可以傳遞參數(shù)
creationParams: <String, dynamic>{"text": text},
// 用來(lái)編碼 creationParams 的形式讯屈,可選 [StandardMessageCodec], [JSONMessageCodec], [StringCodec], or [BinaryCodec]
// 如果存在 creationParams蛋哭,則該值不能為null
creationParamsCodec: const StandardMessageCodec(),
);
}else if (defaultTargetPlatform == TargetPlatform.iOS) {
return UiKitView(
viewType: "platform_text_view",
creationParams: <String, dynamic>{"text": text},
creationParamsCodec: const StandardMessageCodec(),
);
}
return Text("不支持的平臺(tái)");
}
2、
在原生Android端創(chuàng)建一個(gè)類繼承PlatformView
,之后創(chuàng)建一個(gè)類繼承PlatformViewFactory
,在create
方法中返回繼承PlatformView
類,代碼如下
class AndroidCustomeView(context: Context) : PlatformView {
val contentView: TextView = TextView(context)
override fun getView(): View {
return contentView
}
override fun dispose() {}
}
class AndroidCustomeViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
val androidTextView = AndroidTextView(context)
androidTextView.contentView.id = viewId
val params = args?.let { args as Map<*, *> }
val text = params?.get("text") as CharSequence?
text?.let {
androidTextView.contentView.text = it
}
return androidTextView
}
}
3涮母、iOS中與Android中一樣
但是多一個(gè)步驟,在配置文件info.plist增加io.flutter.embedded_views_preview=true
小結(jié)一下
- 我們了解了 AndroidViewController谆趾、_AndroidPlatformView 都做了什么。
- AndroidView 的大小是由父節(jié)點(diǎn)的大小去定的所以上面使用 Expanded 包裹則可以生效叛本,如果不進(jìn)行包裹沪蓬,則大小為父控件大小,在 Column 中會(huì)出現(xiàn)問(wèn)題来候。當(dāng) Widget size 小于 View size跷叉,F(xiàn)lutter 會(huì)進(jìn)行裁剪。當(dāng) Widget size 大于 View size 時(shí)营搅,多出來(lái)的位置會(huì)被背景填充云挟。在 Android 側(cè),實(shí)現(xiàn)了 PlatformView 的 View 會(huì)被包裹在 FrameLayout 中剧防,可以對(duì) View 的繪制添加監(jiān)聽(tīng),打印出 View 的 parent辫樱;
- platform view 是在 native 側(cè)渲染的峭拘,返回給 Flutter 側(cè)一個(gè) _textureId ,通過(guò)這個(gè) id Flutter 將 View 直接展示出來(lái)狮暑。這部分也說(shuō)明了為什么 platform view 在 Flutter 中的性能開(kāi)銷比較大鸡挠,整個(gè)過(guò)程數(shù)據(jù)需要從 GPU -> CPU -> GPU,這部分的代價(jià)是比較大的搬男。
如何開(kāi)發(fā)一個(gè) platform view
其實(shí) Flutter 官方維護(hù)了一些 plugin拣展,鏈接如下:
https://github.com/flutter/plugins
其中的 webview_flutter 、google_maps_flutter 就是通過(guò) platform view缔逛,就是一個(gè)很好的 demo 备埃。
Q4: 動(dòng)態(tài)加載技術(shù)?
1、什么是動(dòng)態(tài)加載技術(shù)褐奴?
動(dòng)態(tài)加載技術(shù)就是使用類加載器加載相應(yīng)的apk
按脚、dex
、jar
(必須含有dex
文件)敦冬,再通過(guò)反射獲得該apk
辅搬、dex
、jar
內(nèi)部的資源(class
脖旱、圖片
堪遂、color
等等)進(jìn)而供宿主app
使用介蛉。它的優(yōu)點(diǎn)可以讓應(yīng)用程序?qū)崿F(xiàn)插件化
、插拔式
結(jié)構(gòu)
2溶褪、關(guān)于動(dòng)態(tài)加載使用的類加載器
使用動(dòng)態(tài)加載技術(shù)時(shí)币旧,一般需要用到這兩個(gè)類加載器:
- PathClassLoader - 只能加載已經(jīng)安裝的apk,即/data/app目錄下的apk竿滨。
- DexClassLoader - 能加載手機(jī)中未安裝的apk佳恬、jar、dex于游,只要能在找到對(duì)應(yīng)的路徑毁葱。
這兩個(gè)加載器分別對(duì)應(yīng)使用的場(chǎng)景各不同,所以接下來(lái)贰剥,分別講解它們各自加載相同的插件apk的使用倾剿。
3、使用PathClassLoader
加載已安裝的apk插件蚌成,獲取相應(yīng)的資源供宿主app使用
1.
首先我們需要知道一個(gè)manifest中的屬性:SharedUserId
前痘。
該屬性是用來(lái)干嘛的呢?簡(jiǎn)單的說(shuō)担忧,應(yīng)用從一開(kāi)始安裝在Android系統(tǒng)上時(shí)芹缔,系統(tǒng)都會(huì)給它分配一個(gè)linux user id,之后該應(yīng)用在今后都將運(yùn)行在獨(dú)立的一個(gè)進(jìn)程中瓶盛,其它應(yīng)用程序不能訪問(wèn)它的資源最欠,那么
如果兩個(gè)應(yīng)用的sharedUserId相同,那么它們將共同運(yùn)行在相同的linux進(jìn)程中惩猫,從而便可以數(shù)據(jù)共享芝硬、資源訪問(wèn)了。所以我們?cè)谒拗鱝pp和插件app的manifest上都定義一個(gè)相同的sharedUserId轧房。
2拌阴、
那么我們將插件apk安裝在手機(jī)上后,宿主app怎么知道手機(jī)內(nèi)該插件是否是我們應(yīng)用程序的插件呢奶镶?
我們之前是不是定義過(guò)插件apk也是使用相同的sharedUserId迟赃,那么,我就可以這樣思考了厂镇,是不是可以得到手機(jī)內(nèi)所有已安裝apk的sharedUserId呢捺氢,然后通過(guò)判斷sharedUserId是否和宿主app的相同,如果是剪撬,那么該app就是我們的插件app了摄乒。確實(shí)是這樣的思路的,那么有了思路最大的問(wèn)題就是怎么獲取一個(gè)應(yīng)用程序內(nèi)的sharedUserId了,我們可以通過(guò)PackageInfo.sharedUserId來(lái)獲取馍佑,請(qǐng)看代碼:
/**
* 查找手機(jī)內(nèi)所有的插件
* @return 返回一個(gè)插件List
*/
private List<PluginBean> findAllPlugin() {
List<PluginBean> plugins = new ArrayList<>();
PackageManager pm = getPackageManager();
//通過(guò)包管理器查找所有已安裝的apk文件
List<PackageInfo> packageInfos = pm.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);
for (PackageInfo info : packageInfos) {
//得到當(dāng)前apk的包名
String pkgName = info.packageName;
//得到當(dāng)前apk的sharedUserId
String shareUesrId = info.sharedUserId;
//判斷這個(gè)apk是否是我們應(yīng)用程序的插件
if (shareUesrId != null && shareUesrId.equals("com.sunzxyong.myapp") && !pkgName.equals(this.getPackageName())) {
String label = pm.getApplicationLabel(info.applicationInfo).toString();//得到插件apk的名稱
PluginBean bean = new PluginBean(label,pkgName);
plugins.add(bean);
}
}
return plugins;
}
通過(guò)這段代碼斋否,我們就可以輕松的獲取手機(jī)內(nèi)存在的所有插件,其中PluginBean是定義的一個(gè)實(shí)體類而已拭荤,就不貼它的代碼了茵臭。
3、如果找到了插件舅世,就把可用的插件顯示出來(lái)了旦委,如果沒(méi)有找到,那么就可提示用戶先去下載插件什么的雏亚。
List<HashMap<String, String>> datas = new ArrayList<>();
List<PluginBean> plugins = findAllPlugin();
if (plugins != null && !plugins.isEmpty()) {
for (PluginBean bean : plugins) {
HashMap<String, String> map = new HashMap<>();
map.put("label", bean.getLabel());
datas.add(map);
}
} else {
Toast.makeText(this, "沒(méi)有找到插件缨硝,請(qǐng)先下載!", Toast.LENGTH_SHORT).show();
}
showEnableAllPluginPopup(datas);
4罢低、如果找到后查辩,那么我們選擇對(duì)應(yīng)的插件時(shí),在宿主app中就加載插件內(nèi)對(duì)應(yīng)的資源网持,這個(gè)才是PathClassLoader的重點(diǎn)宜岛。我們首先看看怎么實(shí)現(xiàn)的吧:
/**
* 加載已安裝的apk
* @param packageName 應(yīng)用的包名
* @param pluginContext 插件app的上下文
* @return 對(duì)應(yīng)資源的id
*/
private int dynamicLoadApk(String packageName, Context pluginContext) throws Exception {
//第一個(gè)參數(shù)為包含dex的apk或者jar的路徑,第二個(gè)參數(shù)為父加載器
PathClassLoader pathClassLoader = new PathClassLoader(pluginContext.getPackageResourcePath(),ClassLoader.getSystemClassLoader());
// Class<?> clazz = pathClassLoader.loadClass(packageName + ".R$mipmap");//通過(guò)使用自身的加載器反射出mipmap類進(jìn)而使用該類的功能
//參數(shù):1功舀、類的全名萍倡,2、是否初始化類辟汰,3列敲、加載時(shí)使用的類加載器
Class<?> clazz = Class.forName(packageName + ".R$mipmap", true, pathClassLoader);
//使用上述兩種方式都可以,這里我們得到R類中的內(nèi)部類mipmap莉擒,通過(guò)它得到對(duì)應(yīng)的圖片id酿炸,進(jìn)而給我們使用
Field field = clazz.getDeclaredField("one");
int resourceId = field.getInt(R.mipmap.class);
return resourceId;
}
這個(gè)方法就是加載包名為packageName的插件瘫絮,然后獲得插件內(nèi)名為one.png的圖片的資源id涨冀,進(jìn)而供宿主app使用該圖片。現(xiàn)在我們一步一步來(lái)講解一下:
- 首先就是new出一個(gè)PathClassLoader對(duì)象麦萤,它的構(gòu)造方法為:
public PathClassLoader(String dexPath, ClassLoader parent)
中其中第一個(gè)參數(shù)是通過(guò)插件的上下文來(lái)獲取插件apk的路徑鹿鳖,其實(shí)獲取到的就是/data/app/apkthemeplugin.apk,那么插件的上下文怎么獲取呢壮莹?在宿主app中我們只有本app的上下文啊翅帜,答案就是為插件app創(chuàng)建一個(gè)上下文:
Context pluginContext = createPackageContext(packageName, CONTEXT_IGNORE_SECURITY | CONTEXT_INCLUDE_CODE);
通過(guò)插件的包名來(lái)創(chuàng)建上下文,不過(guò)這種方法只適合獲取已安裝的app上下文命满±缘危或者不需要通過(guò)反射直接通過(guò)插件上下文getResource().getxxx(R..);也行,而這里用的是反射方法。第二個(gè)參數(shù)是父加載器歼疮,都是ClassLoader.getSystemClassLoader()杂抽。
-
插件app的類加載器我們創(chuàng)建出來(lái)了,接下來(lái)就是通過(guò)反射獲取對(duì)應(yīng)類的資源了,這里我是獲取R類中的內(nèi)部類mipmap類韩脏,然后通過(guò)反射得到mipmap類中名為one的字段的值缩麸,
然后通過(guò)
plugnContext.getResources().getDrawable(resouceId)
就可以獲取對(duì)應(yīng)id的Drawable得到該圖片資源進(jìn)而宿主app的可用它設(shè)置背景等。
下面演示下該demo效果赡矢,在沒(méi)有插件情況下會(huì)提示請(qǐng)先下載插件杭朱,有插件時(shí)候就選擇對(duì)應(yīng)的插件而供宿主app使用,本demo是換背景的功能演示吹散,我來(lái)看宿主app中mipmap文件夾下并沒(méi)有one.png這張圖片弧械。
4、DexClassLoader加載未安裝的apk,提供資源供宿主app使用
關(guān)于動(dòng)態(tài)加載未安裝的apk埂伦,我先描述下思路:首先我們得到事先知道我們的插件apk存放在哪個(gè)目錄下异希,然后分別得到插件apk的信息(名稱、包名等)唁桩,然后顯示可用的插件,最后動(dòng)態(tài)加載apk獲得資源耸棒。
按照上面這個(gè)思路荒澡,我們需要解決幾個(gè)問(wèn)題:
- 怎么得到未安裝的apk的信息
- 怎么得到插件的context或者Resource,因?yàn)樗俏窗惭b的不可能通過(guò)createPackageContext(...);方法來(lái)構(gòu)建出一個(gè)context与殃,所以這時(shí)只有在Resource上下功夫单山。
現(xiàn)在我們就一一來(lái)解答這些問(wèn)題吧:
1、得到未安裝的apk信息可以通過(guò)mPackageManager.getPackageArchiveInfo()方法獲得幅疼,
public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags)
它的參數(shù)剛好是傳入一個(gè)FilePath米奸,然后返回apk文件的PackageInfo信息:
/**
* 獲取未安裝apk的信息
* @param context
* @param archiveFilePath apk文件的path
* @return
*/
private String[] getUninstallApkInfo(Context context, String archiveFilePath) {
String[] info = new String[2];
PackageManager pm = context.getPackageManager();
PackageInfo pkgInfo = pm.getPackageArchiveInfo(archiveFilePath, PackageManager.GET_ACTIVITIES);
if (pkgInfo != null) {
ApplicationInfo appInfo = pkgInfo.applicationInfo;
String versionName = pkgInfo.versionName;//版本號(hào)
Drawable icon = pm.getApplicationIcon(appInfo);//圖標(biāo)
String appName = pm.getApplicationLabel(appInfo).toString();//app名稱
String pkgName = appInfo.packageName;//包名
info[0] = appName;
info[1] = pkgName;
}
return info;
}
2、得到對(duì)應(yīng)未安裝apk的Resource對(duì)象爽篷,我們需要通過(guò)反射來(lái)獲得:
/**
* @param apkName
* @return 得到對(duì)應(yīng)插件的Resource對(duì)象
*/
private Resources getPluginResources(String apkName) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);//反射調(diào)用方法addAssetPath(String path)
//第二個(gè)參數(shù)是apk的路徑:Environment.getExternalStorageDirectory().getPath()+File.separator+"plugin"+File.separator+"apkplugin.apk"
addAssetPath.invoke(assetManager, apkDir+File.separator+apkName);//將未安裝的Apk文件的添加進(jìn)AssetManager中悴晰,第二個(gè)參數(shù)為apk文件的路徑帶apk名
Resources superRes = this.getResources();
Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
return mResources;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
通過(guò)得到AssetManager中的內(nèi)部的方法addAssetPath,將未安裝的apk路徑傳入從而添加進(jìn)assetManager中逐工,然后通過(guò)new Resource把a(bǔ)ssetManager傳入構(gòu)造方法中铡溪,進(jìn)而得到未安裝apk對(duì)應(yīng)的Resource對(duì)象。
好了泪喊!上面兩個(gè)問(wèn)題解決了棕硫,那么接下來(lái)就是加載未安裝的apk獲得它的內(nèi)部資源。
/**
* 加載apk獲得內(nèi)部資源
* @param apkDir apk目錄
* @param apkName apk名字,帶.apk
* @throws Exception
*/
private void dynamicLoadApk(String apkDir, String apkName, String apkPackageName) throws Exception {
File optimizedDirectoryFile = getDir("dex", Context.MODE_PRIVATE);//在應(yīng)用安裝目錄下創(chuàng)建一個(gè)名為app_dex文件夾目錄,如果已經(jīng)存在則不創(chuàng)建
Log.v("zxy", optimizedDirectoryFile.getPath().toString());// /data/data/com.example.dynamicloadapk/app_dex
//參數(shù):1袒啼、包含dex的apk文件或jar文件的路徑哈扮,2纬纪、apk、jar解壓縮生成dex存儲(chǔ)的目錄滑肉,3育八、本地library庫(kù)目錄,一般為null赦邻,4髓棋、父ClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(apkDir+File.separator+apkName, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());
Class<?> clazz = dexClassLoader.loadClass(apkPackageName + ".R$mipmap");//通過(guò)使用apk自己的類加載器,反射出R類中相應(yīng)的內(nèi)部類進(jìn)而獲取我們需要的資源id
Field field = clazz.getDeclaredField("one");//得到名為one的這張圖片字段
int resId = field.getInt(R.id.class);//得到圖片id
Resources mResources = getPluginResources(apkName);//得到插件apk中的Resource
if (mResources != null) {
//通過(guò)插件apk中的Resource得到resId對(duì)應(yīng)的資源
findViewById(R.id.background).setBackgroundDrawable(mResources.getDrawable(resId));
}
}
其中通過(guò)new DexClassLoader()來(lái)創(chuàng)建未安裝apk的類加載器惶洲,我們來(lái)看看它的參數(shù):
public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)
- dexPath - 就是apk文件的路徑
- optimizedDirectory - apk解壓縮后的存放dex的目錄按声,值得注意的是,在4.1以后該目錄不允許在sd卡上恬吕,看官方文檔:
A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.
This class loader requires an application-private, writable directory to cache optimized classes. Use Context.getDir(String, int) to create such a directory:
File dexOutputDir = context.getDir("dex", 0);
Do not cache optimized classes on external storage. External storage does not provide access controls necessary to protect your application from code injection
所以我們用getDir()方法在應(yīng)用內(nèi)部創(chuàng)建一個(gè)dexOutputDir签则。
- libraryPath - 本地的library,一般為null
- parent - 父加載器
接下來(lái)铐料,就是通過(guò)反射的方法渐裂,獲取出需要的資源。
再看看拷貝了三個(gè)插件:
copyApkFile("apkthemeplugin-1.apk");
copyApkFile("apkthemeplugin-2.apk");
copyApkFile("apkthemeplugin-3.apk");
可以看到只要一有插件下載钠惩,就能顯示出來(lái)并使用它柒凉。
當(dāng)然插件化開(kāi)發(fā)并不只是像只有這種換膚那么簡(jiǎn)單的用途,這只是個(gè)demo篓跛,學(xué)習(xí)這種插件化開(kāi)發(fā)思想的膝捞。由此可以聯(lián)想,這種插件化的開(kāi)發(fā)愧沟,是不是像QQ里的表情包啊蔬咬、背景皮膚啊,通過(guò)線上下載線下維護(hù)的方式沐寺,可以在線下載使用相應(yīng)的皮膚林艘,不使用時(shí)候就可以刪了,所以插件化開(kāi)發(fā)是插件與宿主app進(jìn)行解耦了混坞,即使在沒(méi)有插件情況下狐援,也不會(huì)對(duì)宿主app有任何影響,而有的話就供用戶選擇性使用了拔第。