插件化技术的演进之路-程序员宅基地

注:底稿,PPT地址:http://slides.vimerzhao.top/006-android-plugin-tech-share.html

编号006。

序言

关于本次分享

今天要和大家分享的是Android的插件化技术,这在Android中其实是一个相对来说比较复杂,历史悠久,内容庞杂的知识,不是今天一个小时能讲完的,所以我今天也是有选择地分享一些我认为比较重要的内容。

在坐的大多有3年以上的开发经验,对于插件化技术也或多或少有所了解,相反是我入行时间更短一点,所以今天分享不会是单方面的知识灌输,更准确地说是我把最近的一些收获、心得整理出来,抛砖引玉,能让大家有所启发,并产生一些有益的讨论。

下面进入正题,今天大概会讨论5部分的内容,分别是:

1.插件化的前置知识2.插件化的发展历史3.Activity的framework源码与插件化实现4.各大插件化方案的借鉴与革新5.对于插件化的几点思考

需要事先告知的是,接下来一个小时的内容其实是比较艰深晦涩的,所以希望大家稍微集中注意力,我其实是不太喜欢信息密度过大的分享的,但是限于时间,只能做一些妥协了。

插件化的前置知识

这一部分,每一个基础知识都可以单独拎出来讲上一个小时的,所以我只是简单提及一下,不会深入讲解,平时大家也多少有些了解,这就足够应付接下来的内容了

什么是插件化(What)?  不依赖安装,动态加载Native功能。

为什么要插件化(Why)?  动态发版、包大小精简、逻辑解耦、编译提速?

方向提问:带来这些好处的背后,有哪些挑战? 客户端稳定性,开发体验。

什么时候出现插件化(When)?  2012年。

插件化在哪些地方盛行(Where)?  国内,手淘、携程、滴滴等大型App

谁在做插件化(Who)?  前期个人开发者,后期大公司。

怎么做插件化(How)?  总的来说,插件化分Hook派和静态代理派,Hook派涉及的基础知识会更多一些。

Apk打包流程(Gradle、aapt)

有一些方案会在编译器修改插件的字节码,还有些方案在编译器做资源重排,所以要了解此部分内容。

Apk安装流程(PMS、Manifest解析、dexopt)

类似DroidPlugin、VirutalApp等方案,模拟了系统安装apk等流程,所以这部分需要了解。

四大组件的管理(AMS,主要是启动流程、生命周期)

插件化的核心就是四大组件的插件化,所以这部分必须了解。

资源、so库的加载机制

同上。

IPC/Binder

Hook派插件化要和system_process进程打交道,而比较成熟的方案都会把插件作为一个单独的进程,所以这部分也要了解。

类加载机制

加载插件的重要环节就是加载插件类,这个是任何方案都无法避免的。

反射与动态代理

Hook派插件必须用到的Java技术。

插件化的发展历史

下面的表格中,我梳理了插件化技术各开源方案的出现时间,主要参考自前人的文章和自己对相关新闻以及代码提交记录的整理(这部分有点考究历史资料的感觉),一个方案的出现不适一蹴而就的,公布时间和开始开发时间肯定有个跨度,所有过于计较时间的精确度意义不大,但输出出时间脉络也有助于我们获得一些信息。另外,之前网上也有一些版本,但是都比较老旧了,而且有些遗漏。我有信心说是目前(2020年5月)最完善、最新的就是我整理的这张表了。

