转至:http://blog.csdn.net/jiangwei0910410003/article/details/47679843

一、前言

今天又到周末了,感觉时间过的很快呀.又要写blog了。那么今天就来看看应用的换肤原理解析。在之前的一篇博客中我说道了Android中的插件开发篇的基础:类加载器的相关知识。没看过的同学可以转战:

http://blog.csdn.net/jiangwei0910410003/article/details/41384667

二、原理介绍

现在市场上有很多应用都有换肤的功能,就是能够提供给用户一些皮肤包,然后下载,替换。而且有些皮肤是要收费的。对于这个功能的话,其实没有什么技术难度的,但是他包含了一个现阶段很火的一个技术:动态加载

好了,既然说到了动态加载,那么如果有不熟悉的同学,可以转战看另外的一篇blog了:

http://blog.csdn.net/jiangwei0910410003/article/details/17679823

我们先来看一个市场上的一个app具有的换肤功能的例子:QQ空间

点击我的空间=>个性化=>原创主题=>选择下载主题

        

下载主题,然后可以替换了。接下来我们看看这个主题包放到哪了?因为既然下载肯定是存放起来了。

两个地方可以放:一个是SD卡,一个是应用的数据目录

我们先来看看应用的目录(配置好了adb命令):

第一步:得到QQ空间的的包名:

打开QQ空间app,不要退出。然后执行命令:adb shell dumpsys activity top

这个命令还是很有用的吧,能够快速的得到一个应用的包名

我们看到QQ空间的包名:com.qzone

第二步:进入QQ应用的目录,查看对应的资源

我们在他的shared_prefs中找到了theme.xml文件,查看该文件,就可以找到了对应皮肤的位置:

/data/user/0/com.qzone/files/cache/qz_external_resource/theme_res/38

我们进入到这个目录:

看到上面红色圈起来的地方的目录结构和文件名是不是很眼熟.对,这个就是我们把一个正常的apk解压之后得到的东西。那么我们可以断定,QQ空间的皮肤包其实就是一个apk,然后动态加载apk,取到对应的资源然后替换。

三、如何设计一个换肤插件

好了,既然上面我们解读了QQ空间的换肤功能,也知道了它的大体的原理了,下面我们来自己动手制作我们自己的主题包。

关于动态加载的相关技术这里就不详细介绍了,看我的前面提到的两个相关文章的介绍。

我们这里需要建立三个工程:

宿主程序(主程序):ResourceLoader

主题包1的工程:ResourceLoaderApk1

主题包2的工程:ResourceLoaderApk2

在宿主程序中我们需要编写动态加载的代码:

下面来看一下具体代码:

MainActivity.Java

