Flutter 總結(jié)《二》

Q1: Flutter是如何自定義View?

通過繼承CustomPainter,然后實現(xiàn)paint(Canvas canvas, Size size)方法拿到canvas,使用Canvas來繪制需要的View, 這個繪制方法和Android和iOS都基本類似

Q2: 在Flutter中如何使用Android和iOS的原生View?(什么是 platform view?)

platform view 就是 AndroidViewUIKitView 的總稱,允許將 native view 嵌入到了 flutter widget 體系中,完成 Datr 代碼對native view 的控制。

1、在Flutter中使用一個Widget包裹platform view便于使用
@override
Widget build(BuildContext context) {
     // 根據(jù)運行平臺判斷執(zhí)行代碼
     if (defaultTargetPlatform == TargetPlatform.android) {
           return AndroidView(
                   // 在 native 中的唯一標(biāo)識符,需要與 native 側(cè)的值相同
                   viewType: "platform_text_view",
                   // 在創(chuàng)建 AndroidView 的同時,可以傳遞參數(shù)
                   creationParams: <String, dynamic>{"text": text},
                 // 用來編碼 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("不支持的平臺");
}
2、 在原生Android端創(chuàng)建一個類繼承PlatformView,之后創(chuàng)建一個類繼承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中一樣

但是多一個步驟,在配置文件info.plist增加io.flutter.embedded_views_preview=true

小結(jié)一下

  • 我們了解了 AndroidViewController、_AndroidPlatformView 都做了什么。
  • AndroidView 的大小是由父節(jié)點的大小去定的所以上面使用 Expanded 包裹則可以生效,如果不進行包裹,則大小為父控件大小,在 Column 中會出現(xiàn)問題。當(dāng) Widget size 小于 View size,F(xiàn)lutter 會進行裁剪。當(dāng) Widget size 大于 View size 時,多出來的位置會被背景填充。在 Android 側(cè),實現(xiàn)了 PlatformView 的 View 會被包裹在 FrameLayout 中,可以對 View 的繪制添加監(jiān)聽,打印出 View 的 parent;
  • platform view 是在 native 側(cè)渲染的,返回給 Flutter 側(cè)一個 _textureId ,通過這個 id Flutter 將 View 直接展示出來。這部分也說明了為什么 platform view 在 Flutter 中的性能開銷比較大,整個過程數(shù)據(jù)需要從 GPU -> CPU -> GPU,這部分的代價是比較大的。

如何開發(fā)一個 platform view

其實 Flutter 官方維護了一些 plugin,鏈接如下:
https://github.com/flutter/plugins

其中的 webview_flutter 、google_maps_flutter 就是通過 platform view,就是一個很好的 demo 。

Q4: 動態(tài)加載技術(shù)?

1、什么是動態(tài)加載技術(shù)?

動態(tài)加載技術(shù)就是使用類加載器加載相應(yīng)的apk、dex、jar(必須含有dex文件),再通過反射獲得該apk、dexjar內(nèi)部的資源(class、圖片、color等等)進而供宿主app使用。它的優(yōu)點可以讓應(yīng)用程序?qū)崿F(xiàn)插件化插拔式結(jié)構(gòu)

2、關(guān)于動態(tài)加載使用的類加載器

使用動態(tài)加載技術(shù)時,一般需要用到這兩個類加載器:

  • PathClassLoader - 只能加載已經(jīng)安裝的apk,即/data/app目錄下的apk。
  • DexClassLoader - 能加載手機中未安裝的apk、jar、dex,只要能在找到對應(yīng)的路徑。
    這兩個加載器分別對應(yīng)使用的場景各不同,所以接下來,分別講解它們各自加載相同的插件apk的使用。
3、使用PathClassLoader加載已安裝的apk插件,獲取相應(yīng)的資源供宿主app使用

1. 首先我們需要知道一個manifest中的屬性:SharedUserId。

SharedUserId

該屬性是用來干嘛的呢?簡單的說,應(yīng)用從一開始安裝在Android系統(tǒng)上時,系統(tǒng)都會給它分配一個linux user id,之后該應(yīng)用在今后都將運行在獨立的一個進程中,其它應(yīng)用程序不能訪問它的資源,那么如果兩個應(yīng)用的sharedUserId相同,那么它們將共同運行在相同的linux進程中,從而便可以數(shù)據(jù)共享、資源訪問了。所以我們在宿主app和插件app的manifest上都定義一個相同的sharedUserId。

2、那么我們將插件apk安裝在手機上后,宿主app怎么知道手機內(nèi)該插件是否是我們應(yīng)用程序的插件呢?

我們之前是不是定義過插件apk也是使用相同的sharedUserId,那么,我就可以這樣思考了,是不是可以得到手機內(nèi)所有已安裝apk的sharedUserId呢,然后通過判斷sharedUserId是否和宿主app的相同,如果是,那么該app就是我們的插件app了。確實是這樣的思路的,那么有了思路最大的問題就是怎么獲取一個應(yīng)用程序內(nèi)的sharedUserId了,我們可以通過PackageInfo.sharedUserId來獲取,請看代碼:

    /** 
     * 查找手機內(nèi)所有的插件 
     * @return 返回一個插件List 
     */  
    private List<PluginBean> findAllPlugin() {  
        List<PluginBean> plugins = new ArrayList<>();  
        PackageManager pm = getPackageManager();  
        //通過包管理器查找所有已安裝的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;  
            //判斷這個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;  
    }  

通過這段代碼,我們就可以輕松的獲取手機內(nèi)存在的所有插件,其中PluginBean是定義的一個實體類而已,就不貼它的代碼了。

3、如果找到了插件,就把可用的插件顯示出來了,如果沒有找到,那么就可提示用戶先去下載插件什么的。

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, "沒有找到插件,請先下載!", Toast.LENGTH_SHORT).show();  
}  
showEnableAllPluginPopup(datas);  