时间 方案 介绍 主体
2012年7月 mmin18/AndroidDynamicLoader[1] A plugin system that runs like a browser, but instead of load web pages, it load apk plugins which runs natively on Android system. 大众点评/屠毅敏
2013年 23Code - -
2014年初 alibaba/atlas[2] A powerful Android Dynamic Component Framework. 阿里/伯奎
2014年7月 houkx/android-pluginmgr[3] apk plug apkplug apk load houkx
2014年底 singwhatiwanna/dynamic-load-apk[4] DL : dynamic load framework in android 百度/任玉刚
2014年11月 Direct-Load-apk[5] a very powerful plugin framework, through the use of it, you can achieve incredible function -----load directly from a basic apk! 罗迪
2015年4月 HiWong/OpenAtlas[6] OpenAtlasCore,Android Component Dynamic Deployment(plugin) Framework BunnyBlue
2015年8月 DroidPluginTeam/DroidPlugin[7] A plugin framework on android,Run any third-party apk without installation, modification or repackage 360/张勇
2015年底 CtripMobile/DynamicAPK[8] Solution to implement multi apk dynamic loading and hot fixing for Android App. -
2015年底 wequick/Small[9] A small framework to split app into small parts 林广亮
2016年7月 asLody/VirtualApp[10] Virtual Engine for Android(Support 10.0 in business version) 罗迪
2017年6月 didi/VirtualAPK[11] A powerful and lightweight plugin framework for Android -
2017年7月 Qihoo360/RePlugin[12] RePlugin - A flexible, stable, easy-to-use Android Plug-in Framework -
2018年10月 ManbangGroup/Phantom[13] Phantom — 唯一零 Hook 稳定占坑类 Android 热更新插件化方案 -
2019年6月 Tencent/Shadow[14] 零反射全动态Android插件框架 -

粗略看这种表格,我个人有以下发现,欢迎补充:

•早期野蛮生长,以个人名义开源为主;后期逐渐成熟,以公司名义开源为主。前期注重方案实现,后期注重方案可用性(如RePlugin的口号中提到的“flexible, stable, easy-to-use”)。•16年是一个分水岭,其后的方案都是优化(具体优化后面会说),没有突破性的创新。•16年之前,描述侧重框架特色,并没有统一使用Plugin Framework这个词,16年之后插件化一词深入人心,侧重描述方案的完备性、稳定性、灵活性等实际诉求。

那下载估计大家有点晕了,我们所知道的插件化原理其实就那么几板斧,面对出现过的这么多方案,我们该怎么消化呢?

这也是我结下来内容安排的原因,我们先来看看插件化最核心的能力:Activity在framework层是怎么处理,再来逐一分析每个方案的借鉴与创新,而不是孤立地看待每个方案,相信这样会轻松很多。

Activity的framework源码与插件化实现

为什么从原理到实现方案都重点分析Activity?因为它是最重要、最常用的、最复杂的组件,也是插件化方案的核心,最简单的app可以只有Activity。

启动流程

Activity的启动流程算是我们面试的必问题了,相信大家也比较清楚。这里我做了一个单步调试的视频,帮助大家回忆一下。总结来说,Activity的启动流程可以用下面这张图来概括:

图片来自:https://juejin.im/post/5c7f3471f265da2db5425c4b

活动管理

关于Activity,我觉得还有一个点可以单独拿出来讨论,就是其栈管理机制,比较完备的插件化方案是无法避免这个问题的,比如DroidPlugin里面就有这样的代码:

......
 <activity
            android:name=".stub.ActivityStub$P00$Standard00"
            android:allowTaskReparenting="true"
            android:excludeFromRecents="true"
            android:exported="false"
            android:hardwareAccelerated="true"
            android:label="@string/stub_name_activity"
            android:launchMode="standard"
            android:noHistory="true"
            android:theme="@style/DroidPluginTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="com.morgoo.droidplugin.category.PROXY_STUB" />
            </intent-filter>
            <meta-data
                android:name="com.morgoo.droidplugin.ACTIVITY_STUB_INDEX"
                android:value="0" />
        </activity>
        <activity
            android:name=".stub.ActivityStub$P00$SingleInstance00"
            android:allowTaskReparenting="true"
            android:excludeFromRecents="true"
            android:exported="false"
            android:hardwareAccelerated="true"
            android:label="@string/stub_name_activity"
            android:launchMode="singleInstance"
            android:theme="@style/DroidPluginTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="com.morgoo.droidplugin.category.PROXY_STUB" />
            </intent-filter>
            <meta-data
                android:name="com.morgoo.droidplugin.ACTIVITY_STUB_INDEX"
                android:value="0" />
        </activity>
