《设计思想解读开源框架》
第一章、 热修复设计
-
第一节、 AOT/JIT & dexopt 与 dex2oat
-
第二节、 热修复设计之 CLASS_ISPREVERIFIED 问题
-
第三节、热修复设计之热修复原理
-
第四节、Tinker 的集成与使用(自动补丁包生成)
第二章、 插件化框架设计
-
第一节、 Class 文件与 Dex 文件的结构解读
-
第二节、 Android 资源加载机制详解
-
第三节、 四大组件调用原理
-
第四节、 so 文件加载机制
-
第五节、 Android 系统服务实现原理
第三章、 组件化框架设计
-
第一节、阿里巴巴开源路由框——ARouter 原理分析
-
第二节、APT 编译时期自动生成代码&动态类加载
-
第三节、 Java SPI 机制
-
第四节、 AOP&IOC
-
第五节、 手写组件化架构
第四章、图片加载框架
-
第一节、图片加载框架选型
-
第二节、Glide 原理分析
-
第三节、手写图片加载框架实战
第五章、网络访问框架设计
-
第一节、网络通信必备基础
-
第二节、OkHttp 源码解读
-
第三节、Retrofit 源码解析
第六章、 RXJava 响应式编程框架设计
-
第一节、链式调用
-
第二节、 扩展的观察者模式
-
第三节、事件变换设计
-
第四节、Scheduler 线程控制
第七章、 IOC 架构设计
-
第一节、 依赖注入与控制反转
-
第二节、ButterKnife 原理上篇、中篇、下篇
-
第三节、Dagger 架构设计核心解密
第八章、 Android 架构组件 Jetpack
-
第一节、 LiveData 原理
-
第二节、 Navigation 如何解决 tabLayout 问题
-
第三节、 ViewModel 如何感知 View 生命周期及内核原理
-
第四节、 Room 架构方式方法
-
第五节、 dataBinding 为什么能够支持 MVVM
-
第六节、 WorkManager 内核揭秘
-
第七节、 Lifecycles 生命周期
本文包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
然而,这里的补救措施没有执行,那自然有地方有另外的补救措施:
回到之前的LayoutInflater的下面这段代码:
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
这段代码的下面,如果view是空,补救措施如下:
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf(‘.’)) {//包含.说明这不是权限定名的类名
view = onCreateView(parent, name, attrs);
} else {//权限定名走这里
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
这里的两个方法onCreateView(parent, name, attrs)
和createView(name, null, attrs);
都最终索引到:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it’s real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class – remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
Object lastContext = mConstructorArgs[0];
if (mConstructorArgs[0] == null) {
// Fill in the context if not already within inflation.
mConstructorArgs[0] = mContext;
}
Object[] args = mConstructorArgs;
args[1] = attrs;
final View view = constructor.newInstance(args); // 真正需要关注的关键代码,就是这一行,执行了构造函数,返回了一个View对象
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
mConstructorArgs[0] = lastContext;
return view;
} catch (NoSuchMethodException e) {
·····
}
}
这么一大段好像有点让人害怕。其实真正需要关注的,就是反射的代码,最后的 newInstance().
OK,Activity上那些丰富多彩的View的来源,就说到这里, 如果有看不懂的,欢迎留言探讨. ( ̄▽ ̄) !
- app中资源文件大管家
Resources
/AssetManager
是怎么工作的
从我们的终极目的出发:我们要做的是“换肤”,如果我们拿到了要换肤的View,可以对他们进行setXXX属性来改变UI,那么属性值从哪里来?
界面元素丰富多彩,但是这些View,都是用资源文件来进行 "装扮"出来的,资源文件大致可以分为:
图片,文字,颜色,声音视频,字体
等。如果我们控制了资源文件,那么是不是有能力对界面元素进行set某某属性来进行“再装扮”呢? 当然,这是可行的。因为,我们平时拿到一个TextView
,就能对它进行setTextColor
,这种操作,在view
还存活的时候,都可以进行操作,并且这种操作,并不会造成Activity
的重启。
这些资源文件,有一个统一的大管家。可能有人说是R.java文件,它里面统筹了所有的资源文件int值.没错,但是这个R文件是如何产生作用的呢? 答案:Resources.
本来这里应该写上源码追踪记录的,但是由于 源码无法追踪,原因暂时还没找到,之前追查
setContentView(R.layout.xxxx)
的时候还可以debug
,现在居然不行了,很诡异!
答案找到了:因为我使用的是 真机,一般手机厂商都会对原生系统进行修改,然后将系统写到到真机里面。
而,我们debug
,用的是原生SDK
。 用实例来说,我本地是SDK 27
的源码,真机也是27
的系统,但是真机的运行起来的系统的代码,是被厂家修改了的,和我本地的必然有所差别,所以,有些代码报红,就很正常了,无法debug
也很正常。
既然如此,那我就直接写结论了,一张图说明一切:
5. “全app一键换肤” Demo源码详解(戳这里获得源码)
- 项目工程结构:
- 关键类 SkinFactory
SkinFactory
类, 继承LayoutInflater.Factory2 ,它的实例,会负责创建View,收集 支持换肤的view
import android.content.Context;
import android.content.res.TypedArray;
import android.support.v7.app.AppCompatDelegate;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import com.enjoy02.skindemo.R;
import com.enjoy02.skindemo.view.ZeroView;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class SkinFactory implements LayoutInflater.Factory2 {
private AppCompatDelegate mDelegate;//预定义一个委托类,它负责按照系统的原有逻辑来创建view
private List listCacheSkinView = new ArrayList<>();//我自定义的list,缓存所有可以换肤的View对象
/**
- 给外部提供一个set方法
- @param mDelegate
*/
public void setDelegate(AppCompatDelegate mDelegate) {
this.mDelegate = mDelegate;
}
/**
- Factory2 是继承Factory的,所以,我们这次是主要重写Factory的onCreateView逻辑,就不必理会Factory的重写方法了
- @param name
- @param context
- @param attrs
- @return
*/
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
/**
- @param parent
- @param name
- @param context
- @param attrs
- @return
*/
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// TODO: 关键点1:执行系统代码里的创建View的过程,我们只是想加入自己的思想,并不是要全盘接管
View view = mDelegate.createView(parent, name, context, attrs);//系统创建出来的时候有可能为空,你问为啥?请全文搜索 “标记标记,因为” 你会找到你要的答案
if (view == null) {//万一系统创建出来是空,那么我们来补救
try {
if (-1 == name.indexOf(‘.’)) {//不包含. 说明不带包名,那么我们帮他加上包名
view = createViewByPrefix(context, name, prefixs, attrs);
} else {//包含. 说明 是权限定名的view name,
view = createViewByPrefix(context, name, null, attrs);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//TODO: 关键点2 收集需要换肤的View
collectSkinView(context, attrs, view);
return view;
}
/**
- TODO: 收集需要换肤的控件
- 收集的方式是:通过自定义属性isSupport,从创建出来的很多View中,找到支持换肤的那些,保存到map中
*/
private void collectSkinView(Context context, AttributeSet attrs, View view) {
// 获取我们自己定义的属性
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Skinable);
boolean isSupport = a.getBoolean(R.styleable.Skinable_isSupport, false);
if (isSupport) {//找到支持换肤的view
final int Len = attrs.getAttributeCount();
HashMap<String, String> attrMap = new HashMap<>();
for (int i = 0; i < Len; i++) {//遍历所有属性
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
attrMap.put(attrName, attrValue);//全部存起来
}
SkinView skinView = new SkinView();
skinView.view = view;
skinView.attrsMap = attrMap;
listCacheSkinView.add(skinView);//将可换肤的view,放到listCacheSkinView中
}
}
/**
- 公开给外界的换肤入口
*/
public void changeSkin() {
for (SkinView skinView : listCacheSkinView) {
skinView.changeSkin();
}
}
static class SkinView {
View view;
HashMap<String, String> attrsMap;
/**
- 真正的换肤操作
*/
public void changeSkin() {
if (!TextUtils.isEmpty(attrsMap.get(“background”))) {//属性名,例如,这个background,text,textColor…
int bgId = Integer.parseInt(attrsMap.get(“background”).substring(1));//属性值,R.id.XXX ,int类型,
// 这个值,在app的一次运行中,不会发生变化
String attrType = view.getResources().getResourceTypeName(bgId); // 属性类别:比如 drawable ,color
if (TextUtils.equals(attrType, “drawable”)) {//区分drawable和color
view.setBackgroundDrawable(SkinEngine.getInstance().getDrawable(bgId));//加载外部资源管理器,拿到外部资源的drawable
} else if (TextUtils.equals(attrType, “color”)) {
view.setBackgroundColor(SkinEngine.getInstance().getColor(bgId));
}
}
if (view instanceof TextView) {
if (!TextUtils.isEmpty(attrsMap.get(“textColor”))) {
int textColorId = Integer.parseInt(attrsMap.get(“textColor”).substring(1));
((TextView) view).setTextColor(SkinEngine.getInstance().getColor(textColorId));
}
}
//那么如果是自定义组件呢
if (view instanceof ZeroView) {
//那么这样一个对象,要换肤,就要写针对性的方法了,每一个控件需要用什么样的方式去换,尤其是那种,自定义的属性,怎么去set,
// 这就对开发人员要求比较高了,而且这个换肤接口还要暴露给 自定义View的开发人员,他们去定义
// …
}
}
}
/**
- 所谓hook,要懂源码,懂了之后再劫持系统逻辑,加入自己的逻辑。
- 那么,既然懂了,系统的有些代码,直接拿过来用,也无可厚非。
*/
//*下面一大片,都是从源码里面抄过来的,并不是我自主设计
// 你问我抄的哪里的?到 AppCompatViewInflater类源码里面去搜索:view = createViewFromTag(context, name, attrs);
static final Class<?>[] mConstructorSignature = new Class[]{Context.class, AttributeSet.class};//
final Object[] mConstructorArgs = new Object[2];//View的构造函数的2个"实"参对象
private static final HashMap<String, Constructor<? extends View>> sConstructorMap = new HashMap<String, Constructor<? extends View>>();//用映射,将View的反射构造函数都存起来
static final String[] prefixs = new String[]{//安卓里面控件的包名,就这么3种,这个变量是为了下面代码里,反射创建类的class而预备的
“android.widget.”,
“android.view.”,
“android.webkit.”
};
/**
- 反射创建View
- @param context
- @param name
- @param prefixs
- @param attrs
- @return
*/
private final View createViewByPrefix(Context context, String name, String[] prefixs, AttributeSet attrs) {
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
if (constructor == null) {
try {
if (prefixs != null && prefixs.length > 0) {
for (String prefix : prefixs) {
clazz = context.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);//控件
if (clazz != null) break;
}
} else {
if (clazz == null) {
clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);
}
}
if (clazz == null) {
return null;
}
constructor = clazz.getConstructor(mConstructorSignature);//拿到 构造方法,
} catch (Exception e) {
e.printStackTrace();
return null;
}
constructor.setAccessible(true);//
sConstructorMap.put(name, constructor);//然后缓存起来,下次再用,就直接从内存中去取
}
Object[] args = mConstructorArgs;
args[1] = attrs;
try {
//通过反射创建View对象
final View view = constructor.newInstance(args);//执行构造函数,拿到View对象
return view;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//**********************************************************************************************
}
关键类 SkinEngine
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Method;
public class SkinEngine {
//单例
private final static SkinEngine instance = new SkinEngine();
public static SkinEngine getInstance() {
return instance;
}
private SkinEngine() {
}
public void init(Context context) {
mContext = context.getApplicationContext();
//使用application的目的是,如果万一传进来的是Activity对象
//那么它被静态对象instance所持有,这个Activity就无法释放了
}
private Resources mOutResource;// TODO: 资源管理器
private Context mContext;//上下文
private String mOutPkgName;// TODO: 外部资源包的packageName
/**
- TODO: 加载外部资源包
*/
public void load(final String path) {//path 是外部传入的apk文件名
File file = new File(path);
if (!file.exists()) {
return;
}
//取得PackageManager引用
PackageManager mPm = mContext.getPackageManager();
//“检索在包归档文件中定义的应用程序包的总体信息”,说人话,外界传入了一个apk的文件路径,这个方法,拿到这个apk的包信息,这个包信息包含什么?
PackageInfo mInfo = mPm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
mOutPkgName = mInfo.packageName;//先把包名存起来
AssetManager assetManager;//资源管理器
try {
//TODO: 关键技术点3 通过反射获取AssetManager 用来加载外面的资源包
assetManager = AssetManager.class.newInstance();//反射创建AssetManager对象,为何要反射?使用反射,是因为他这个类内部的addAssetPath方法是hide状态
//addAssetPath方法可以加载外部的资源包
Method addAssetPath = assetManager.getClass().getMethod(“addAssetPath”, String.class);//为什么要反射执行这个方法?因为它是hide的,不直接对外开放,只能反射调用
addAssetPath.invoke(assetManager, path);//反射执行方法
mOutResource = new Resources(assetManager,//参数1,资源管理器
mContext.getResources().getDisplayMetrics(),//这个好像是屏幕参数
mContext.getResources().getConfiguration());//资源配置
//最终创建出一个 "外部资源包"mOutResource ,它的存在,就是要让我们的app有能力加载外部的资源文件
} catch (Exception e) {
e.printStackTrace();
}
}
/**
- 提供外部资源包里面的颜色
- @param resId
- @return
*/
public int getColor(int resId) {
if (mOutResource == null) {
return resId;
}
String resName = mOutResource.getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resName, “color”, mOutPkgName);
if (outResId == 0) {
return resId;
}
return mOutResource.getColor(outResId);
}
/**
- 提供外部资源包里的图片资源
- @param resId
- @return
*/
public Drawable getDrawable(int resId) {//获取图片
if (mOutResource == null) {
return ContextCompat.getDrawable(mContext, resId);
}
String resName = mOutResource.getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resName, “drawable”, mOutPkgName);
if (outResId == 0) {
return ContextCompat.getDrawable(mContext, resId);
}
return mOutResource.getDrawable(outResId);
}
//… 这里还可以提供外部资源包里的String,font等等等,只不过要手动写代码来实现getXX方法
}
- 关键类的调用方式
1. 初始化"换肤引擎"
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
//初始化换肤引擎
SkinEngine.getInstance().init(this);
}
}
2. 劫持 系统创建view的过程
public class BaseActivity extends AppCompatActivity {
…
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO: 关键点1:hook(劫持)系统创建view的过程
if (ifAllowChangeSkin) {
mSkinFactory = new SkinFactory();
mSkinFactory.setDelegate(getDelegate());
LayoutInflater layoutInflater = LayoutInflater.from(this);
layoutInflater.setFactory2(mSkinFactory);//劫持系统源码逻辑
}
super.onCreate(savedInstanceState);
}
3. 执行换肤操作
protected void changeSkin(String path) {
if (ifAllowChangeSkin) {
File skinFile = new File(Environment.getExternalStorageDirectory(), path);
SkinEngine.getInstance().load(skinFile.getAbsolutePath());//加载外部资源包
mSkinFactory.changeSkin();//执行换肤操作
mCurrentSkin = path;
}
}
- 效果展示
- 注意事项
1. 皮肤包skin_plugin module,里面,只提供需要换肤的资源即可,不需要换肤的资源,还有src目录下的源码
(只是删掉java源码文件,不要删目录结构啊....(●´∀`●)
),不要放在这里,无端增大皮肤包的体积.
2. 皮肤包 skin_plugin module的gradle sdk版本最好和app module的保持完全一致,否则无法保证不会出现奇葩问题.
3. 用皮肤包skin_plugin module 打包生成的apk文件,常规来说,是放在手机内存里面,然后由app module内的代码去加载。至于是手机内存里面的哪个位置,那就见仁见智了. 我是使用的mumu模拟器,我放在了最外层的根目录下面,然后读取这个位置的代码是:
File skinFile = new File(Environment.getExternalStorageDirectory(), "skin.apk");
*4. 上图中,打了两个皮肤包,要注意:打两个皮肤包运行demo,打之前,一定要记得替换drawable图片资源为同名文件,以及
不然切换没有效果.*
结语
学习福利
【Android 详细知识点思维脑图(技能树)】
其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。
虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
[外链图片转存中…(img-XxFuPDnn-1714819795359)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!