[java] view plaincopy
  1. package com.example.resourceloader;
  2. import java.io.File;
  3. import java.lang.reflect.Field;
  4. import java.lang.reflect.Method;
  5. import android.annotation.SuppressLint;
  6. import android.content.Context;
  7. import android.graphics.drawable.Drawable;
  8. import android.os.Bundle;
  9. import android.util.Log;
  10. import android.view.View;
  11. import android.view.View.OnClickListener;
  12. import android.widget.ImageView;
  13. import android.widget.LinearLayout;
  14. import android.widget.TextView;
  15. public class MainActivity extends BaseActivity {
  16. /**
  17. * 需要替换主题的控件
  18. * 这里就列举三个:TextView,ImageView,LinearLayout
  19. */
  20. private TextView textV;
  21. private ImageView imgV;
  22. private LinearLayout layout;
  23. @Override
  24. protected void onCreate(Bundle savedInstanceState) {
  25. super.onCreate(savedInstanceState);
  26. setContentView(R.layout.activity_main);
  27. textV = (TextView)findViewById(R.id.text);
  28. imgV = (ImageView)findViewById(R.id.imageview);
  29. layout = (LinearLayout)findViewById(R.id.layout);
  30. findViewById(R.id.btn1).setOnClickListener(new OnClickListener(){
  31. @Override
  32. public void onClick(View arg0) {
  33. String filesDir = getCacheDir().getAbsolutePath();
  34. String filePath = filesDir + File.separator +"apk1.apk";
  35. Log.i("Loader", "filePath:"+filePath);
  36. Log.i("Loader", "isExist:"+new File(filePath).exists());
  37. //loadResources(filePath);
  38. //setContent();
  39. //printResourceId();
  40. setContent1();
  41. //printRField();
  42. }});
  43. findViewById(R.id.btn2).setOnClickListener(new OnClickListener(){
  44. @Override
  45. public void onClick(View v) {
  46. String filesDir = getCacheDir().getAbsolutePath();
  47. String filePath = filesDir + File.separator +"apk2.apk";
  48. //loadResources(filePath);
  49. setContent();
  50. }});
  51. }
  52. /**
  53. * 动态加载主题包中的资源,然后替换每个控件
  54. */
  55. @SuppressLint("NewApi")
  56. private void setContent(){
  57. try{
  58. Class clazz = classLoader.loadClass("com.example.resourceloaderapk.UIUtil");
  59. Method method = clazz.getMethod("getTextString", Context.class);
  60. String str = (String)method.invoke(null, this);
  61. textV.setText(str);
  62. method = clazz.getMethod("getImageDrawable", Context.class);
  63. Drawable drawable = (Drawable)method.invoke(null, this);
  64. imgV.setBackground(drawable);
  65. method = clazz.getMethod("getLayout", Context.class);
  66. View view = (View)method.invoke(null, this);
  67. layout.addView(view);
  68. }catch(Exception e){
  69. Log.i("Loader", "error:"+Log.getStackTraceString(e));
  70. }
  71. }
  72. /**
  73. * 另外的一种方式获取
  74. */
  75. private void setContent1(){
  76. int stringId = getTextStringId();
  77. int drawableId = getImgDrawableId();
  78. int layoutId = getLayoutId();
  79. Log.i("Loader", "stringId:"+stringId+",drawableId:"+drawableId+",layoutId:"+layoutId);
  80. }
  81. @SuppressLint("NewApi")
  82. private int getTextStringId(){
  83. try{
  84. Class clazz = classLoader.loadClass("com.example.resourceloaderapk1.R$string");
  85. Field field = clazz.getField("app_name");
  86. int resId = (int)field.get(null);
  87. return resId;
  88. }catch(Exception e){
  89. Log.i("Loader", "error:"+Log.getStackTraceString(e));
  90. }
  91. return 0;
  92. }
  93. @SuppressLint("NewApi")
  94. private int getImgDrawableId(){
  95. try{
  96. Class clazz = classLoader.loadClass("com.example.resourceloaderapk1.R$drawable");
  97. Field field = clazz.getField("ic_launcher");
  98. int resId = (int)field.get(null);
  99. return resId;
  100. }catch(Exception e){
  101. Log.i("Loader", "error:"+Log.getStackTraceString(e));
  102. }
  103. return 0;
  104. }
  105. @SuppressLint("NewApi")
  106. private int getLayoutId(){
  107. try{
  108. Class clazz = classLoader.loadClass("com.example.resourceloaderapk1.R$layout");
  109. Field field = clazz.getField("activity_main");
  110. int resId = (int)field.get(null);
  111. return resId;
  112. }catch(Exception e){
  113. Log.i("Loader", "error:"+Log.getStackTraceString(e));
  114. }
  115. return 0;
  116. }
  117. @SuppressLint("NewApi")
  118. private void printResourceId(){
  119. try{
  120. Class clazz = classLoader.loadClass("com.example.resourceloaderapk.UIUtil");
  121. Method method = clazz.getMethod("getTextStringId", null);
  122. Object obj = method.invoke(null, null);
  123. Log.i("Loader", "stringId:"+obj);
  124. Log.i("Loader", "newId:"+R.string.app_name);
  125. method = clazz.getMethod("getImageDrawableId", null);
  126. obj = method.invoke(null, null);
  127. Log.i("Loader", "drawableId:"+obj);
  128. Log.i("Loader", "newId:"+R.drawable.ic_launcher);
  129. method = clazz.getMethod("getLayoutId", null);
  130. obj = method.invoke(null, null);
  131. Log.i("Loader", "layoutId:"+obj);
  132. Log.i("Loader", "newId:"+R.layout.activity_main);
  133. }catch(Exception e){
  134. Log.i("Loader", "error:"+Log.getStackTraceString(e));
  135. }
  136. }
  137. private void printRField(){
  138. Class clazz = R.id.class;
  139. Field[] fields = clazz.getFields();
  140. for(Field field : fields){
  141. Log.i("Loader", "fields:"+field);
  142. }
  143. Class clazzs = R.layout.class;
  144. Field[] fieldss = clazzs.getFields();
  145. for(Field field : fieldss){
  146. Log.i("Loader", "fieldss:"+field);
  147. }
  148. }
  149. }

