欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

淺談Android插件化

 更新時(shí)間:2021年09月24日 10:21:02   作者:QiShare  
插件化技術(shù)最初源于免安裝運(yùn)行 Apk的想法,這個(gè)免安裝的 Apk 就可以理解為插件,而支持插件的 app 我們一般叫 宿主,下面就跟著小編一起學(xué)習(xí)Android插件化吧,希望能幫助到你

一、認(rèn)識(shí)插件化

1.1 插件化起源

插件化技術(shù)最初源于免安裝運(yùn)行 Apk的想法,這個(gè)免安裝的 Apk 就可以理解為插件,而支持插件的 app 我們一般叫 宿主。

想必大家都知道,在 Android 系統(tǒng)中,應(yīng)用是以 Apk 的形式存在的,應(yīng)用都需要安裝才能使用。但實(shí)際上 Android 系統(tǒng)安裝應(yīng)用的方式相當(dāng)簡(jiǎn)單,其實(shí)就是把應(yīng)用 Apk 拷貝到系統(tǒng)不同的目錄下、然后把 so 解壓出來(lái)而已。

常見(jiàn)的應(yīng)用安裝目錄有:

  • /system/app:系統(tǒng)應(yīng)用
  • /system/priv-app:系統(tǒng)應(yīng)用
  • /data/app:用戶(hù)應(yīng)用

那可能大家會(huì)想問(wèn),既然安裝這個(gè)過(guò)程如此簡(jiǎn)單,Android 是怎么運(yùn)行應(yīng)用中的代碼的呢,我們先看 Apk 的構(gòu)成,一個(gè)常見(jiàn)的 Apk 會(huì)包含如下幾個(gè)部分:

  • classes.dex:Java 代碼字節(jié)碼
  • res:資源文件
  • lib:so 文件
  • assets:靜態(tài)資產(chǎn)文件
  • AndroidManifest.xml:清單文件

其實(shí) Android 系統(tǒng)在打開(kāi)應(yīng)用之后,也只是開(kāi)辟進(jìn)程,然后使用 ClassLoader 加載 classes.dex 至進(jìn)程中,執(zhí)行對(duì)應(yīng)的組件而已。

那大家可能會(huì)想一個(gè)問(wèn)題,既然 Android 本身也是使用類(lèi)似反射的形式加載代碼執(zhí)行,憑什么我們不能執(zhí)行一個(gè) Apk 中的代碼呢?

1.2 插件化優(yōu)點(diǎn)

插件化讓 Apk 中的代碼(主要是指 Android 組件)能夠免安裝運(yùn)行,這樣能夠帶來(lái)很多收益:

  • 減少安裝Apk的體積、按需下載模塊
  • 動(dòng)態(tài)更新插件
  • 宿主和插件分開(kāi)編譯,提升開(kāi)發(fā)效率
  • 解決方法數(shù)超過(guò)65535的問(wèn)題

想象一下,你的應(yīng)用擁有 Native 應(yīng)用一般極高的性能,又能獲取諸如 Web 應(yīng)用一樣的收益。

嗯,理想很美好不是嘛?

1.3 與組件化的區(qū)別

  • 組件化:是將一個(gè)App分成多個(gè)模塊,每個(gè)模塊都是一個(gè)組件(module),開(kāi)發(fā)過(guò)程中可以讓這些組件相互依賴(lài)或獨(dú)立編譯、調(diào)試部分組件,但是這些組件最終會(huì)合并成一個(gè)完整的Apk去發(fā)布到應(yīng)用市場(chǎng)。
  • 插件化:是將整個(gè)App拆分成很多模塊,每個(gè)模塊都是一個(gè)Apk(組件化的每個(gè)模塊是一個(gè)lib),最終打包的時(shí)候?qū)⑺拗鰽pk和插件Apk分開(kāi)打包,只需發(fā)布宿主Apk到應(yīng)用市場(chǎng),插件Apk通過(guò)動(dòng)態(tài)按需下發(fā)到宿主Apk。

二、插件化的技術(shù)難點(diǎn)

想讓插件的Apk真正運(yùn)行起來(lái),首先要先能找到插件Apk的存放位置,然后我們要能解析加載Apk里面的代碼。