......


VirtualAPK、RePlugin里面也有类似的。可见各家方案,都有兼容各种launch的野心~

那我们来看下framework层是怎么实现的呢?借用gityuan和大圣丶的两张图可以清楚得表面其设计:

具体源码在startActivityUnchecked[15],非常繁杂,这里我就不多做分析了,大家知道宏观示意即可。

看过一些源码之后,我发现VirtualApp的实现是最完善的,在此不做具体分析,简单来说就它也像AMS一样记录了Activity栈的变化(相当于把AMS的数据做了一份快照在自己的进程,不知道能不能直接获取AMS的数据,应该没有这样的接口~),并根据launchMode/Flag触发onNewIntent之类的回调,可以说是所有方案里面完成度最高的了,其他方案都只能支持比较简单的launchMode。

各大插件化方案的借鉴与革新

开始下面的内容之前,有必要区分本质问题与衍生问题:

•本质问题:如何启动插件Activity,加载资源、so,实现生命周期回调等•衍生问题:插件管理、安装、加载,页面路由等

mmin18/AndroidDynamicLoader

•借助fragment实现插件化•反射addAssetPath实现资源插件化出现

    static MyResources getResource(MyClassLoader mcl) {
        ......
        try {
            AssetManager am = (AssetManager) AssetManager.class.newInstance();
            am.getClass().getMethod("addAssetPath", String.class).invoke(am, path.getAbsolutePath());
            Resources superRes = MyApplication.instance().getResources();
            Resources res = new Resources(am, superRes.getDisplayMetrics(), superRes.getConfiguration());
            ......
        }


23Code

Pass

alibaba/atlas

•开始出现了对framework的Hook,对后面的方案影响深远。下图是伯奎当时的分享•开源的版本17年才公布,和分享时的设计已经差别较大了

houkx/android-pluginmgr

•这个方案很有趣,但很少被提及,首先Hook掉ClassLoader,通过DexMaker在客户端修改插件类(变成一个已经在Manifest注册过的类)•但是客户端做这个事情效率很低,事实上后来的RePlugin、Shadow又走回了这条路,只是放在了编译期

From: https://blog.csdn.net/hkxxx/article/details/42194387

singwhatiwanna/dynamic-load-apk

•用静态代理实现,第一个完成度比较高的插件化方案•但是包括that关键字在内的设计,对开发侵入性较大

Direct-Load-apk

•lody早期的项目,针对上两种方案的优化(开发体验)•试图通过把StubActivity的数据给插件Activity,让插件Activity能像正常Activit一样,参见下面的核心代码:

    public void dispatchProxyToPlugin() {
        try {
            //开始伪装插件为实体Activity
            pluginRef.set("mBase", proxy);
            pluginRef.set("mDecor", proxyRef.get("mDecor"));
            pluginRef.set("mTitleColor", proxyRef.get("mTitleColor"));
            ......
            Instrumentation instrumentation = proxyRef.get("mInstrumentation");
            pluginRef.set("mInstrumentation", new LPluginInsrument(instrumentation));
            ......
            pluginRef.set("mUiThread", proxyRef.get("mUiThread"));
            pluginRef.set("mHandler", proxyRef.get("mHandler"));
            pluginRef.set("mInstanceTracker", proxyRef.get("mInstanceTracker"));
            ......
        } catch (ReflectException e) {
            e.printStackTrace();
        }


    }


HiWong/OpenAtlas

•作者通过逆向手淘代码,实现的“山寨版”atlas,由于一些原因删库了,后来变成了ACDD,算是OpenAtlas的优化版•公开了一个hook编译工具aapt来解决资源冲突的实现,对后面的方案影响深远•比较遗憾的是插件必须注册在Manifest中,这和插件化的精神有所背离,所以对外宣称是容器化,即容器内的Bundle可以升级更新•绕过Activity启动检查的方案初具雏形,开始Hook一些我们耳熟能详的方法,参考核心代码:

    /****
     *
     * public ActivityResult execStartActivity( Context who, IBinder
     * contextThread, IBinder token, Activity target, Intent intent, int
     * requestCode);
     * ***/
    public InstrumentationHook(Instrumentation instrumentation, Context context) {
        this.context = context;
        this.mBase = instrumentation;
        try {
            mInstrumentationInvoke = Hack.into("android.app.Instrumentation");
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
                mExecStartActivity = mInstrumentationInvoke.method("execStartActivity", Context.class,
                        IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
            } else {
                mExecStartActivity = mInstrumentationInvoke.method("execStartActivity", Context.class,
                        IBinder.class, IBinder.class, Activity.class, Intent.class, int.class);
            }
            ......
        } catch (HackAssertionException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        }
    }


From: https://alibaba.github.io/atlas/principle-intro/Runtime_principle.html

DroidPluginTeam/DroidPlugin

•第一个Hook流派的完成度非常高的版本,几乎Hook了AMS、PMS等相关服务•可以真正地加载一个完全独立的第三方apk文件•从这里开始,插件化具备了成为沙盒/虚拟机的可能,而不是宿主的能力补充

调用AMS前:

        protected boolean doReplaceIntentForStartActivityAPIHigh(Object[] args) throws RemoteException {
            int intentOfArgIndex = findFirstIntentIndexInArgs(args);
            if (args != null && args.length > 1 && intentOfArgIndex >= 0) {
                Intent intent = (Intent) args[intentOfArgIndex];
                //XXX String callingPackage = (String) args[1];
                if (!PluginPatchManager.getInstance().canStartPluginActivity(intent)) {
                    PluginPatchManager.getInstance().startPluginActivity(intent);
                    return false;
                }
                ActivityInfo activityInfo = resolveActivity(intent);
                if (activityInfo != null && isPackagePlugin(activityInfo.packageName)) {
                    ComponentName component = selectProxyActivity(intent);
                    if (component != null) {
                        Intent newIntent = new Intent();
                        try {
                            ClassLoader pluginClassLoader = PluginProcessManager.getPluginClassLoader(component.getPackageName());
                            setIntentClassLoader(newIntent, pluginClassLoader);
                        } catch (Exception e) {
                            Log.w(TAG, "Set Class Loader to new Intent fail", e);
                        }
                        newIntent.setComponent(component);
                        newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent);
                        newIntent.setFlags(intent.getFlags());




                        String callingPackage = (String) args[1];
                        if (TextUtils.equals(mHostContext.getPackageName(), callingPackage)) {
                            newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        args[intentOfArgIndex] = newIntent;
                        args[1] = mHostContext.getPackageName();
                    } else {
                        Log.w(TAG, "startActivity,replace selectProxyActivity fail");
                    }
                }
            }


            return true;
        }




AMS反调回来

    @Override
    public boolean handleMessage(Message msg) {
        long b = System.currentTimeMillis();
        try {
            if (!mEnable) {
                return false;
            }


            if (PluginProcessManager.isPluginProcess(mHostContext)) {
                if (!PluginManager.getInstance().isConnected()) {
                    Log.i(TAG, "handleMessage not isConnected post and wait,msg=%s", msg);
                    mOldHandle.sendMessageDelayed(Message.obtain(msg), 5);
                    return true;
                }
            }


            if (msg.what == LAUNCH_ACTIVITY) {
                return handleLaunchActivity(msg);
            }
            if (mCallback != null) {
                return mCallback.handleMessage(msg);
            } else {
                return false;
            }
        } finally {
            Log.i(TAG, "handleMessage(%s,%s) cost %s ms", msg.what, codeToString(msg.what), (System.currentTimeMillis() - b));


        }
    }




CtripMobile/DynamicAPK

•主要借鉴了OpenAtlas•aapt部分有所创新

wequick/Small

•提供了一种通过脚本的方式解决资源冲突•Activity依然是通过Hook的方式,没有特别大的改动

asLody/VirtualApp

•算是对DroidPlugin的一次大升级,把虚拟framework完整地实现了,在源码命名和结构上就有所体现了

didi/VirtualAPK

•综合考虑了大量Hook导致的不稳定问题和静态代理的开发侵入性,又开始回归到仅Hook mInstrumentation 的设计,主要是execStartActivity 和 newActivity•Service主要用两个真实的Service做转调•ContentProvider通过一个代理Provider进行操作的分发

Qihoo360/RePlugin

•更进一步,One Hook,结合编译期的修改,把插件变成类似静态代理的类(像是把houkx/android-pluginmgr的策略换了个地方),同时Hook了ClassLoader使之加载插件的类:

•记录下目标页 ActivityA,替换成已自动注册在 AndroidManifest 中的坑位 ActivityNS。•在 ClassLoader 中拦截ActivityNS的创建,创建出ActivityA返回。•返回的ActivityA占用着 ActivityNS 这个坑位, 坑位由 Gradle 编译时自动生成在AndroidManifest中。

•主打的是稳定性和兼容性,文档资料相对来说比较健全

Q:您们和360之前发的DroidPlugin的主要区别是什么?A:这个问题问得很好。很多人都有这个疑惑——“为什么你们360要开发两套不同的插件化框架呢”?其实归根结底,最根本的区别是——目标的不同:DroidPlugin主要解决的是各个独立功能拼装在一起,能够快速发布,其间不需要有任何的交互。目前市面上的一些双开应用,和DroidPlugin的思路有共同之处。当然了,要做到完整的双开,则仍需要大量的修改,如Native Hook等。RePlugin解决的是各个功能模块能独立升级,又能需要和宿主、插件之间有一定交互和耦合。此外,从技术层面上,其最核心的区别就一个:Hook点的多少。DroidPlugin可以做到让APK“直接运行在主程序”中,无需任何额外修改。但需要Hook大量的API(包括AMS、PackageManager等),在适配上需要做大量的工作。RePlugin只Hook了ClassLoader,所以极为稳定,且同样支持绝大多数单品的特性,但需要插件做“少许修改”。好在作为插件开发者而言无需过于关心,因为通过“动态编译方案”,开发者可做到“无需开发者修改Java Code,即可运行在主程序中”的效果。可以肯定的是,DroidPlugin也是一款业界公认的,优秀的免安装插件方案。我相信,随着时间的推移,RePlugin和DroidPlugin会分别在各自领域(全面插件化 & 应用免安装)打造出属于自己的一番天地。回答者:@张炅轩

From: https://github.com/Qihoo360/RePlugin/wiki/FAQ

From:https://github.com/Qihoo360/RePlugin/pull/840 在看shadow的时候,发现其作者提出了一个改进,但是并未被接受,这个死循环也没有解释原因,感觉维护者这个态度有问题啊~个人感觉这个改的挺好的

ManbangGroup/Phantom

•比360的RePlugin更进一步,宣称零Hook•相关资料较少,暂未深入研究。除了一篇宣传性质的README,没有什么实质资料•分析其代码后发现还是拓展了RePlugin

编译期替换,静态代理

Tencent/Shadow

•同样是零Hook,同时宣称支持插件框架本身的动态化(这个其实也不难)•更像是RePlugin的另一个种实现,没有本质的不同

对于插件化的几点思考

前面,我们快速看完了每一个插件化的主要特点,隐隐约约已有所感悟,似乎应了《三国演义》的那句“合久必分、分久必合”,很多方案看似新颖,但无不是在前人的基础上,根据自己的业务做一些创新,论里程碑的仁者见仁、智者见智,但切不可被花里胡哨的项目README.md迷惑双眼。

从脉络上来看,从静态代理、Hook framework各自的蓬勃发展,到最近几年,两个方案互相取长补短,总体似乎是朝着客户端轻Hook+编译期侵入修改的方向走去,下面我来拆分几个维度,具体讨论一下。

开发期or运行期

想把我们的插件Activity像正常Activity一样启动,做一些侵入性的修改是不可避免的,在我看来,一路发展过来,主要分了三个方向:

•以dynamic-load-apk为代表的,开发期侵入•以DroidPlugin为代表的,运行期侵入•以RePlugin、shadow为代表的,编译期侵入

编译期侵入算是开发期侵入的自动化版本,既避免了不稳定的Hook,也可以保证开发人员的体验;运行期侵入对于追求稳定来说,始终是一个挑战。

独立or合并

对于ClassLoader和Resource都存在这个问题,有些方案会选择公用类加载器或者资源,这样就要解决插件和宿主,插件和插件的潜在冲突,aapt的魔改正是为了处理这种情况。特别是Resource,独立可以避免冲突,共用则能降低插件size。我觉得还是要区分场景,DroidPlugin这种每个插件都很独立,适合分开加载;如果插件是宿主功能的一个补充,则可以共用。Shawdow甚至通过白名单来控制是隔离还是共用ClassLoader。

动态化or插件化

插件化和动态化多少有一些共通之处,他们都可以降低宿主大小、动态发版等,目前来看,插件化的优势是涉及到四大组件的时候更合适,而动态化则适合纯View级别的修改。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_45899674/article/details/106168912

智能推荐

淘金优化算法GRO求解不闭合MD-MTSP,可以修改旅行商个数及起点(提供MATLAB代码)-程序员宅基地

文章浏览阅读607次,点赞7次,收藏9次。第5个旅行商的路径:18->14->20->25->7->23->27->8->24。第4个旅行商的路径:16->13->4->18->14->17->22->11。第3个旅行商的路径:15->20->10->19->25->7->23。% 最大迭代次数(可以修改)第2个旅行商的路径:5->6->12->28->8->24->27。第1个旅行商的路径:1->21->2->29->3->26->9。第3个旅行商的路径:15->4->13->10->2。第1个旅行商的路径:1->28->6->12->9。

使用docker打包python项目并在本地模拟部署aws lambda-程序员宅基地

文章浏览阅读1.9k次,点赞2次,收藏7次。本文主要记录使用docker打包python项目并部署到lambda的流程以及遇到的一些问题。_docker打包python项目

Linux离线安装 elasticdump工具_npm-cache.tar-程序员宅基地

文章浏览阅读7.7k次,点赞4次,收藏9次。elasticdump是一个对elasticsearch进行数据导入导出的工具安装包:https://download.csdn.net/download/fanzhijian110/11261855https://download.csdn.net/download/fanzhijian110/11261850node-v10.16.0-linux-x64.tar.xz 这个包..._npm-cache.tar

新洞察 - 智能产品行业出海趋势分享-程序员宅基地

文章浏览阅读621次,点赞29次,收藏20次。在智能产品行业出海的浪潮中,中国企业正扮演着重要角色。这个快速增长的行业正在经历着多样化产品形态、人工智能技术融合、生态化趋势以及隐私安全重视等发展趋势。中国智能产品出口规模持续扩大,不仅在传统市场表现强劲,新兴市场也呈现出令人鼓舞的增长势头。随着国际渗透率的提高,中国智能产品正逐步获得全球消费者的认可。为了在国际市场上取得成功,企业需要加快本地化布局,融入当地市场,提供优质的组织、营销和售后服务。在这个充满机遇的时代,把握住智能产品行业出海的红利,将为中国企业开启新的增长空间。

centos8安装mysql报错:The GPG keys listed for the “MySQL 8.0 Community Server“ repository are already ins_the gpg keys listed for the "mysql 8.0 community s-程序员宅基地

文章浏览阅读3.6k次。目录 centos8安装mysql报错:The GPG keys listed for the "MySQL 8.0 Community Server" repository are already installed but they are not correct for this package. 安装sql命令如下:原因分析:解决办法:1.可以先尝试这个:2.然后再执行: 3.不行的话,可以用这个:注意事项:报错信息、报错截图示下: 如上述命令,要安装MySQL数_the gpg keys listed for the "mysql 8.0 community server" repository are alre

js当前系统时间转换成日期格式转换成YYYYMMDD格式_js日期格式转换yyyymmdd-程序员宅基地

文章浏览阅读1.1k次。修改系统时间为指定格式_js日期格式转换yyyymmdd

随便推点

华为官方推荐:学习鸿蒙开发高分好书,这6本能帮你很多!_鸿蒙开发 推荐 书籍-程序员宅基地

文章浏览阅读1.3k次,点赞21次,收藏13次。华为官方推荐:这6本鸿蒙高分好书,99%的小白都看得懂!随着科技的日新月异,华为鸿蒙系统(HarmonyOS)已经成为了全球科技领域的热点话题。作为一个新兴的操作系统,鸿蒙正吸引着越来越多的人们的关注。为了帮助大家更好地了解鸿蒙系统及其背后的技术原理,华为官方推荐了6本鸿蒙高分好书,这些书籍通俗易懂,即使是99%的小白也能轻松入门!_鸿蒙开发 推荐 书籍

踩坑笔记_TabLayout只显示一个Tab问题_安卓tablayout 和 viewpager只有1个标签没有填满屏幕-程序员宅基地

文章浏览阅读1.4k次。问题: 在使用TabLayout开发的过程中遇到了一个奇怪的错误,根据网上资料写的一个TabLayout的Demo可以正常关连ViewPager并生成多个TabLayout. 然而在把Demo代码拷贝到一个项目中运行后界面就只显示一个Tab而ViewPager正常。如图1 图1解决过程: 1.初步猜测可能是框架问题,项目使用..._安卓tablayout 和 viewpager只有1个标签没有填满屏幕

VUE3面试题及知识点,并且带答案!_vue3面试题必问题和答案-程序员宅基地

文章浏览阅读4.2k次,点赞5次,收藏27次。答:Vue 3.0是Vue.js框架的最新版本。Composition API:提供更灵活的逻辑组织方式,使组件更易于复用和测试。更好的性能:Vue 3.0使用了重写的响应式系统,使得渲染速度更快、内存占用更小。改进的TypeScript支持:Vue 3.0在TypeScript方面做出了改进,允许开发者更好地利用TypeScript的类型检查功能。答:在Vue 3.0中,可以使用directive方法注册自定义指令。具体来说,可以将一个包含mountedupdated和unmounted。_vue3面试题必问题和答案

Ubuntu 12.04 LTS 中文输入法的安装_ubuntu12.04如何安装中文输入法-程序员宅基地

文章浏览阅读1.9k次。转自:http://blog.csdn.net/muyang_ren/article/details/39211201本文是笔者使用 Ubuntu 操作系统写的第一篇文章!参考了红黑联盟的这篇文章:Ubuntu 12.04中文输入法的安装安装 Ubuntu 12.04 着实费力一番功夫,老是在用 Ubuntu 来引导 Windows,结果 Ubuntu 倒是能用,一进入 W_ubuntu12.04如何安装中文输入法

计算机组成原理-程序员宅基地

文章浏览阅读33次。经常使用的程序会从主存想cache复制写入一份,能够更快的更频繁的使用。辅存的速度越快,辅存读入主存的速度也就会越快,开机的速度也就越快。

数据挖掘与数据分析-程序员宅基地

文章浏览阅读1k次,点赞10次,收藏18次。1、数据挖掘(Data Mining)数据挖掘是指对大规模数据进行分析,以发现其中潜在的模式、规律或关联性的过程。其目的在于从数据中提取有价值的信息,以支持决策制定、预测未来趋势等。数据挖掘涉及多种技术和方法,包括机器学习、统计分析、数据库技术等。2、数据分析(Data Analysis)数据分析是指对数据进行收集、清洗、转换和建模等处理,以获得对问题的洞察和理解的过程。数据分析旨在揭示数据背后的意义,为决策提供支持和指导。它可以采用多种统计和计算方法,如描述性统计、推断统计、预测分析等。

推荐文章

热门文章

相关标签