这里的代码没有大的难度,就是我们使用DexClassLoader类加载每个主题的apk包,然后用反射的方法调用apk包中的方法来获取资源。

下面来看一下主题包工程代码:

UIUtil.java

[java] view plaincopy
  1. package com.example.resourceloaderapk;
  2. import android.content.Context;
  3. import android.graphics.drawable.Drawable;
  4. import android.view.LayoutInflater;
  5. import android.view.View;
  6. import com.example.resourceloaderapk1.R;
  7. public class UIUtil {
  8. public static String getTextString(Context ctx){
  9. return ctx.getResources().getString(R.string.app_name);
  10. }
  11. public static Drawable getImageDrawable(Context ctx){
  12. return ctx.getResources().getDrawable(R.drawable.ic_launcher);
  13. }
  14. public static View getLayout(Context ctx){
  15. return LayoutInflater.from(ctx).inflate(R.layout.activity_main, null);
  16. }
  17. public static int getTextStringId(){
  18. return R.string.app_name;
  19. }
  20. public static int getImageDrawableId(){
  21. return R.drawable.ic_launcher;
  22. }
  23. public static int getLayoutId(){
  24. return R.layout.activity_main;
  25. }
  26. }

这个类就是提供给外部的获取资源的方法,我们在宿主程序中也就是反射这个方法来获取资源的,这个方法中我们提供了两种方式获取资源:一种是直接返回资源的内容,还有一种是返回一个资源的Id。

关于主题包2的工程这里就不介绍了,代码是一样的,只是资源不一样。

我们运行两个主题包,得到两个apk

ResourceLoaderApk1.apk

ResourceLoaderApk2.apk

这时候我们使用adb push命令,将这两个apk放到宿主程序的cache目录下。


温馨提示:

这里不能将需要加载的apk放到非宿主程序的沙盒目录外,不然会加载失败,抛出异常。关于程序的沙盒目录概念其实很好理解:就是/data/data/xxx.xxx/目录,就是这个目录是这个程序所独有的,其他没有共享权限的app是不能访问的(当然除了获取root权限外),这个其实也很好理解为何要这么做,Google也是为了安全,自己需要加载的apk/dex/jar就应当被保护起来。

当然这里不一定要放到cache目录下,只要是沙盒目录下都可以,新建一个目录也是可以的。不过一般都是使用cache目录。

项目地址:http://download.csdn.net/detail/jiangwei0910410003/9008423

这时候我们运行宿主程序:

两个btn,可以加载不同的主题内容,但是问题来啦。。点击之后发现没有效果,捕获异常,我们打印log看看:

adb logcat -s Loader

他说找不到资源异常,我们来分析一下。

我们在主题1工程中,调用的是主题1apk中的资源R.string.xxx