​ 但是光能執(zhí)行Java代碼是沒(méi)有意義的,在Android系統(tǒng)中有四大組件是需要在系統(tǒng)中注冊(cè)的,具體來(lái)說(shuō)是在 Android 系統(tǒng)的 ActivityManagerService (AMS) 和 PackageManagerService (PMS) 中注冊(cè)的,而四大組件的解析和啟動(dòng)都需要依賴(lài) AMS 和 PMS,如何欺騙系統(tǒng),讓他承認(rèn)一個(gè)未安裝的 Apk 中的組件,如何讓宿主動(dòng)態(tài)加載執(zhí)行插件Apk中 Android 組件(即 Activity、Service、BroadcastReceiver、ContentProvider、Fragment)等是插件化最大的難點(diǎn)。

​ 另外,應(yīng)用資源引用(特指 R 中引用的資源,如 layout、values 等)也是一大問(wèn)題,想象一下你在宿主進(jìn)程中使用反射加載了一個(gè)插件 Apk,代碼中的 R 對(duì)應(yīng)的 id 卻無(wú)法引用到正確的資源,會(huì)產(chǎn)生什么后果。

總結(jié)一下,其實(shí)做到插件化的要點(diǎn)就這幾個(gè):

  • 如何加載并執(zhí)行插件 Apk 中的代碼(ClassLoader Injection
  • 讓系統(tǒng)能調(diào)用插件 Apk 中的組件(Runtime Container
  • 正確識(shí)別插件 Apk 中的資源(Resource Injection

當(dāng)然還有其他一些小問(wèn)題,但可能不是所有場(chǎng)景下都會(huì)遇到,我們后面再單獨(dú)說(shuō)。

三、ClassLoader Injection

ClassLoader 是插件化中必須要掌握的,因?yàn)槲覀冎?code>Android 應(yīng)用本身是基于魔改的 Java 虛擬機(jī)的,而由于插件是未安裝的 apk,系統(tǒng)不會(huì)處理其中的類(lèi),所以需要使用 ClassLoader 加載 Apk,然后反射里面的代碼。

3.1 java 中的 ClassLoader

  • BootstrapClassLoader 負(fù)責(zé)加載 JVM 運(yùn)行時(shí)的核心類(lèi),比如 JAVA_HOME/lib/rt.jar 等等
  • ExtensionClassLoader 負(fù)責(zé)加載 JVM 的擴(kuò)展類(lèi),比如 JAVA_HOME/lib/ext 下面的 jar 包
  • AppClassLoader 負(fù)責(zé)加載 classpath 里的 jar 包和目錄

3.2 android 中的 ClassLoader

Android系統(tǒng)中ClassLoader是用來(lái)加載dex文件的,有包含 dex 的 apk 文件以及 jar 文件,dex 文件是一種對(duì)class文件優(yōu)化的產(chǎn)物,在Android中應(yīng)用打包時(shí)會(huì)把所有class文件進(jìn)行合并、優(yōu)化(把不同的class文件重復(fù)的東西只保留一份),然后生成一個(gè)最終的class.dex文件

  • PathClassLoader 用來(lái)加載系統(tǒng)類(lèi)和應(yīng)用程序類(lèi),可以加載已經(jīng)安裝的 apk 目錄下的 dex 文件
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

  • DexClassLoader 用來(lái)加載 dex 文件,可以從存儲(chǔ)空間加載 dex 文件。
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

我們?cè)诓寮幸话闶褂玫氖?DexClassLoader

3.3 雙親委派機(jī)制

每一個(gè) ClassLoader 中都有一個(gè) parent 對(duì)象,代表的是父類(lèi)加載器,在加載一個(gè)類(lèi)的時(shí)候,會(huì)先使用父類(lèi)加載器去加載,如果在父類(lèi)加載器中沒(méi)有找到,自己再進(jìn)行加載,如果 parent 為空,那么就用系統(tǒng)類(lèi)加載器來(lái)加載。通過(guò)這樣的機(jī)制可以保證系統(tǒng)類(lèi)都是由系統(tǒng)類(lèi)加載器加載的。 下面是 ClassLoader loadClass 方法的具體實(shí)現(xiàn)。

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        // 先從父類(lèi)加載器中進(jìn)行加載
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 沒(méi)有找到,再自己加載
                    c = findClass(name);
                }
            }
            return c;
    }

