Android插件化的思考——仿QQ一键换肤,思考比实现更重要!


今天群友希望写一个关于插件的Blog,思来想去,插件也不是很懂,只是用大致的思路看看能不能模拟一个,思路还是比较重要的,如果你有兴趣的话,也可以加群:555974449,你也可以说出你想看的Blog哦,嘿嘿!好的,不多说,我们进入正题:

关于QQ的换肤,他们的实现思路我不是很清楚,但是你可以看一下这张换肤的截图

我们想使用哪个主题就直接下载就好了,这一实现的过程我们大致的可以猜想:

首选是下载到本地指定文件夹,然后通过插件加载到我们的apk,最后应用为皮肤,逻辑大致是这样的逻辑了,那我们是不是应该动动手啊动动脑?

首选我们新建一个工程好了——PlugInSample

一.实现思路

其实说起来,这个插件的实现思路,确实是比较的麻烦,思来想去,还是一种办法比较靠谱,首先,我们刻意去获取手机上所有的安装的/未安装的程序,过滤掉没用的,留下我们的插件apk,我们的插件apk怎么去辨别呢?我们可用通过设置sharedUserId,然后用实体类把插件名称和包名保存下来,有了包名,就比较好说了,我们可用获取插件的上下文,也就是createPackageContext,然后就可以做点坏事了,我们可以去剖析我们的R文件

因为R文件里面都是静态的原因,我们很容易联想到反射机制,是的,我们可以再一次过滤掉无用的信息,通过我们的PathClassLoader去加载,访问我们的内加载器反射到我们的图片ID,也就是后面的那段数字,然后,嘿嘿,就可以使用了,是不是思路比较清晰了?这里要注意的就是图片命名统一,这样就比较号过来,那具体我们应该怎么做?

二.PlugIn主程序

我们写一个Spinner,每次切换就直接换肤怎么样?OK,每次换的时候就从插件APK里加载我们的图片资源,看起来是比较顺畅的逻辑,那我们具体该怎么做呢?

<?xml version="1.0" encoding="utf-8"?>
<LinearLayoutandroid:id="@+id/mLinearLayout"xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:orientation="vertical"><Spinnerandroid:id="@+id/mSpinner"android:layout_width="wrap_content"android:layout_height="wrap_content"/></LinearLayout>