但是我们知道获取资源的时候是用Resource类来得到的,对于一个程序来说一个Context只会持有一个Resource对象,但是我们加载apk的时候,主题apk没有得到对应的Context。因为动态加载不想正常的运行一个程序,每个程序都有一个全局的Context变量,但是加载出来的话是没有的。那有人就说了:在代码里面我们用反射的方式去获取的时候不是将宿主的Context变量传递过去了吗?

对,看上去是没有任何问题,但是这里其实还是那个问题:就是宿主的Context如何能加载插件apk中的资源,我们知道一个app的工程的资源文件都会隐射到R文件中,而这个R文件的包名则是这个应用的包名,一个包名一般对应一个Context。那么我们现在即使将宿主的Context传递过去,也是对应宿主的包名,也就是找到宿主工程的R文件,所以还是找不到对应的资源。其实我们要解决的问题就是将插件apk中资源添加到宿主apk中。这时候就需要用一种方式了,采用反射的机制:

通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources中,由于addAssetPath是隐藏api我们无法直接调用,所以只能通过反射,下面是它的声明,通过注释我们可以看出,传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了,然后再通过AssetManager来创建一个新的Resources对象,这个对象就是我们可以使用的apk中的资源了。


我们看一下代码:

[java] view plaincopy
  1. protected void loadResources(String dexPath) {
  2. try {
  3. AssetManager assetManager = AssetManager.class.newInstance();
  4. Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
  5. addAssetPath.invoke(assetManager, dexPath);
  6. mAssetManager = assetManager;
  7. } catch (Exception e) {
  8. e.printStackTrace();
  9. }
  10. Resources superRes = super.getResources();
  11. superRes.getDisplayMetrics();
  12. superRes.getConfiguration();
  13. mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
  14. mTheme = mResources.newTheme();
  15. mTheme.setTo(super.getTheme());
  16. }

参数就是需要加载资源的包的路径。

当然我们还需要重写Context的三个方法:

[java] view plaincopy
  1. @Override
  2. public AssetManager getAssets() {
  3. return mAssetManager == null ? super.getAssets() : mAssetManager;
  4. }
  5. @Override
  6. public Resources getResources() {
  7. return mResources == null ? super.getResources() : mResources;
  8. }
  9. @Override
  10. public Theme getTheme() {
  11. return mTheme == null ? super.getTheme() : mTheme;
  12. }

重写的这三个方法就是让系统获取我们加载apk包之后的变量即可

这里,我们把代码在修改一下:在宿主工程中添加一个BaseActivity类:

BaseActivity.java

[java] view plaincopy
  1. package com.example.resourceloader;
  2. import java.io.File;
  3. import java.lang.reflect.Method;
  4. import android.app.Activity;
  5. import android.content.res.AssetManager;
  6. import android.content.res.Resources;
  7. import android.content.res.Resources.Theme;
  8. import android.os.Bundle;
  9. import dalvik.system.DexClassLoader;
  10. public class BaseActivity extends Activity{
  11. protected AssetManager mAssetManager;//资源管理器
  12. protected Resources mResources;//资源
  13. protected Theme mTheme;//主题
  14. @Override
  15. protected void onCreate(Bundle savedInstanceState) {
  16. super.onCreate(savedInstanceState);
  17. }
  18. protected void loadResources(String dexPath) {
  19. try {
  20. AssetManager assetManager = AssetManager.class.newInstance();
  21. Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
  22. addAssetPath.invoke(assetManager, dexPath);
  23. mAssetManager = assetManager;
  24. } catch (Exception e) {
  25. e.printStackTrace();
  26. }
  27. Resources superRes = super.getResources();
  28. superRes.getDisplayMetrics();
  29. superRes.getConfiguration();
  30. mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
  31. mTheme = mResources.newTheme();
  32. mTheme.setTo(super.getTheme());
  33. }
  34. @Override
  35. public AssetManager getAssets() {
  36. return mAssetManager == null ? super.getAssets() : mAssetManager;
  37. }
  38. @Override
  39. public Resources getResources() {
  40. return mResources == null ? super.getResources() : mResources;
  41. }
  42. @Override
  43. public Theme getTheme() {
  44. return mTheme == null ? super.getTheme() : mTheme;
  45. }
  46. }