3.4 如何加載插件中的類(lèi)

要加載插件中的類(lèi),我們首先要?jiǎng)?chuàng)建一個(gè) DexClassLoader先看下 DexClassLoader 的構(gòu)造函數(shù)需要哪些參數(shù)。

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        // ...
    }
}

構(gòu)造函數(shù)需要四個(gè)參數(shù): dexPath 是需要加載的 dex / apk / jar 文件路徑 optimizedDirectory 是 dex 優(yōu)化后存放的位置,在 ART 上,會(huì)執(zhí)行 oat 對(duì) dex 進(jìn)行優(yōu)化,生成機(jī)器碼,這里就是存放優(yōu)化后的 odex 文件的位置 librarySearchPath 是 native 依賴(lài)的位置 parent 就是父類(lèi)加載器,默認(rèn)會(huì)先從 parent 加載對(duì)應(yīng)的類(lèi)

創(chuàng)建出 DexClassLaoder 實(shí)例以后,只要調(diào)用其 loadClass(className) 方法就可以加載插件中的類(lèi)了。具體的實(shí)現(xiàn)在下面:

    // 從 assets 中拿出插件 apk 放到內(nèi)部存儲(chǔ)空間
    private fun extractPlugin() {
        var inputStream = assets.open("plugin.apk")
        File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
    }

    private fun init() {
        extractPlugin()
        pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
        nativeLibDir = File(filesDir, "pluginlib").absolutePath
        dexOutPath = File(filesDir, "dexout").absolutePath
        // 生成 DexClassLoader 用來(lái)加載插件類(lèi)
        pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader)
    }

3.5 執(zhí)行插件類(lèi)的方法

通過(guò)反射來(lái)執(zhí)行類(lèi)的方法

val loadClass = pluginClassLoader.loadClass(activityName)
loadClass.getMethod("test",null).invoke(loadClass)

我們稱(chēng)這個(gè)過(guò)程叫做 ClassLoader 注入。完成注入后,所有來(lái)自宿主的類(lèi)使用宿主的 ClassLoader 進(jìn)行加載,所有來(lái)自插件 Apk 的類(lèi)使用插件 ClassLoader 進(jìn)行加載,而由于 ClassLoader 的雙親委派機(jī)制,實(shí)際上系統(tǒng)類(lèi)會(huì)不受 ClassLoader 的類(lèi)隔離機(jī)制所影響,這樣宿主 Apk 就可以在宿主進(jìn)程中使用來(lái)自于插件的組件類(lèi)了。

四、Runtime Container

我們之前說(shuō)到 Activity 插件化最大的難點(diǎn)是如何欺騙系統(tǒng),讓他承認(rèn)一個(gè)未安裝的 Apk 中的組件。 因?yàn)椴寮莿?dòng)態(tài)加載的,所以插件的四大組件不可能注冊(cè)到宿主的 Manifest 文件中,而沒(méi)有在 Manifest 中注冊(cè)的四大組件是不能和系統(tǒng)直接進(jìn)行交互的。 如果直接把插件的 Activity 注冊(cè)到宿主 Manifest 里就失去了插件化的動(dòng)態(tài)特性,因?yàn)槊看尾寮行略?Activity 都要修改宿主 Manifest 并且重新打包,那就和直接寫(xiě)在宿主中沒(méi)什么區(qū)別了。

4.1 為什么沒(méi)有注冊(cè)的 Activity 不能和系統(tǒng)交互

這里的不能直接交互的含義有兩個(gè)

  • 系統(tǒng)會(huì)檢測(cè) Activity 是否注冊(cè) 如果我們啟動(dòng)一個(gè)沒(méi)有在 Manifest 中注冊(cè)的 Activity,會(huì)發(fā)現(xiàn)報(bào)如下 error:
android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zyg.commontec/com.zyg.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?

這個(gè) log 在 Instrumentation checkStartActivityResult 方法中可以看到:

public class Instrumentation {
    public static void checkStartActivityResult(int res, Object intent) {
        if (!ActivityManager.isStartResultFatalError(res)) {
            return;
        }

        switch (res) {
            case ActivityManager.START_INTENT_NOT_RESOLVED:
            case ActivityManager.START_CLASS_NOT_FOUND:
                if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
                    throw new ActivityNotFoundException(
                            "Unable to find explicit activity class "
                            + ((Intent)intent).getComponent().toShortString()
                            + "; have you declared this activity in your AndroidManifest.xml?");
                throw new ActivityNotFoundException(
                        "No Activity found to handle " + intent);
                ...
        }
    }
}

  • Activity 的生命周期無(wú)法被調(diào)用,其實(shí)一個(gè) Activity 主要的工作,都是在其生命周期方法中調(diào)用了,既然上一步系統(tǒng)檢測(cè)了 Manifest 注冊(cè)文件,啟動(dòng) Activity 被拒絕,那么其生命周期方法也肯定不會(huì)被調(diào)用了。從而插件 Activity 也就不能正常運(yùn)行了。

4.2 運(yùn)行時(shí)容器技術(shù)

由于Android中的組件(Activity,Service,BroadcastReceiverContentProvider)是由系統(tǒng)創(chuàng)建的,并且由系統(tǒng)管理生命周期。 僅僅構(gòu)造出這些類(lèi)的實(shí)例是沒(méi)用的,還需要管理組件的生命周期。其中以Activity最為復(fù)雜,不同框架采用的方法也不盡相同。插件化如何支持組件生命周期的管理。 大致分為兩種方式:

  • 運(yùn)行時(shí)容器技術(shù)(ProxyActivity代理)
  • 預(yù)埋StubActivity,hook系統(tǒng)啟動(dòng)Activity的過(guò)程

我們的解決方案很簡(jiǎn)單,即運(yùn)行時(shí)容器技術(shù),簡(jiǎn)單來(lái)說(shuō)就是在宿主 Apk 中預(yù)埋一些空的 Android 組件,以 Activity 為例,我預(yù)置一個(gè) ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注冊(cè)它。

它要做的事情很簡(jiǎn)單,就是幫助我們作為插件 Activity 的容器,它從 Intent 接受幾個(gè)參數(shù),分別是插件的不同信息,如:

  • pluginName
  • pluginApkPath
  • pluginActivityName

等,其實(shí)最重要的就是 pluginApkPath pluginActivityName,當(dāng) ContainerActivity 啟動(dòng)時(shí),我們就加載插件的 ClassLoader、Resource,并反射 pluginActivityName 對(duì)應(yīng)的 Activity 類(lèi)。當(dāng)完成加載后,ContainerActivity 要做兩件事:

  • 轉(zhuǎn)發(fā)所有來(lái)自系統(tǒng)的生命周期回調(diào)至插件 Activity
  • 接受 Activity 方法的系統(tǒng)調(diào)用,并轉(zhuǎn)發(fā)回系統(tǒng)

我們可以通過(guò)復(fù)寫(xiě) ContainerActivity 的生命周期方法來(lái)完成第一步,而第二步我們需要定義一個(gè) PluginActivity然后在編寫(xiě)插件 Apk 中的 Activity 組件時(shí),不再讓其集成 android.app.Activity,而是集成自我們的 PluginActivity。

public class ContainerActivity extends Activity {
    private PluginActivity pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        String pluginActivityName = getIntent().getString("pluginActivityName", "");
        pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
        if (pluginActivity == null) {
            super.onCreate(savedInstanceState);
            return;
        }

        pluginActivity.onCreate();
    }

    @Override
    protected void onResume() {
        if (pluginActivity == null) {
            super.onResume();
            return;
        }
        pluginActivity.onResume();
    }

    @Override
    protected void onPause() {
        if (pluginActivity == null) {
            super.onPause();
            return;
        }
        pluginActivity.onPause();
    }

    // ...
}

public class PluginActivity {
    private ContainerActivity containerActivity;

    public PluginActivity(ContainerActivity containerActivity) {
        this.containerActivity = containerActivity;
    }

    @Override
    public <T extends View> T findViewById(int id) {
        return containerActivity.findViewById(id);
    }
    // ...
}

// 插件 `Apk` 中真正寫(xiě)的組件
public class TestActivity extends PluginActivity {
    // ......
}