1.初始化

 /*** 初始化View*/private void initView() {//初始化控件mSpinner = (Spinner) findViewById(R.id.mSpinner);}

当然,我这刚应用就一个View,但是实际开发当中可不止,所以步骤一定要明了

2.获取所有的插件

/*** 获取手机里的插件** @return*/private List<PlugInBean> findPlugIn() {mList = new ArrayList<>();//获取相关信息PackageManager mPackageManager = getPackageManager();//获取卸载/未安装的安装包信息List<PackageInfo> mUninstallPackage = mPackageManager.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);//遍历拿到我们的信息for (PackageInfo info : mUninstallPackage) {String pkgNmae = info.packageName;//获取shareId,根据id判断是否是我们的IDString shareUserId = info.sharedUserId;if (!TextUtils.isEmpty(shareUserId)) {//如果id相同if (shareUserId.equals("com.liuguilin.share")) {//且排除自己的包名if (!pkgNmae.equals(getPackageName())) {//这个就是我们的插件了String lable = mPackageManager.getApplicationLabel(info.applicationInfo).toString();PlugInBean bean = new PlugInBean();bean.setLabelNmae(lable);bean.setPackagNmae(pkgNmae);mList.add(bean);}}}}return mList;}

这里就是过滤了一下,通过sharedUserId去拿到我们的插件APK了,然后就可以拿到我们的包名和应用名,他返回给我们一个数据集

//所有的插件List<PlugInBean> allPlugIn = findPlugIn();

3.加载皮肤数据

 /*** 加载皮肤** @param allPlugIn*/private void LoadSkin(List<PlugInBean> allPlugIn) {//遍历for (PlugInBean bean : allPlugIn) {HashMap<String, Object> mMap = new HashMap<>();mMap.put("lable", bean.getLabelNmae());mMap.put("package", bean.getPackagNmae());mData.add(mMap);}//建立Adapter并且绑定数据源mAdapter = new SimpleAdapter(this, mData, android.R.layout.simple_list_item_1, new String[]{"lable"}, new int[]{android.R.id.text1});//设置数据mSpinner.setAdapter(mAdapter);//设置监听事件mSpinner.setOnItemSelectedListener(this);}

我们通过刚才的数据集便可以把我们拿到的数据给直接显示出来了,这里其实可以判断一下size是否为0,如果为0的话也就没有插件,OK,我们设置adapter和监听,做到这里,其实你可以运行一下,虽然我们现在什么都没有,我们要做的还有很多

4.获取插件Context

    /*** 选中监听事件** @param adapterView* @param view* @param i* @param l*/@Overridepublic void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {PlugInBean bean = mList.get(i);//插件的包名String packageNmae = bean.getPackagNmae();Context mContext = null;try {//无视警告 访问代码mContext = createPackageContext(packageNmae, CONTEXT_IGNORE_SECURITY | CONTEXT_INCLUDE_CODE);} catch (PackageManager.NameNotFoundException e) {e.printStackTrace();}//获取图片getImg(packageNmae, mContext);//通过ID加载插件的图片getWindow().setBackgroundDrawable(mContext.getResources().getDrawable(mListId.get(i)));}@Overridepublic void onNothingSelected(AdapterView<?> adapterView) {}

这里的代码就比较有意思,一定要仔细看,我们首先拿到选中的item的包名,通过我们的createPackageContext拿到我们的上下文,通过这两个我们可用拿到我们的资源ID,也就是R清单里面的ID,然后直接设置window的背景,这里为了好看才设置window的背景,实际上你要设置的是你根布局的背景,那好,我们来看一下如何通过插件的上下文和包名拿到R清单的资源ID

5.获取插件图片 / 返回图片R文件ID / 反射R文件

 /*** 获取插件图片 / 返回图片R文件ID / 反射R文件** @param packageNmae* @param mContext*/private void getImg(String packageNmae, Context mContext) {//类加载器反射插件PathClassLoader pathClass = new PathClassLoader(mContext.getPackageResourcePath(), ClassLoader.getSystemClassLoader());//反射 $ 访问类加载器try {Class<?> forNmae = Class.forName(packageNmae + ".R$drawable", true, pathClass);//拿到所有图片的idField[] files = forNmae.getDeclaredFields();for (Field id : files) {//过滤 / 这里的命名可以注意一下if (id.getName().startsWith("img")) {int drawId = 0;这就是我们图片R下的IDdrawId = id.getInt(R.drawable.class);mListId.add(drawId);}}} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}}

这里我们做了很多事情,首选是拿到我们的类加载器去反射我们的插件,然后通过Class去拿我们的资源,这里注意packageNmae是我们的文件目录,他下面的R文件,$代表类部类的意思,他下面的drawable子节点,然后再一次过滤,过滤之后我们可用遍历一遍拿到我们的ID用List保存起来,也就有了我们选中的时候的设置,好的,到这里主程序算是编写完成了,不过要注意的是,记住要添加sharedUserId啊,至关重要!!!

android:sharedUserId="com.liuguilin.share"

我们现在运行也是空的,无意义,我们直接来写我们的插件吧!

三.PlugInApk插件

插件的编写很简单,我们新建一个PlugInApk的工程

工程里要做的事情就三件

  • 1.添加sharedUserId
android:sharedUserId="com.liuguilin.share"
  • 2.更改name

这就取决于你了,比如我这里是Angelababy的主题,我就把名字改成Angelababy

  • 3.把图片放在drawable文件夹下

好的,做完这三部,我们本能的把插件运行一下,运行之后,我们再次启动主程序,你会看到…

其实我们主程序里啥也没有,对吧,但是的却加载进来了,这就说明我们的插件化算是圆满实现了,那我们多来点主题看看最终的效果是什么样子的?

通过这个思路确实可以加载到图片,但是这个逻辑依旧有些不完美,不过最重要的,思考比实现更重要,对吧,后续的也就是一步步的优化了,希望大家和我一起探讨一下!

当上完整的代码

MainActivity

package com.liuguilin.pluginsample;import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.AdapterView;
import android.widget.SimpleAdapter;
import android.widget.Spinner;import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;import dalvik.system.PathClassLoader;public class MainActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener {//下拉private Spinner mSpinner;//数据源private SimpleAdapter mAdapter;//插件数据private List<PlugInBean> mList;//加载的皮肤数据private List<Map<String, Object>> mData = new ArrayList<>();//资源idprivate int drawId = 0;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initView();//所有的插件List<PlugInBean> allPlugIn = findPlugIn();//加载皮肤数据LoadSkin(allPlugIn);}/*** 加载皮肤** @param allPlugIn*/private void LoadSkin(List<PlugInBean> allPlugIn) {//遍历for (PlugInBean bean : allPlugIn) {HashMap<String, Object> mMap = new HashMap<>();mMap.put("lable", bean.getLabelNmae());mMap.put("package", bean.getPackagNmae());mData.add(mMap);}//建立Adapter并且绑定数据源mAdapter = new SimpleAdapter(this, mData, android.R.layout.simple_list_item_1, new String[]{"lable"}, new int[]{android.R.id.text1});//设置数据mSpinner.setAdapter(mAdapter);//设置监听事件mSpinner.setOnItemSelectedListener(this);}/*** 获取手机里的插件** @return*/private List<PlugInBean> findPlugIn() {mList = new ArrayList<>();//获取相关信息PackageManager mPackageManager = getPackageManager();//获取卸载/未安装的安装包信息List<PackageInfo> mUninstallPackage = mPackageManager.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);//遍历拿到我们的信息for (PackageInfo info : mUninstallPackage) {String pkgNmae = info.packageName;//获取shareId,根据id判断是否是我们的IDString shareUserId = info.sharedUserId;if (!TextUtils.isEmpty(shareUserId)) {//如果id相同if (shareUserId.equals("com.liuguilin.share")) {//且排除自己的包名if (!pkgNmae.equals(getPackageName())) {//这个就是我们的插件了String lable = mPackageManager.getApplicationLabel(info.applicationInfo).toString();PlugInBean bean = new PlugInBean();bean.setLabelNmae(lable);bean.setPackagNmae(pkgNmae);mList.add(bean);}}}}return mList;}/*** 初始化View*/private void initView() {//初始化控件mSpinner = (Spinner) findViewById(R.id.mSpinner);}/*** 选中监听事件** @param adapterView* @param view* @param i* @param l*/@Overridepublic void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {PlugInBean bean = mList.get(i);//插件的包名String packageNmae = bean.getPackagNmae();Context mContext = null;try {//无视警告 访问代码mContext = createPackageContext(packageNmae, CONTEXT_IGNORE_SECURITY | CONTEXT_INCLUDE_CODE);} catch (PackageManager.NameNotFoundException e) {e.printStackTrace();}//获取图片getImg(packageNmae, mContext);//通过ID加载插件的图片getWindow().setBackgroundDrawable(mContext.getResources().getDrawable(drawId));//findViewById(R.id.mLinearLayout).setBackgroundDrawable(mContext.getResources().getDrawable(drawId));}@Overridepublic void onNothingSelected(AdapterView<?> adapterView) {}/*** 获取插件图片 / 返回图片R文件ID / 反射R文件** @param packageNmae* @param mContext*/private void getImg(String packageNmae, Context mContext) {//类加载器反射插件PathClassLoader pathClass = new PathClassLoader(mContext.getPackageResourcePath(), ClassLoader.getSystemClassLoader());//反射 $ 访问类加载器try {Class<?> forNmae = Class.forName(packageNmae + ".R$drawable", true, pathClass);//拿到所有图片的idField[] files = forNmae.getDeclaredFields();for (Field id : files) {//过滤 / 这里的命名可以注意一下if (id.getName().startsWith("img")) {这就是我们图片R下的IDdrawId = id.getInt(R.drawable.class);//mListId.add(drawId);}}} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}}
}

这里还有一个实体类哦,具体看Demo:

package com.liuguilin.pluginsample;/**  项目名:  PlugInSample *  包名:    com.liuguilin.pluginsample*  文件名:   PlugInBean*  创建者:   LGL*  创建时间:  2016/9/17 4:18*  描述:    插件实体类*/public class PlugInBean {//包名private String packagNmae;//应用名private String labelNmae;public String getPackagNmae() {return packagNmae;}public void setPackagNmae(String packagNmae) {this.packagNmae = packagNmae;}public String getLabelNmae() {return labelNmae;}public void setLabelNmae(String labelNmae) {this.labelNmae = labelNmae;}}

主程序及插件程序:http://download.csdn.net/detail/qq_26787115/9632026

有兴趣的可以加群:555974449

Android插件化的思考——仿QQ一键换肤,思考比实现更重要!相关推荐

  1. android 仿qq换肤功能,Android插件化的思考——仿QQ一键换肤,思考比实现更重要!.doc...

    Android插件化的思考--仿QQ一键换肤,思考比实现更重要! 关于QQ的换肤,他们的实现思路我不是很清楚,但是你可以看一下这张换肤的截图 我们想使用哪个主题就直接下载就好了,这一实现的过程我们大致 ...

  2. opengl源码 实现无缝切换图片过场_手把手讲解 Android hook技术实现一键换肤

    前言 产品大佬又提需求啦,要求app里面的图表要实现白天黑夜模式的切换,以满足不同光线下都能保证足够的图表清晰度. 怎么办?可能解决的办法很多,你可以给图表view增加一个toggle方法,参数Str ...

  3. wegame一键蹲替换文件_手把手讲解 Android hook技术实现一键换肤

    前言 产品大佬又提需求啦,要求app里面的图表要实现白天黑夜模式的切换,以满足不同光线下都能保证足够的图表清晰度. 怎么办?可能解决的办法很多,你可以给图表view增加一个toggle方法,参数Str ...

  4. Android插件化开发指南——实践之仿酷狗音乐首页

    文章目录 1. 前言 2. 布局分析 3. 底部导航栏的实现 4. 顶部导航栏和ViewPager+Fragment的关联 1. 前言 在Android插件化开发指南--2.15 实现一个音乐播放器A ...

  5. Android插件化开发指南——实践之Activity转场效果(仿酷狗音乐启动页)

    文章目录 1. 前言 2. Activity退出动画 2.1 简单使用 2.2 overridePendingTransition 3. 后记 1. 前言 在Android插件化开发指南--2.15 ...

  6. Android插件化思考

    最近几年移动开发业界兴起了「 插件化技术 」的旋风,各个大厂都推出了自己的插件化框架,各种开源框架都评价自身功能优越性,令人目不暇接.随着公司业务快速发展,项目增多,开发资源却有限,如何能在有限资源内 ...

  7. Android插件化开发之用DexClassLoader加载未安装的APK资源文件来实现app切换背景皮肤

    第一步.先制做一个有我们需要的图片资源的APK 如下图,这里有个about_log.png,我们需要生成apk文件. 生成的apk文件如果你不到项目的文件夹里面去取apk,想通过命令放到手机里面去可以 ...

  8. Android 插件化总结

    2019独角兽企业重金招聘Python工程师标准>>> 1.Android中插件开发篇总结和概述 2.Android组件化和插件化开发 3.携程Android App插件化和动态加载 ...

  9. Android 插件化原理 完胜360插件框架 技术实战

    性能优化 Android 性能优化 (一)APK高效瘦身 http://blog.csdn.net/whb20081815/article/details/70140063 Android 性能优化 ...

最新文章

  1. (转载)文件系统与数据库系统的区别
  2. 织梦根目录下面404页面,主页能正常运行404页面,切换至栏目页404页面内的图片不能正常显示,解决...
  3. webuploader结合php实现图片上传到本地和保存数据库
  4. 基于行为树的新手引导设计
  5. 什么是 SAP enhancement package
  6. autoencoder自编码器原理以及在mnist数据集上的实现
  7. 基于jQuery焦点图片新闻代码(JS+CSS)
  8. 廖雪峰Java10加密与安全-3摘要算法-3SHA-1算法
  9. Software caused connection abort: socket write error 问题原因推测
  10. 推荐20个值得收藏的前端开源项目
  11. c语言作业班级管理系统,班级信息管理系统(C语言)
  12. 【2】基于深度神经网络的脑电睡眠分期方法研究(标签导入)
  13. 加入收藏 设为首页代码收藏本页的代码和收藏本站的代码设为首页代码
  14. java retainall_Java Set retainAll()用法及代码示例
  15. 慢就是快的人生哲理_关于慢的境界的哲理美文
  16. 一招解决谷歌浏览器打不开wiki问题
  17. HDMI 连接笔记本与显示器
  18. Oracle分析函数PERCENTILE_CONT
  19. AD(Altium Designer)导出BOM时出错处理
  20. 什么是机器学习?简单理解

热门文章

  1. 《WAP》团队第三次作业--团队项目的原型设计与开发
  2. WSL2 上不了外网。解决!
  3. react入口文件_React项目文件结构解析
  4. 快速修改oracle默认端口号,解除对8080的占用
  5. 云服务器搭建mc服务器小记
  6. 计算机控制技术课程综述,胡计算机控制技术课程综述.doc
  7. 物流、快递自定义打印标签、自定义标签解决方案
  8. 四六级作文——说明文
  9. Java基础IO流概述、字符流、字节流、流操作规律、File类、Properties类、打印流、序列流
  10. mysql字符集的设置