4、如果找到后,那么我們選擇對應(yīng)的插件時,在宿主app中就加載插件內(nèi)對應(yīng)的資源,這個才是PathClassLoader的重點。我們首先看看怎么實現(xiàn)的吧:

    /** 
     * 加載已安裝的apk 
     * @param packageName 應(yīng)用的包名 
     * @param pluginContext 插件app的上下文 
     * @return 對應(yīng)資源的id 
     */  
    private int dynamicLoadApk(String packageName, Context pluginContext) throws Exception {  
        //第一個參數(shù)為包含dex的apk或者jar的路徑,第二個參數(shù)為父加載器  
        PathClassLoader pathClassLoader = new PathClassLoader(pluginContext.getPackageResourcePath(),ClassLoader.getSystemClassLoader());  
//        Class<?> clazz = pathClassLoader.loadClass(packageName + ".R$mipmap");//通過使用自身的加載器反射出mipmap類進而使用該類的功能  
        //參數(shù):1、類的全名,2、是否初始化類,3、加載時使用的類加載器  
        Class<?> clazz = Class.forName(packageName + ".R$mipmap", true, pathClassLoader);  
        //使用上述兩種方式都可以,這里我們得到R類中的內(nèi)部類mipmap,通過它得到對應(yīng)的圖片id,進而給我們使用  
        Field field = clazz.getDeclaredField("one");  
        int resourceId = field.getInt(R.mipmap.class);  
        return resourceId;  
    }  

這個方法就是加載包名為packageName的插件,然后獲得插件內(nèi)名為one.png的圖片的資源id,進而供宿主app使用該圖片。現(xiàn)在我們一步一步來講解一下:

  • 首先就是new出一個PathClassLoader對象,它的構(gòu)造方法為:
public PathClassLoader(String dexPath, ClassLoader parent)  

中其中第一個參數(shù)是通過插件的上下文來獲取插件apk的路徑,其實獲取到的就是/data/app/apkthemeplugin.apk,那么插件的上下文怎么獲取呢?在宿主app中我們只有本app的上下文啊,答案就是為插件app創(chuàng)建一個上下文:

 Context pluginContext = createPackageContext(packageName, CONTEXT_IGNORE_SECURITY | CONTEXT_INCLUDE_CODE);

通過插件的包名來創(chuàng)建上下文,不過這種方法只適合獲取已安裝的app上下文?;蛘卟恍枰ㄟ^反射直接通過插件上下文getResource().getxxx(R..);也行,而這里用的是反射方法。第二個參數(shù)是父加載器,都是ClassLoader.getSystemClassLoader()。

  • 插件app的類加載器我們創(chuàng)建出來了,接下來就是通過反射獲取對應(yīng)類的資源了,這里我是獲取R類中的內(nèi)部類mipmap類,然后通過反射得到mipmap類中名為one的字段的值,



    然后通過

plugnContext.getResources().getDrawable(resouceId)  