是不是感覺(jué)有點(diǎn)看懂了,雖然真正搞的時(shí)候還有很多小坑,但大概原理就是這么簡(jiǎn)單,啟動(dòng)插件組件需要依賴(lài)容器,容器負(fù)責(zé)加載插件組件并且完成雙向轉(zhuǎn)發(fā),轉(zhuǎn)發(fā)來(lái)自系統(tǒng)的生命周期回調(diào)至插件組件,同時(shí)轉(zhuǎn)發(fā)來(lái)自插件組件的系統(tǒng)調(diào)用至系統(tǒng)。

4.3 字節(jié)碼替換

該方式雖然能夠很好的實(shí)現(xiàn)啟動(dòng)插件Activity的目的,但是由于開(kāi)發(fā)式侵入性很強(qiáng),插件中的Activity必須繼承PluginActivity,如果想把之前的模塊改造成插件需要很多額外的工作。

class TestActivity extends Activity {}
->
class TestActivity extends PluginActivity {}

有沒(méi)有什么辦法能讓插件組件的編寫(xiě)與原來(lái)沒(méi)有任何差別呢?

Shadow 的做法是字節(jié)碼替換插件,這是一個(gè)非常棒的想法,簡(jiǎn)單來(lái)說(shuō),Android 提供了一些 Gradle 插件開(kāi)發(fā)套件,其中有一項(xiàng)功能叫 Transform Api,它可以介入項(xiàng)目的構(gòu)建過(guò)程,在字節(jié)碼生成后、dex 文件生成前,對(duì)代碼進(jìn)行某些變換,具體怎么做的不說(shuō)了,可以自己看文檔。

實(shí)現(xiàn)的功能嘛,就是用戶(hù)配置 Gradle 插件后,正常開(kāi)發(fā),依然編寫(xiě):

class TestActivity extends Activity {}

然后完成編譯后,最后的字節(jié)碼中,顯示的卻是:

class TestActivity extends PluginActivity {}

到這里基本的框架就差不多結(jié)束了。

五、Resource Injection

最后要說(shuō)的是資源注入,其實(shí)這一點(diǎn)相當(dāng)重要,Android 應(yīng)用的開(kāi)發(fā)其實(shí)崇尚的是邏輯與資源分離的理念,所有資源(layout、values 等)都會(huì)被打包到 Apk 中,然后生成一個(gè)對(duì)應(yīng)的 R 類(lèi),其中包含對(duì)所有資源的引用 id。

資源的注入并不容易,好在 Android 系統(tǒng)給我們留了一條后路,最重要的是這兩個(gè)接口:

PackageManager#getPackageArchiveInfo:根據(jù) Apk 路徑解析一個(gè)未安裝的 Apk 的 PackageInfo
PackageManager#getResourcesForApplication:根據(jù) ApplicationInfo 創(chuàng)建一個(gè) Resources 實(shí)例

我們要做的就是在上面 ContainerActivity#onCreate 中加載插件 Apk 的時(shí)候,用這兩個(gè)方法創(chuàng)建出來(lái)一份插件資源實(shí)例。具體來(lái)說(shuō)就是先用 PackageManager#getPackageArchiveInfo 拿到插件 Apk 的 PackageInfo,有了 PacakgeInfo 之后我們就可以自己組裝一份 ApplicationInfo,然后通過(guò) PackageManager#getResourcesForApplication 來(lái)創(chuàng)建資源實(shí)例,大概代碼像這樣:

PackageManager packageManager = getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
    pluginApkPath,
    PackageManager.GET_ACTIVITIES
    | PackageManager.GET_META_DATA
    | PackageManager.GET_SERVICES
    | PackageManager.GET_PROVIDERS
    | PackageManager.GET_SIGNATURES
);
packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;

Resources injectResources = null;
try {
    injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
} catch (PackageManager.NameNotFoundException e) {
    // ...
}

拿到資源實(shí)例后,我們需要將宿主的資源和插件資源 Merge 一下,編寫(xiě)一個(gè)新的 Resources 類(lèi),用這樣的方式完成自動(dòng)代理:

public class PluginResources extends Resources {
    private Resources hostResources;
    private Resources injectResources;

    public PluginResources(Resources hostResources, Resources injectResources) {
        super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
        this.hostResources = hostResources;
        this.injectResources = injectResources;
    }