在MainActivity中调用loadResource方法:

这时候我们在运行宿主程序:

点击主题1:我们发现文字变成了:ResourceLoaderApk;因为这里的图片都是用的机器人所以看上去没变化,看到底下的LinearLayout加载了主题包中的布局xml内容。

点击主题2:效果同上,只是内容是主题包2apk中的。

好了。到这里我们就完美的开发了我们自己的换肤功能。但是有的同学可能认为,这哪是换肤的功能,没看到效果呢?我这个例子不是完完全全的开发一个换肤的工程。只是介绍原理呢。不过真的换肤也没有难度的,我们需要解决一些问题:

1、对于需要替换主题的控件需要统一定义一下。

2、对于每个主题包的工程中的对外接口要统一(或者是要符合一定规范),比如这个例子中主题包中必须有一个:

com.example.resourceloaderapk.UIUtil类,而且这个类中必须有三个方法:getTextString,getImageDrawable,getLayout

所以这就是一个规范,当然我这里的规范设计的不是很好,正确的做法是在提供一个接口,然后每个主题包工程必须实现这个接口,然后主题包工程和宿主工程都包含这个接口,这样就能够很灵活了。

3、一般主题包apk是从网上下载下来的,所以我们需要事前设置要几个默认的主题包在本地,如果从网上下载下来的主题包出现问题了,我们去加载默认的主题。这样就不会出现任何异常情况。

四、问题总结

其实这篇文章我们看到了上面我们其实就是解决一个问题,就是如何加载主题包apk中的资源。其实这个问题有人还有一种想法,就是我们将需要的资源全部打包(可以是任何压缩包的格式),从网上下载下来之后,解压文件,通过流的方式读取每个资源文件到工程中,其实这种方式是可行的,但是效率上有很大的问题(反正我是没有尝试过)。所以这里的这种方式很方便而且高效。

五、实际用途

本文说到的这个技术现在市面上主要的作用就是:

1、在线替换主题(皮肤),语言包等

2、减小主apk的包大小,将不是很重要的资源打包成apk放到服务端。

六、总结

这篇文章主要介绍了应用换肤的原理,核心技术就是:如何加载插件Apk中的资源。后续还会技术讲解Android中插件的用途:免安装运行程序,制作中。。。

插播一条消息

$*********************************************************************************************$

博主推荐:

风萧兮兮易水寒,“天真”一去兮不复还。如何找到天真的那份快乐。小编倾力推荐app: 天真无谐

下载方式:豌豆荚,应用宝,360手机助手,百度手机助手,安卓,91市场搜索:天真无谐

关注我们:查看详情

$*********************************************************************************************$

