Android插件化的思考——仿QQ一键换肤,思考比实现更重要!
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"?>
<LinearLayout
android: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">
<Spinner
android: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判断是否是我们的ID
String 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
*/
@Override
public 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)));
}
@Override
public 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);
//拿到所有图片的id
Field[] files = forNmae.getDeclaredFields();
for (Field id : files) {
//过滤 / 这里的命名可以注意一下
if (id.getName().startsWith("img")) {
int drawId = 0;
////这就是我们图片R下的ID
drawId = 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<>();
//资源id
private int drawId = 0;
@Override
protected 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判断是否是我们的ID
String 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
*/
@Override
public 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));
}
@Override
public 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);
//拿到所有图片的id
Field[] files = forNmae.getDeclaredFields();
for (Field id : files) {
//过滤 / 这里的命名可以注意一下
if (id.getName().startsWith("img")) {
////这就是我们图片R下的ID
drawId = 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一键换肤,思考比实现更重要!的更多相关文章
- 有关Android插件化思考
最近几年移动开发业界兴起了「 插件化技术 」的旋风,各个大厂都推出了自己的插件化框架,各种开源框架都评价自身功能优越性,令人目不暇接.随着公司业务快速发展,项目增多,开发资源却有限,如何能在有限资源内 ...
- Android 插件化 动态升级
最新内容请见原文:Android 插件化 动态升级 不少朋友私信以及 Android开源交流几个 QQ 群 中都问到这个问题,这里简单介绍下 1.作用 大多数朋友开始接触这个问题是因为 App 爆棚了 ...
- Android 插件化和热修复知识梳理
概述 在Android开发中,插件化和热修复的话题越来越多的被大家提及,同时随着技术的迭代,各种框架的发展更新,插件化和热修复的框架似乎已经日趋成熟,许多开发者也把这两项技术运用到实际开发协作和正式的 ...
- Android插件化(三)载入插件apk中的Resource资源
Android载入插件apk中的Resource资源 简单介绍 怎样载入未安装apk中的资源文件呢?我们从android.content.res.AssetManager.java的源代码中发现,它有 ...
- 安卓架构 视频 Android 插件化架构设计
韩梦飞沙 韩亚飞 313134555@qq.com yue31313 han_meng_fei_sha Android 插件化架构设计-Dream老师 自定义SDK =====
- 【我的Android进阶之旅】Android插件化开发学习资料
1.目前开源的插件开发框架大致有哪些? 1. 任玉刚 的 dynamic-load-apk Github 地址:https://github.com/singwhatiwanna/dynamic-lo ...
- Android插件化开发,初入殿堂
好久没有写博客了,这次准备写写我这几天的研究成果--Android插件化开发框架CJFrameForAndroid. 好久没有写博客了,这次准备写写我这几天的研究成果--Android插件化开发框架C ...
- Android插件化技术——原理篇
<Android插件化技术——原理篇> 转载:https://mp.weixin.qq.com/s/Uwr6Rimc7Gpnq4wMFZSAag?utm_source=androi ...
- 深入理解Android插件化技术
深入理解Android插件化技术 转 https://zhuanlan.zhihu.com/p/33017826 插件化技术可以说是Android高级工程师所必须具备的技能之一,从2012年插件化概 ...
随机推荐
- servlet之cookie实现
三个servlet的实现: package app02c;import java.io.IOException;import java.io.PrintWriter;import javax.serv ...
- ABP领域层知识回顾之---工作单元
1. 前言 在上一篇博文中(http://www.cnblogs.com/xiyin/p/6842958.html) 我们讲到了ABP领域层的仓储.这边博文我们来讲 工作单元.个人觉得比较重要.文 ...
- codefroces 297E Mystic Carvings
problem:一个圆上依次有1~2*n的数字.每个数字都有且只有另一个数字与他相连.选出三条线,使得每条线的两端之间隔的最少点(只包括被选择的6个点)的个数相等.输入输出格式输入格式: The fi ...
- [ZJOI2008]瞭望塔
题目描述 致力于建设全国示范和谐小村庄的H村村长dadzhi,决定在村中建立一个瞭望塔,以此加强村中的治安. 我们将H村抽象为一维的轮廓.如下图所示 我们可以用一条山的上方轮廓折线(x1, y1), ...
- bzoj 5248: [2018多省省队联测]一双木棋
Description 菲菲和牛牛在一块n行m列的棋盘上下棋,菲菲执黑棋先手,牛牛执白棋后手.棋局开始时,棋盘上没有任何棋子, 两人轮流在格子上落子,直到填满棋盘时结束.落子的规则是:一个格子可以落子 ...
- bzoj 4542: [Hnoi2016]大数
Description 小 B 有一个很大的数 S,长度达到了 N 位:这个数可以看成是一个串,它可能有前导 0,例如00009312345 小B还有一个素数P.现在,小 B 提出了 M 个询问,每个 ...
- spoj 1676 AC自动机+矩阵快速
Text Generator Time Limit: 1386MS Memory Limit: 1572864KB 64bit IO Format: %lld & %llu Submi ...
- WiFi天线分集
0 概述 在调试一款古董级射频芯片时,发现它支持1发2收,由于在画板工程师将辅助天线也整出来.等板子贴出来后,就与同事一起折腾这个分集接收功能. 碰到过如下问题,先记录,以便后期有空再继续. 1)发现 ...
- python中读取文件数据时要注意文件路径
我们在用python进行数据处理时往往需要将文件中的数据取出来做一些处理,这时我们应该注意数据文件的路径.文件路径不对,回报如下错误: FileNotFoundError: File b'..Adve ...
- HttpClient 实现 get,post请求
private String sendPost(Map<String,Object> data, String url) { CloseableHttpClient httpClient ...