    @Override
    public String getString(int id, Object... formatArgs) throws NotFoundException {
        try {
            return injectResources.getString(id, formatArgs);
        } catch (NotFoundException e) {
            return hostResources.getString(id, formatArgs);
        }
    }

    // ...
}

然后我們?cè)?ContainerActivity 完成插件組件加載后,創(chuàng)建一份 Merge 資源,再?gòu)?fù)寫(xiě) ContainerActivity#getResources,將獲取到的資源替換掉:

public class ContainerActivity extends Activity {
    private Resources pluginResources;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...
        pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
        // ...
    }

    @Override
    public Resources getResources() {
        if (pluginActivity == null) {
            return super.getResources();
        }
        return pluginResources;
    }
}

這樣就完成了資源的注入。

到此這篇關(guān)于淺談Android插件化的文章就介紹到這了,更多相關(guān)Android插件化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Android使用SharedPreferences存儲(chǔ)XML文件的實(shí)現(xiàn)方法

    Android使用SharedPreferences存儲(chǔ)XML文件的實(shí)現(xiàn)方法

    這篇文章主要介紹了Android使用SharedPreferences存儲(chǔ)XML文件的實(shí)現(xiàn)方法,實(shí)例分析了SharedPreferences類(lèi)的基本初始化與文件存儲(chǔ)相關(guān)技巧,需要的朋友可以參考下
    2016-07-07
  • Android 自定義TextView去除paddingTop和paddingBottom

    Android 自定義TextView去除paddingTop和paddingBottom

    這篇文章主要介紹了Android 自定義TextView去除paddingTop和paddingBottom的相關(guān)資料,這里提供實(shí)例來(lái)幫助大家實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下
    2017-09-09
  • Android相機(jī)管理工具類(lèi)

    Android相機(jī)管理工具類(lèi)

    這篇文章主要為大家詳細(xì)介紹了Android相機(jī)管理工具類(lèi),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-02-02
  • Android中DialogFragment自定義背景與寬高的方法

    Android中DialogFragment自定義背景與寬高的方法

    DialogFragment 彈出框默認(rèn)是在屏幕的中央,左右還有留白,那么如何自定義背景和寬高呢?下面這篇文章就來(lái)給大家介紹了關(guān)于Android中DialogFragment自定義背景與寬高的方法,需要的朋友可以參考下。
    2017-08-08
  • Android中Item實(shí)現(xiàn)點(diǎn)擊水波紋效果

    Android中Item實(shí)現(xiàn)點(diǎn)擊水波紋效果

    這篇文章主要給大家介紹了關(guān)于Android中Item實(shí)現(xiàn)點(diǎn)擊水波紋效果的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)各位Android開(kāi)發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2018-11-11
  • App內(nèi)切換語(yǔ)言詳解

    App內(nèi)切換語(yǔ)言詳解

    本篇文章主要介紹了App內(nèi)切換語(yǔ)言的方法。具有很好的參考價(jià)值。下面跟著小編一起來(lái)看下吧
    2017-04-04
  • Android 中build.prop 文件與 getprop 命令

    Android 中build.prop 文件與 getprop 命令

    這篇文章主要介紹了Android 中build.prop 文件與 getprop 命令的相關(guān)資料,需要的朋友可以參考下
    2017-06-06
  • android利用handler實(shí)現(xiàn)倒計(jì)時(shí)功能

    android利用handler實(shí)現(xiàn)倒計(jì)時(shí)功能

    這篇文章主要為大家詳細(xì)介紹了android利用handler實(shí)現(xiàn)倒計(jì)時(shí)功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-11-11
  • Android自定義ListView單擊事件失效的解決方法

    Android自定義ListView單擊事件失效的解決方法

    這篇文章主要為大家詳細(xì)介紹了Android自定義ListView單擊事件失效的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-07-07
  • Android性能優(yōu)化之RecyclerView分頁(yè)加載組件功能詳解

    Android性能優(yōu)化之RecyclerView分頁(yè)加載組件功能詳解

    這篇文章主要為大家介紹了Android性能優(yōu)化之RecyclerView分頁(yè)加載組件功能詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-09-09

最新評(píng)論