1.Android xml属性资源的加载

以ImageView的src为例 看看Android是如何找到图片资源的
我们通常使用image的代码如下

    <ImageViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/btn_back"/>

Android是如何加载到这个资源的呢
点进src的源码 这是Android源码的定义的文件 显然这种写法和我们使用

    <declare-styleable name="ImageView"><!-- Sets a drawable as the content of this ImageView. --><attr name="src" format="reference|color" />...</declare-styleable>

很明显 这里使用了下xml自定义属性 查看ImageView源码进一步证实这一点
我们在imageview的构造方法中发现了如下代码

        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);final Drawable d = a.getDrawable(R.styleable.ImageView_src);//重点跟踪if (d != null) {setImageDrawable(d);}

这是经典的读取自定义属性的代码 我们接着跟踪 看drawable是如何获取到的
TypeArray部分代码:

    @Nullablepublic Drawable getDrawable(@StyleableRes int index) {return getDrawableForDensity(index, 0);}public Drawable getDrawableForDensity(@StyleableRes int index, int density) {if (mRecycled) {throw new RuntimeException("Cannot make calls to a recycled instance!");}final TypedValue value = mValue;if (getValueAt(index * STYLE_NUM_ENTRIES, value)) {if (value.type == TypedValue.TYPE_ATTRIBUTE) {throw new UnsupportedOperationException("Failed to resolve attribute at index " + index + ": " + value);}if (density > 0) {// If the density is overridden, the value in the TypedArray will not reflect this.// Do a separate lookup of the resourceId with the density override.mResources.getValueForDensity(value.resourceId, density, value, true);}return mResources.loadDrawable(value, value.resourceId, density, mTheme);//重点}return null;}

最终我们发现 图片资源由mResources进行解析
那么mResources是如何进行初始化的呢?

2.mResources的初始化

TypeArray中的mResources初始化不是很容易看懂,我们从其他方面入手,比如我们经常使用context.getResources()来获取string 颜色 图片等 那么 这里的getResources中的Resource是如何初始化的呢(API 28)

 getResources().getDrawable(-1,null);//ContextThemeWrapper@Overridepublic Resources getResources() {return getResourcesInternal();}private Resources getResourcesInternal() {if (mResources == null) {if (mOverrideConfiguration == null) {mResources = super.getResources();//走这里} else {final Context resContext = createConfigurationContext(mOverrideConfiguration);mResources = resContext.getResources();}}return mResources;}//ContextWrapper@Overridepublic Resources getResources() {return mBase.getResources();}// Contextpublic abstract Resources getResources();// 追踪到实现类 ContextImpl@Overridepublic Resources getResources() {return mResources;}void setResources(Resources r) {if (r instanceof CompatResources) {((CompatResources) r).setContext(this);}mResources = r;}c.setResources(createResources(mActivityToken, pi, null, displayId, null,getDisplayAdjustments(displayId).getCompatibilityInfo()));private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {final String[] splitResDirs;final ClassLoader classLoader;try {splitResDirs = pi.getSplitPaths(splitName);classLoader = pi.getSplitClassLoader(splitName);} catch (NameNotFoundException e) {throw new RuntimeException(e);}return ResourcesManager.getInstance().getResources(activityToken,//关键pi.getResDir(),splitResDirs,pi.getOverlayDirs(),pi.getApplicationInfo().sharedLibraryFiles,displayId,overrideConfig,compatInfo,classLoader);}// ResourcesManagerpublic @Nullable Resources getResources(@Nullable IBinder activityToken,@Nullable String resDir,@Nullable String[] splitResDirs,@Nullable String[] overlayDirs,@Nullable String[] libDirs,int displayId,@Nullable Configuration overrideConfig,@NonNull CompatibilityInfo compatInfo,@Nullable ClassLoader classLoader) {try {Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");final ResourcesKey key = new ResourcesKey(resDir,splitResDirs,overlayDirs,libDirs,displayId,overrideConfig != null ? new Configuration(overrideConfig) : null, // CopycompatInfo);classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();return getOrCreateResources(activityToken, key, classLoader);// 关键} finally {Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);}}// ResourcesMananger// 创建Resources的有两个方法 getOrCreateResourcesForActivityLocked以及getOrCreateResourcesLockedprivate @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {synchronized (this) {if (activityToken != null) {...} else {// Clean up any dead references so they don't pile up.ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);// Not tied to an Activity, find a shared Resources that has the right ResourcesImplResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);if (resourcesImpl != null) {if (DEBUG) {Slog.d(TAG, "- using existing impl=" + resourcesImpl);}return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);}// We will create the ResourcesImpl object outside of holding this lock.}// If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.ResourcesImpl resourcesImpl = createResourcesImpl(key);if (resourcesImpl == null) {return null;}// Add this ResourcesImpl to the cache.mResourceImpls.put(key, new WeakReference<>(resourcesImpl));final Resources resources;//重点 看他如何初始化if (activityToken != null) {resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,resourcesImpl, key.mCompatInfo);} else {resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);}return resources;}}//1 getOrCreateResourcesLockedprivate @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,@NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) {// Find an existing Resources that has this ResourcesImpl set.final int refCount = mResourceReferences.size();for (int i = 0; i < refCount; i++) {WeakReference<Resources> weakResourceRef = mResourceReferences.get(i);Resources resources = weakResourceRef.get();if (resources != null &&Objects.equals(resources.getClassLoader(), classLoader) &&resources.getImpl() == impl) {if (DEBUG) {Slog.d(TAG, "- using existing ref=" + resources);}return resources;}}// Create a new Resources reference and use the existing ResourcesImpl object.Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader): new Resources(classLoader);//重点resources.setImpl(impl);mResourceReferences.add(new WeakReference<>(resources));if (DEBUG) {Slog.d(TAG, "- creating new ref=" + resources);Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);}return resources;}//2 getOrCreateResourcesForActivityLockedprivate @NonNull Resources getOrCreateResourcesForActivityLocked(@NonNull IBinder activityToken,@NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl,@NonNull CompatibilityInfo compatInfo) {final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(activityToken);final int refCount = activityResources.activityResources.size();for (int i = 0; i < refCount; i++) {WeakReference<Resources> weakResourceRef = activityResources.activityResources.get(i);Resources resources = weakResourceRef.get();if (resources != null&& Objects.equals(resources.getClassLoader(), classLoader)&& resources.getImpl() == impl) {if (DEBUG) {Slog.d(TAG, "- using existing ref=" + resources);}return resources;}}Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader): new Resources(classLoader);//重点resources.setImpl(impl);activityResources.activityResources.add(new WeakReference<>(resources));if (DEBUG) {Slog.d(TAG, "- creating new ref=" + resources);Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);}return resources;}//两个创建resources的方法都类似 都使用了缓存机制 都是通过如下代码创建resources的Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader): new Resources(classLoader);//而通过CompatResources创建的其实和直接用Resources创建的差不多// CompatResources.javapublic CompatResources(ClassLoader cls) {super(cls);//super是ResourcesmContext = new WeakReference<>(null);}//  Resources.javapublic Resources(@Nullable ClassLoader classLoader) {mClassLoader = classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader;}// 也就是无论如何 都是通过 Resources resources = new Resources(classLoader)创建的Resources// 而实际上 Resources内部包含了ResourcesImpl对象 Resources利用ResourcesImpl来获取系统的一些资源// 如1.获取图片public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)throws NotFoundException {return getDrawableForDensity(id, 0, theme);}public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {final TypedValue value = obtainTempTypedValue();try {final ResourcesImpl impl = mResourcesImpl;impl.getValueForDensity(id, density, value, true);return impl.loadDrawable(this, value, id, density, theme);} finally {releaseTempTypedValue(value);}}// 2.获取Dimensionpublic float getDimension(@DimenRes int id) throws NotFoundException {final TypedValue value = obtainTempTypedValue();try {final ResourcesImpl impl = mResourcesImpl;impl.getValue(id, value, true);if (value.type == TypedValue.TYPE_DIMENSION) {return TypedValue.complexToDimension(value.data, impl.getDisplayMetrics());}throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)+ " type #0x" + Integer.toHexString(value.type) + " is not valid");} finally {releaseTempTypedValue(value);}}// 3.获取colorpublic int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException {final TypedValue value = obtainTempTypedValue();try {final ResourcesImpl impl = mResourcesImpl;impl.getValue(id, value, true);if (value.type >= TypedValue.TYPE_FIRST_INT&& value.type <= TypedValue.TYPE_LAST_INT) {return value.data;} else if (value.type != TypedValue.TYPE_STRING) {throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)+ " type #0x" + Integer.toHexString(value.type) + " is not valid");}final ColorStateList csl = impl.loadColorStateList(this, value, id, theme);return csl.getDefaultColor();} finally {releaseTempTypedValue(value);}}// 4.获取Stringpublic String getString(@StringRes int id) throws NotFoundException {return getText(id).toString();}@NonNull public CharSequence getText(@StringRes int id) throws NotFoundException {CharSequence res = mResourcesImpl.getAssets().getResourceText(id);if (res != null) {return res;}throw new NotFoundException("String resource ID #0x"+ Integer.toHexString(id));}// 我们可以直接创建ResourcesImpl或者创建Resources后通过其内部的ResourcesImpl来访问资源 这里我像视频一样选择后者// 其中要使得Resources和mResourcesImpl都进行初始化 最简单的调用时调用Resources的三个参数的构造方法public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {this(null);mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());}// 需要看一下assets metrics config是如何初始化的/*** Only for creating the System resources.*/private Resources() {this(null);final DisplayMetrics metrics = new DisplayMetrics();metrics.setToDefaults();final Configuration config = new Configuration();config.setToDefaults();mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config,new DisplayAdjustments());}

其中最重要的是参数AssetManager 上述例子使用的是系统的AssetManager 我们自己创建的时候需要使用自己path来创建AssetManager,具体灵感来自API 26的源码
AssetManager assets = new AssetManager();
assets.addAssetPath(resDir)// resDir apk的目录
API 28的相关代码如下

    /*** @deprecated Use {@link #setApkAssets(ApkAssets[], boolean)}* @hide*/@Deprecatedpublic int addAssetPath(String path) {return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);}private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {Preconditions.checkNotNull(path, "path");synchronized (this) {ensureOpenLocked();final int count = mApkAssets.length;// See if we already have it loaded.for (int i = 0; i < count; i++) {if (mApkAssets[i].getAssetPath().equals(path)) {return i + 1;}}final ApkAssets assets;try {if (overlay) {// TODO(b/70343104): This hardcoded path will be removed once// addAssetPathInternal is deleted.final String idmapPath = "/data/resource-cache/"+ path.substring(1).replace('/', '@')+ "@idmap";assets = ApkAssets.loadOverlayFromPath(idmapPath, false /*system*/);} else {assets = ApkAssets.loadFromPath(path, false /*system*/, appAsLib);}} catch (IOException e) {return 0;}mApkAssets = Arrays.copyOf(mApkAssets, count + 1);mApkAssets[count] = assets;nativeSetApkAssets(mObject, mApkAssets, true);invalidateCachesLocked(-1);return count + 1;}}

有了以上的调查 我们就可以创建自己的Resource并取得图片 颜色 String等资源了

3.Demo 获取其他APK的资源

准备工作:

3.1.创建皮肤包

创建一个Android项目 里面仅仅放一个图片abc.png 生成一个APK 重命名为test.skin 将apk放置到手机内存根目录
/storage/emulated/0/test.skin
首先需要申请内存访问权限,之前就是因为没有申请权限 导致获取的id一直是0 结果调试了半天 才发现是没有内存的访问权限

3.2.申请内存访问权限

public class Util {public static void checkAndRequestReadSDCardPermission(Activity activity) {if (activity == null) {return;}if (activity.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {activity.requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);}}
}<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

3.3.创建Resource和AssertManager

    protected void initView() {Util.checkAndRequestReadSDCardPermission(this);loadImg();}private void loadImg() {try {ImageView img = findViewById(R.id.emptyImg);// 读取本地的一个 .skin里面的资源Resources superRes = getResources();// 创建AssetManagerAssetManager assetManager = AssetManager.class.newInstance();// 寻找hide的方法Method method = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);// method.setAccessible(true); 如果是私有的Log.e(TAG, "loadImg from : "+Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator + "test.skin");// 反射执行方法 assetManager指向指定的皮肤包 /storage/emulated/0/test.skinmethod.invoke(assetManager, Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator + "test.skin");Resources resource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());// 获取指定路径apk的packageNameString packageName = "";if (this.getApplication() != null) {Log.e(TAG, "loadImage: getApplication!=null");if (this.getApplication().getPackageManager() != null) {String myPath = Environment.getExternalStorageDirectory().getAbsolutePath() +File.separator + "test.skin";Log.e(TAG, "loadImage: myPath ==="+myPath);packageName = this.getApplication().getPackageManager().getPackageArchiveInfo(Environment.getExternalStorageDirectory().getAbsolutePath() +File.separator + "test.skin", PackageManager.GET_ACTIVITIES).packageName;} else {Log.e(TAG, "loadImage: getPackageManager==null");}}// 获取资源 idint drawableId = resource.getIdentifier("abc","drawable",packageName);Drawable drawable = resource.getDrawable(drawableId);img.setImageDrawable(drawable);} catch (Exception e) {Log.e(TAG, "loadImg: "+e.getStackTrace().toString());e.printStackTrace();}}

4.AssetManager的Android部分源码(API28)

我们之前已经知道资源的加载是通过Resources类来加载的,而实际上执行方法的是Resources的内部成员ResourcesImpl,这里我们继续往下深入
以Resources的getDrawableForDensity方法为例 其内部调用了ResourcesImpl的方法
loadDrawable(this, value, id, density, theme);

    @NullableDrawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,int density, @Nullable Resources.Theme theme)throws NotFoundException {...try {if (TRACE_FOR_PRELOAD) {// Log only framework resourcesif ((id >>> 24) == 0x1) {final String name = getResourceName(id);//获取预加载的Android原生的资源 内部调用的也是AssetManager的方法if (name != null) {Log.d("PreloadDrawable", name);}}}...// First, check whether we have a cached version of this drawable// that was inflated against the specified theme. Skip the cache if// we're currently preloading or we're not using the cache.if (!mPreloading && useCache) {// 缓存机制final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);if (cachedDrawable != null) {cachedDrawable.setChangingConfigurations(value.changingConfigurations);return cachedDrawable;}}// 预加载资源// Next, check preloaded drawables. Preloaded drawables may contain// unresolved theme attributes.final Drawable.ConstantState cs;if (isColorDrawable) {cs = sPreloadedColorDrawables.get(key);} else {cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);}Drawable dr;boolean needsNewDrawableAfterCache = false;if (cs != null) {//加载Android原生资源的caseif (TRACE_FOR_DETAILED_PRELOAD) {// Log only framework resources if (((id >>> 24) == 0x1) && (android.os.Process.myUid() != 0)) {final String name = getResourceName(id);if (name != null) {Log.d(TAG_PRELOAD, "Hit preloaded FW drawable #"+ Integer.toHexString(id) + " " + name);}}}dr = cs.newDrawable(wrapper);} else if (isColorDrawable) {//加载颜色类型的drawabledr = new ColorDrawable(value.data);} else {// 加载真正的drawable 重点!!!dr = loadDrawableForCookie(wrapper, value, id, density);}...}}private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,int id, int density) {...if (file.endsWith(".xml")) {//加在xml类型的drawablefinal XmlResourceParser rp = loadXmlResourceParser(file, id, value.assetCookie, "drawable");dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);rp.close();} else {// 加载png jpeg等类型的图片final InputStream is = mAssets.openNonAsset(value.assetCookie, file, AssetManager.ACCESS_STREAMING);AssetInputStream ais = (AssetInputStream) is;dr = decodeImageDrawable(ais, wrapper, value);}...}

我们知道加载是通过Resources类来加载的,而实际上执行方法的是Resources的内部成员,现在看了源码 其实真正查找资源的是AssetManager
我们看看AssetManager是如何初始化的吧
查看AssetManager的构造方法 有两个

    /*** Private constructor that doesn't call ensureSystemAssets.* Used for the creation of system assets.*/@SuppressWarnings("unused")private AssetManager(boolean sentinel) {mObject = nativeCreate();if (DEBUG_REFS) {mNumRefs = 0;incRefsLocked(hashCode());}}/*** Create a new AssetManager containing only the basic system assets.* Applications will not generally use this method, instead retrieving the* appropriate asset manager with {@link Resources#getAssets}.    Not for* use by applications.* @hide*/public AssetManager() {final ApkAssets[] assets;synchronized (sSync) {createSystemAssetsInZygoteLocked();assets = sSystemApkAssets;}mObject = nativeCreate();if (DEBUG_REFS) {mNumRefs = 0;incRefsLocked(hashCode());}// Always set the framework resources.setApkAssets(assets, false /*invalidateCaches*/);}

方法的注释已经说明了区别 无参的构造方法是专门为了创建系统资源的 通过源码我们也知道无参的构造方法在createSystemAssetsInZygoteLocked中调用了包含boolean作为参数的构造方法。
createSystemAssetsInZygoteLocked方法中有个值得关注的点

apkAssets.add(ApkAssets.loadFromPath(FRAMEWORK_APK_PATH, true /*system*/));
private static final String FRAMEWORK_APK_PATH = "/system/framework/framework-res.apk";

这里加载了Android系统的资源apk 我猜测 小米的MIUI或者其他更改系统皮肤的手机制造商 是可以更改这个apk中的资源来达到控制UI式样的目的的
由于Android9.0中 AssetManager的源码变动较大,我能力有限,没有看懂在9.0中AssetManager如何在framework层初始化的
猜测可能的创建方式如下

 nativeCreate()// frameworks/base/core/jni/android_util_AssetManager.cppstatic jlong NativeCreate(JNIEnv* /*env*/, jclass /*clazz*/) {// AssetManager2 needs to be protected by a lock. To avoid cache misses, we allocate the lock and// AssetManager2 in a contiguous block (GuardedAssetManager).return reinterpret_cast<jlong>(new GuardedAssetManager());}struct GuardedAssetManager : public ::AAssetManager {Guarded<AssetManager2> guarded_assetmanager;};//AssetManager2.cppAssetManager2::AssetManager2() {memset(&configuration_, 0, sizeof(configuration_));}

最终创建的可能是AssetManager2
因为不是很懂c++ 再往下就分析不动了。。。
罗升阳的博客讲的比较清楚,不过他的Android版本可能是6.0版本的 Android9.0系统源码已经大变 不过还是有很大的参考价值
https://blog.csdn.net/luoshengyang/article/details/8791064
另外值得说一下的是 resources.arsc
这个是打包后的apk中的一个文件 是各种资源和id的映射,assetManager管理资源应该和这个文件关系很大 使用Android Studio打开apk并选择resources.arsc打开 可以看到详细的映射 我找到了系统的资源apk的resources.arsc 如图

打开我们之前使用皮肤包 还能看到我们放入的资源的id与图片名称的映射

红橙Darren视频笔记 换肤框架1 获取其他apk中的资源相关推荐

  1. 红橙Darren视频笔记 换肤框架4 换肤的功能完善 内存泄漏分析

    上一篇完成了换肤框架的基本搭建,这一次 我们继续补完上一次遗留的一些可以完善的部分 1.完善换肤 1.1退出后再进入应用 不会丢失上一次保存的皮肤 基本原理:将上一次切换的皮肤path保存在Share ...

  2. 红橙Darren视频笔记 IOC注解框架 自己写个注解框架

    本节是架构篇开篇 这里先解释一下最终项目的架构 目标 目标就是不需要进行一大堆的findviewbyid以及各种setListener的杂乱代码,本文以findviewbyid和setOnClickL ...

  3. 红橙Darren视频笔记 IOC注解框架 了解xUtils3与ButterKnife的原理

    1.什么是IOC IOC是Inversion of Control的缩写,直接翻译过来就叫依赖反转,看起来感觉不明觉厉,我觉得IOC就是一种解耦方式.比如原本我们在Activity中findviewb ...

  4. 红橙Darren视频笔记 面试题 为什么view获取宽高为0 onCreate onResume view.post源码浅析(继承activity api27)

    面试题 下面的输出分别为多少 为什么 <?xml version="1.0" encoding="utf-8"?> <LinearLayout ...

  5. 红橙Darren视频笔记 利用阿里巴巴AndFix进行热修复

    注意 由于AndFix在2017年左右就停止更新了,在最新版本的apk上遇到很多问题,我最终也没有成功进行热修复.本节主要是学习热修复的原理 在上一篇 红橙Darren视频笔记 自己捕获异常并保存到本 ...

  6. 红橙Darren视频笔记 UML图简介

    整体架构复制自红橙原视频的课堂笔记 因为他这一课没有博客,所以没有转载链接,CSDN没有转载地址是无法作为转载类型的文章发表的,暂时标记为原创 参考链接 https://blog.csdn.net/r ...

  7. 红橙Darren视频笔记 代理模式 动态代理和静态代理

    红橙Darren视频笔记 代理模式 动态代理和静态代理(Android API 25) 关于代理模式我之前有过相关的介绍: https://blog.csdn.net/u011109881/artic ...

  8. 红橙Darren视频笔记 类加载机制(API28) 自己写个热修复 查看源码网站

    第一部分 类加载机制 一个Activity是如何被Android虚拟机找到的? 在之前的文章 红橙Darren视频笔记 自定义View总集篇(https://blog.csdn.net/u011109 ...

  9. 红橙Darren视频笔记 Behavior的工作原理源码分析

    主要coordinatorlayout的代码来自coordinatorlayout-1.0.0-sources.jar 本文从源码介绍 CoordinatorLayout 的 behavior 怎么工 ...

最新文章

  1. Scala的基本语法总结
  2. 优秀的缓存工具Memcached
  3. 如何优雅地实施持续交付部署
  4. python程序多线程_Python-多线程编程
  5. 迁移学习领域自适应:具有类间差异的联合概率最大平均差异
  6. 2016校招腾讯研发岗笔试题---递归法求解格雷码
  7. WebSocket原理及使用场景(转载)
  8. 电大计算机网络技术基础,电大--2016年电大 计算机与网络技术基础小抄已排版.doc...
  9. C语言求二阶矩阵最小值,C语言科学计算入门之矩阵乘法的相关计算
  10. docker for windows pull镜像文件的安装位置改变方法
  11. 配置静态路由/下一跳知识
  12. 怎么用c语言利用函数求组合数,C++中求组合数的各种方法总结详解
  13. VC++调用libcurl的VC库使用详解
  14. python字典保存为文件_关于python:如何将字典列表保存到文件中?
  15. global mapper 导入bln文件和tif文件
  16. 检查Telerik UI以使用UWP作为PVS-Studio的入门方法
  17. python logging日志模块以及多进程日志
  18. 面向对象设计原则实践:之四.里氏代换原则
  19. [宝塔版] 如何搭建一个微信小程序开源商城?
  20. DirectDraw学习:第一课

热门文章

  1. Protobuffer教程
  2. python sort dict 总结
  3. Missing iOS Distribution signing identity问题解决
  4. RTT线程管理篇——rtt线程恢复
  5. lazada铺货模式的选品_Lazada的商业模式有哪些?要怎么做?
  6. Java-File-文件操作
  7. HTML表div布局,html使用列表 以及div的布局和table的布局
  8. docker server 容器连接sql_docker 容器连接 host的sql server失败
  9. MogDB存储过程事务控制与异常块
  10. DTC精彩回顾—王义成:国产数据库技术发展的探索与思考