就可以獲取對應(yīng)id的Drawable得到該圖片資源進而宿主app的可用它設(shè)置背景等。
下面演示下該demo效果,在沒有插件情況下會提示請先下載插件,有插件時候就選擇對應(yīng)的插件而供宿主app使用,本demo是換背景的功能演示,我來看宿主app中mipmap文件夾下并沒有one.png這張圖片。

4、DexClassLoader加載未安裝的apk,提供資源供宿主app使用

關(guān)于動態(tài)加載未安裝的apk,我先描述下思路:首先我們得到事先知道我們的插件apk存放在哪個目錄下,然后分別得到插件apk的信息(名稱、包名等),然后顯示可用的插件,最后動態(tài)加載apk獲得資源。
按照上面這個思路,我們需要解決幾個問題:

  • 怎么得到未安裝的apk的信息
  • 怎么得到插件的context或者Resource,因為它是未安裝的不可能通過createPackageContext(...);方法來構(gòu)建出一個context,所以這時只有在Resource上下功夫。

現(xiàn)在我們就一一來解答這些問題吧:
1、得到未安裝的apk信息可以通過mPackageManager.getPackageArchiveInfo()方法獲得,

public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags) 

它的參數(shù)剛好是傳入一個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;//版本號  
            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、得到對應(yīng)未安裝apk的Resource對象,我們需要通過反射來獲得:

    /** 
     * @param apkName  
     * @return 得到對應(yīng)插件的Resource對象 
     */  
    private Resources getPluginResources(String apkName) {  
        try {  
            AssetManager assetManager = AssetManager.class.newInstance();  
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);//反射調(diào)用方法addAssetPath(String path)  
            //第二個參數(shù)是apk的路徑:Environment.getExternalStorageDirectory().getPath()+File.separator+"plugin"+File.separator+"apkplugin.apk"  
            addAssetPath.invoke(assetManager, apkDir+File.separator+apkName);//將未安裝的Apk文件的添加進AssetManager中,第二個參數(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;  
    }  

通過得到AssetManager中的內(nèi)部的方法addAssetPath,將未安裝的apk路徑傳入從而添加進assetManager中,然后通過new Resource把assetManager傳入構(gòu)造方法中,進而得到未安裝apk對應(yīng)的Resource對象。

好了!上面兩個問題解決了,那么接下來就是加載未安裝的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)建一個名為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存儲的目錄,3、本地library庫目錄,一般為null,4、父ClassLoader  
        DexClassLoader dexClassLoader = new DexClassLoader(apkDir+File.separator+apkName, optimizedDirectoryFile.getPath(), null, ClassLoader.getSystemClassLoader());  
        Class<?> clazz = dexClassLoader.loadClass(apkPackageName + ".R$mipmap");//通過使用apk自己的類加載器,反射出R類中相應(yīng)的內(nèi)部類進而獲取我們需要的資源id  
        Field field = clazz.getDeclaredField("one");//得到名為one的這張圖片字段  
        int resId = field.getInt(R.id.class);//得到圖片id  
        Resources mResources = getPluginResources(apkName);//得到插件apk中的Resource  
        if (mResources != null) {  
            //通過插件apk中的Resource得到resId對應(yīng)的資源  
            findViewById(R.id.background).setBackgroundDrawable(mResources.getDrawable(resId));  
        }  
    }  

其中通過new DexClassLoader()來創(chuàng)建未安裝apk的類加載器,我們來看看它的參數(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)建一個dexOutputDir。

  • libraryPath - 本地的library,一般為null
  • parent - 父加載器
    接下來,就是通過反射的方法,獲取出需要的資源。
    再看看拷貝了三個插件:
copyApkFile("apkthemeplugin-1.apk");  
copyApkFile("apkthemeplugin-2.apk");  
copyApkFile("apkthemeplugin-3.apk");  

可以看到只要一有插件下載,就能顯示出來并使用它。

當(dāng)然插件化開發(fā)并不只是像只有這種換膚那么簡單的用途,這只是個demo,學(xué)習(xí)這種插件化開發(fā)思想的。由此可以聯(lián)想,這種插件化的開發(fā),是不是像QQ里的表情包啊、背景皮膚啊,通過線上下載線下維護的方式,可以在線下載使用相應(yīng)的皮膚,不使用時候就可以刪了,所以插件化開發(fā)是插件與宿主app進行解耦了,即使在沒有插件情況下,也不會對宿主app有任何影響,而有的話就供用戶選擇性使用了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容