Android 插件换肤原理解析相关推荐

  1. android之换肤原理解读

    转载自:http://blog.csdn.net/zhongwn/article/details/52891902 如下是解读demo的链接,自行下载 https://github.com/fengj ...

  2. Android一键换肤原理简述

    简介 Android对应用进行换肤操作,首先要生成一个对应的皮肤包,在要换肤的应用中收集需要换肤的控件,获取皮肤包里的资源,一键换肤时遍历View树,对要换肤的控件进行换肤.下面总结为4个步骤 步骤 ...

  3. android 换肤(1)——插件式无缝换肤(解析鸿洋大神的换肤流程)

    对于app换肤,这是一个常见而又常用的功能.虽然我做的项目中还没涉及到换肤,但是还是想研究下. 于是,下载了鸿洋大神的换肤demo来研究. 先看效果图:(尊重鸿洋大神的代码,效果图上原创) 鸿洋大神的 ...

  4. Android 换肤原理分析

    当了解了一些知识,应该用文字记录它,再抽个时间再看它,永远记住它 Android 换肤的理论知识和文章已经很多了,这里记录一下自己对这块的理解.本文效果如下: 工程:一键换肤的快乐 一.换肤的由来 首 ...

  5. Android-skin-support 换肤原理全面解析 1

    文章目录 一.背景 二.demo 三.AppCompatActivity实现 四.Android创建View全过程解析 五.换肤原理详细解析 1.上文预备知识与换肤的关系 2.源码一,创建控件全过程 ...

  6. Android-skin-support 换肤原理全面解析

    一.背景 公司业务上需要用到换肤.为了不重复造轮子,并且快速实现需求,并且求稳,,于是到Github上找了一个star数比较多的换肤框架-Android-skin-support(一款用心去做的And ...

  7. Android换肤原理

    qq 网易云音乐的换肤功能很炫酷,这里总结下换肤原理. 换肤分为两种模式,静态换肤 动态换肤.静态换肤就是把不同皮肤的资源打包到apk中,使用时切换, 这种换肤的弊端就不再多说了(种类固定,apk大) ...

  8. android换肤的实现方案,Android应用开发之Android一键换肤功能实现

    本文将带你了解Android应用开发之Android一键换肤功能实现,希望本文对大家学Android有所帮助. < 市面上对数的App都提供换肤功能,这里暂且不讲白天和夜间模式 下图是网易云音乐 ...

  9. Android主题换肤 无缝切换

    作者 _SOLID 关注 2016.04.17 22:04* 字数 4291 阅读 23224评论 123喜欢 679 今天再给大家带来一篇干货. Android的主题换肤 ,可插件化提供皮肤包,无需 ...

最新文章

  1. Markdown here 离线下载安装
  2. 阿里云服务器常见用语
  3. 太智能了!国内首批自动驾驶出租车即将在长沙上路!
  4. 信息学奥赛一本通(C++)在线评测系统——基础(一)C++语言——1079:计算分数加减表达式的值
  5. observable_在Completablefuture和Observable之间转换
  6. 数据预处理 泰坦尼克号_了解泰坦尼克号数据集的数据预处理
  7. python除了爬虫还做什么_python除了爬虫还可以做什么
  8. [leetcode] 1335. 工作计划的最低难度
  9. php旧物交易开源代码_php二手市场交易系统毕业设计(含源文件)
  10. TapTap实习两周总结
  11. 外置MOS LED驱动IC7195
  12. webpack4.0核心概念(七)———— “devtool:source-map“
  13. 【10大基础算法】线性查找算法-NO5
  14. 多目标优化算法:多目标非洲秃鹫优化算法(Multi-objective Africans Vultures Optimization Algorithm,MOAVOA)提供MATLAB代码及参考文献
  15. IE8 RC版 兼容模式的表格边框问题
  16. black duck 下载_如何创建安全的Java软件:与Black Duck的Tim Mackey交谈
  17. 河南省多校联盟二-A
  18. 像电影里黑客高手一样敲代码攻击入侵网站(模拟)
  19. 将linux镜像源改为阿里云镜像源
  20. JavaScript预习

热门文章

  1. python提取发票信息发票识别_(附完整python源码)基于tensorflow、opencv的入门案例_发票识别二:字符分割...
  2. oracle查询多表连接语句怎么写,Oracle join多表查询
  3. 大卫奥格威-挑选客户的10条准则
  4. HTML之typed.js
  5. pygame飞机大战用精灵组层编写英雄系列(八)英雄的终极技能
  6. 一呼百应招商手册(代理政策)
  7. 商业银行业务分类[整理]
  8. java兔子问题 递归_Java递归算法经典实例(经典兔子问题)
  9. 2020年9月 September CAIA一级二级 notes电子版下载链接
  10. SpringCloud Alibaba 学习