Android骨架屏效果的实现与原理解析

0、前言

大家在使用淘宝的时候,如下图所示有遇到这样的效果,其会只展示一部分骨架大致图,等数据加载完毕之后再展示真正的页面数据。与菊花图相比起来,这样的实现能更好的提升用户的体验,这种效果称做:Skeleton Screen Loading,中文叫做骨架屏

1、骨架屏的实现方式

在现在主流的骨架屏实现效果中有两种方式:

这些开源库中,自己比较喜欢今天Skeleton这个开源库,总结了有如下一些优缺点:

优点:

  1. 代码方案实现及使用方式简单,通过替换View和Adapter实现效果,使用Builder设计模式来构造。
  2. 代码耦合程度不高。没有复杂的设计模式,使得代码结构清晰明了。
  3. 骨架屏的效果使用相对于较灵活,可以对整个布局实现骨架屏效果,也可以对单一View实现骨架屏效果。

缺点:

  1. 需要对每个骨架屏效果单独写一套xml布局。
  2. 使用的removeView和addView对 原有布局的view进行替换,存在一定的风险性
  3. 必须清晰的知道所bind的View类型,存在一定的类型转化问题。
  4. 依赖了shimmerlayout第三方库

2、Skeleton解读

一、Skeleton的使用方式

展示骨架屏效果:

View rootView = findViewById(R.id.rootView);
skeletonScreen = Skeleton.bind(rootView)
           .load(R.layout.activity_view_skeleton)//骨架屏UI
           .duration(1000)//动画时间,以毫秒为单位
           .shimmer(true)//是否开启动画
           .color(R.color.shimmer_color)//shimmer的颜色
           .angle(30)//shimmer的倾斜角度
           .show();

关闭骨架屏效果并展示原有View:

skeletonScreen.hide()

流程:

**1. 选择需要替换的目标view

  1. 将骨架效果xml与目标view进行绑定
  2. 添加一些效果属性,比如:动画时间、是否开启展示动画、动画颜色等
  3. 在合适的实际关闭骨架屏效果**

二、Skeleton源码实现

Skeleton提供两个绑定方法,分别绑定普通View与RecyclerView,分别返回对应的Builder

1
2
3
4
5
6
7
8
public class Skeleton {
public static RecyclerViewSkeletonScreen.Builder bind(RecyclerView recyclerView) {
return new RecyclerViewSkeletonScreen.Builder(recyclerView);
}
public static ViewSkeletonScreen.Builder bind(View view) {
return new ViewSkeletonScreen.Builder(view);
}
}

我们首先来看看如何实现与普通View绑定,构造方法中传入目标View,并对shimmer动画效果设置默认的颜色,在Builder里面我们可以看到各种相关参数的设定。

1
2
3
4
public Builder(View view) {
this.mView = view;
this.mShimmerColor = ContextCompat.getColor(mView.getContext(), R.color.shimmer_color);
}

接下来再到show的步骤,主要实现还是由ViewSkeletonScreen来实现

1
2
3
4
5
public ViewSkeletonScreen show() {
ViewSkeletonScreen skeletonScreen = new ViewSkeletonScreen(this);
skeletonScreen.show();
return skeletonScreen;
}

其中ViewSkeletonScreen与绑定的RecyclerViewSkeletonScreen都实现了SkeletonScreen接口,SkeletonScreen有两个接口方法分别是

void show();
void hide();

对于ViewSkeletonScreen.show()进入源码,这里出现一个比较重要的类ViewReplacer,等下再进行解析,通过show的源码清楚的知道逻辑:
1、生成骨架效果View
2、利用生成的View替换目标View。

其中生成骨架效果View阶段主要还是通过LayoutInflater去加载传入mSkeletonResID

1
2
3
4
5
6
7
@Override
public void show() {
View skeletonLoadingView = generateSkeletonLoadingView();
if (skeletonLoadingView != null) {
mViewReplacer.replace(skeletonLoadingView);
}
}

接下来主要讲解ViewReplacer类,其构造方法传入目标View

1
2
3
4
5
6
public ViewReplacer(View sourceView) {
mSourceView = sourceView;
mSourceViewLayoutParams = mSourceView.getLayoutParams();
mCurrentView = mSourceView;
mSourceViewId = mSourceView.getId();
}

其比较重要的方法有两个:replace()restore() 这两个方法分别为SkeletonScreen 的show()和hide()的最终实现,首先看replace()方法,有两个方法重载,分别传入targetViewResID或者targetView,最终还是会走到replace(View targetView)中。
其主要逻辑为:

**1. 判断所替换的View和骨架屏效果View是否为同一个View

  1. remove掉在父布局中的目标View
  2. 将骨架屏效果View添加到目标View的父布局中**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void replace(int targetViewResID) {
if (mTargetViewResID == targetViewResID) {
return;
}
if (init()) {
mTargetViewResID = targetViewResID;
replace(LayoutInflater.from(mSourceView.getContext()).inflate(mTargetViewResID, mSourceParentView, false));
}
}

public void replace(View targetView) {
if (mCurrentView == targetView) {
return;
}
if (targetView.getParent() != null) {
((ViewGroup) targetView.getParent()).removeView(targetView);
}
if (init()) {
mTargetView = targetView;
mSourceParentView.removeView(mCurrentView);
mTargetView.setId(mSourceViewId);
mSourceParentView.addView(mTargetView, mSourceViewIndexInParent, mSourceViewLayoutParams);
mCurrentView = mTargetView;
}
}

在执行添加到目标View的父布局中,有执行一个init方法,主要做两件事:

**1. 获取目标View的父View

  1. 找到目标View在父View 中的位置索引,为之后添加骨架屏View到父View中做铺垫**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private boolean init() {
if (mSourceParentView == null) {
mSourceParentView = (ViewGroup) mSourceView.getParent();
if (mSourceParentView == null) {
Log.e(TAG, "the source view have not attach to any view");
return false;
}
int count = mSourceParentView.getChildCount();
for (int index = 0; index < count; index++) {
if (mSourceView == mSourceParentView.getChildAt(index)) {
mSourceViewIndexInParent = index;
break;
}
}
}
return true;
}

至此对普通View的骨架屏效果实现流程已经完全梳理完成,那对于RecyclerView呢?其实两者实现逻辑差不多,主要有两个差异:

  1. RecyclerViewSkeletonScreen的Builder中,相比ViewSkeletonScreen多了一个adapter()方法,传入目标RecyclerViewAdapter
  2. 在show的时候对目标RecyclerView的adapter进行替换,使用骨架屏效果的adapter。hide的时候恢复为原先的Adapter

3、总结

  1. Skeleton的原理主要是通过替换目标View和RecyclerView的Adapter
  2. 在Skeleton的使用过程中最需要关心的两个问题是:show()和hide()的时机
  3. 对于整个页面的骨架屏效果实现,个人推荐在布局中添加一个全屏的空View盖在原先内容上
  4. 注意一些异常情况下的hide(),要不然整个页面就“假死”状态了。

参考:
https://juejin.im/post/5c789a4ce51d457c042d3b31

Android 插件化之ClassLoader

0、前言:

插件化要解决的三个核心问题:类加载、资源加载、组件生命周期管理。

在Android插件化中其原理实际是 Java ClassLoader的原理,此博文主要对Android插件化中类加载中的DexClassLoader做总结,便于之后对Android插件化的理解学习。

Android的Dalvik虚拟机和Java虚拟机的运行原理相同都是将对应的java类加载在内存中运行。而Java虚拟机是加载class文件,也可以将一段二进制流通过defineClass方法生产Class进行加载。Dalvik虚拟机加载的dex文件。dex文件是Android对与Class文件做的优化,以便于提高手机的性能。可以想象dex为class文件的一个压缩文件。dex在Android中的加载和class在jvm中的相同都是基于双亲委派模型,都是调用ClassLoader的loadClass方法加载类。

1、DexClassLoader和PathClassLoader区别

Android 也有自己的 ClassLoader,分为 DexClassLoaderPathClassLoader,这两者有什么区别和关联呢?

阅读源码可以看到两者的构造方法分别为:

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

可以发现DexClassLoaderPathClassLoader 多一个参数String optimizedDirectory,那这个参数具体做什么的呢?继续查看源码我们可以知道optimizedDirectory是用来缓存我们需要加载的dex文件的,并创建一个DexFile对象,如果它为null,那么会直接使用dex文件原有的路径来创建DexFile 对象,其具体体现在如下代码区域:

private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

因此两者区别在于 PathClassLoader 不能直接从 zip 包中得到 dex,因此只支持直接操作 dex 文件或者已经安装过的 apk。而 DexClassLoader 可以加载外部的 apk、jar 或 dex文件,并且会在指定的 outpath 路径存放其 dex 文件。所以在插件化中我们使用DexClassLoader来加载class的,接下来讲解DexClassLoader的用法。

2、DexClassLoader用法

其构造方法为:

DexClassLoader(
    String dexPath, 
       String optimizedDirectory, 
    String librarySearchPath,
     ClassLoader parent)

dexPath:被解压的apk路径,不能为空。
optimizedDirectory:解压后的.dex文件的存储路径,不能为空。这个路径强烈建议使用应用程序的私有路径,不要放到sdcard上,否则代码容易被注入攻击。
libraryPath:os库的存放路径,可以为空,若有os库,必须填写。
parent:父亲加载器,一般为context.getClassLoader(),使用当前上下文的类加载器。

接下来讲解具体使用流程:

1、新建一个名为plugin的project,其中新建一个Bean类,只有一个方法getName()返回一个字符串“My App”,然后对plugin这个工程打包为apk,将apk放在主工程的asser目录中。
在这里插入图片描述
2、构造Classloader

File extractFile = getFileStreamPath("app-debug.apk");
String dexPath = extractFile.getPath();
File fileRelease = getDir("dex", 0);
ClassLoader classLoader = new DexClassLoader(dexPath, fileRelease.getAbsolutePath(), null, getClassLoader());

3、利用构造好的Classloader反射调用插件类中的方法

Class mLoadClassBean;
try {
      mLoadClassBean = classLoader.loadClass("com.example.plugin.Bean");
      Object beanObject = mLoadClassBean.newInstance();
      Method getNameMethod = mLoadClassBean.getMethod("getName");
      getNameMethod.setAccessible(true);
      String name = (String) getNameMethod.invoke(beanObject);
      Log.e("julis", name);
  } catch(Exception e) {
      e.printStackTrace();
  }

成功打印出结果:
在这里插入图片描述

参考:

https://www.jianshu.com/p/4b4f1fa6633c

https://www.jianshu.com/p/53aa2de20cf8

https://cloud.tencent.com/developer/article/1071815

单例模式的设计

我们都知道单例模式很简单,大概是这样:

1
2
3
4
5
6
7
8
9
10
//单线程单例模式实现
public class Singleton {
private static Singleton instance=null;
public static Singleton getInstance() {
if(null==instance){
instance = new Singleton();
}
return instance;
}
}

但是呢,在多线程条件下getInstance()并不是一个原子操作。由于代码没有使用任何同步机制,因此该线程可能会出现线程交错的情形:在instance还是null的时候,如果两个线程同时执行到 if(null==instance)那么会创建两个实例,从而违背了初衷。于是通过简单加锁来解决这种问题:

1
2
3
4
5
6
7
8
9
10
11
12
//简单加锁实现单例模式
public class Singleton {
private static Singleton instance=null;
public static Singleton getInstance() {
synchronized (Singleton.class){//加入synchronized同步
if(null==instance){
instance = new Singleton();
}
return instance;
}
}
}

这种方式实现单例模式固然安全,但意味着每次调用 getInstance()都会申请锁,为了避免开销,我们想到了另一种办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//基于双重检查锁定的错误单例模式实现
public class Singleton {
private static Singleton instance=null;
public static Singleton getInstance() {
if(null==instance){//先检查是否为null,再执行之上的代码
synchronized (Singleton.class){
if(null==instance){
instance = new Singleton();
}
}
}
return instance;
}
}

通过这种方法,虽然第一次检查对变量instance的访问没有加锁从而使竞态仍然可能存在,它似乎避免了锁的开销又保障了线程的安全。然后对 instance = new Singleton();进行伪代码独立子操作:

1
2
3
obj=allocate(Singleton.class);//1、分配对象所需的存储空间
invokeConstructor(obj);//2、初始化obj的引用对象
instance=obj;//3、将对象引用写入共享变量

由于重排序的规则,临界区内的操作可以再临界区内重排序,因此JIT编译器可能将上述子操作重排序为:1->3->2,即在初始化对象之前将对象引用写入实例变量instace。由于锁对有序性的保障是有条件的,而操作1读取intance变量的时候并没有加锁,因此重排序是对1操作是有影响的:该线程可能看到一个未初始化(或者为初始化完毕)的实例,即intance不为null。于是该线程直接就直接返回这个instance变量所引用的实例,而实例可能是未初始化完毕的,这就是可能导致程序出错。明白问题的原因之后,解决方法也不难想到了:只需将instance变量加入volatile修饰则可。于是代码变成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//基于双重检查锁定的正确单例模式实现
public class Singleton {
private static volatile Singleton instance=null;//加入volatile修饰
public static Singleton getInstance() {
if(null==instance){
synchronized (Singleton.class){
if(null==instance){
instance = new Singleton();
}
}
}
return instance;
}
}

到此为止,才正确实现安全的“单例模式”。

参考:《黄文海-Java多线程编程实战指南(核心篇)》

Android监听截屏事件之媒体读取的探索

最近做了一个需求:监听用户截屏,然后生成相关海报。
参考了Android 截屏事件监听的文章,大致思路是:a

1、利用ContentObserver用来监听指定Uri的所有资源变化,当媒体库中有相关图片新增的时候,则发送相关的通知。

2、得到回调的Uri后,借助ContentResolver在媒体数据库中查询最后一条数据

3、对数据做一些过滤。比如短时间重复截屏的情况以及其他App也插入了媒体文件等情况做处理。

不过有一些适配性的问题:

1、截屏后读取文件数据库后获取到件的绝对路径后,利用“screenshot”等关键字判断是否是截屏图片,并不能适配所有手机截屏的命名规则,以及其他应用同时间产生带有“screenshot”等关键词的文件也会有问题。

2、在某些型号手机中(现遇到Vivo)从数据库中读取的文件并不是获取到的最新的截屏文件,而且其他目录的文件,这里就有些难以理解了,所以今天取探究一下媒体数据库的读取。

其中ContentObserver如下代码所示:

` /**
 * 媒体内容观察者(观察媒体数据库的改变)
 */
private class MediaContentObserver extends ContentObserver {
    private Uri mContentUri;
    public MediaContentObserver(Uri contentUri, Handler handler) {
        super(handler);
        mContentUri = contentUri;
    }
    [@Override](https://my.oschina.net/u/1162528)
    public void onChange(boolean selfChange) {
        super.onChange(selfChange);
        handleMediaContentChange(mContentUri);
    }
}`

其中获取最后一次更新的媒体文件时的代码(为便于查看 删除了判空处理代码):

 /**
 * 处理媒体数据库的内容改变
 */
private void handleMediaContentChange(Uri contentUri) {
    Cursor cursor = null;
    /** 读取媒体数据库时需要读取的列 */
    private static final String[] MEDIA_PROJECTIONS =  {
        MediaStore.Images.ImageColumns.DATA,
        MediaStore.Images.ImageColumns.DATE_TAKEN };
    try {
        // 数据改变时查询数据库中最后加入的一条数据
        cursor = mContext.getContentResolver().query(
                contentUri,
                 MEDIA_PROJECTIONS,
                null,
                null,
                MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"
        );
        // 获取各列的索引
        int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
        int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);

        // 获取行数据
        String data = cursor.getString(dataIndex);
        long dateTaken = cursor.getLong(dateTakenIndex);

        // 处理获取到的第一行数据
        handleMediaRowData(data, dateTaken);
}

这次的目的主要探究的是从数据库获取相关信息的过程

1、Android 的多媒体如何存储?

Android的多媒体文件主要存储在 /data/data/com.android.providers.media/databases 目录下,该目录下有连个db文件:

内部存储数据库文件:internal.db

存储卡数据库:external-XXXX.db

媒体文件的操作主要是围绕着这两个数据库来进行,这两个数据库的结构是一样的。

这两个数据库包含这些表:
album_art 、audio 、search 、album_info 、audio_genres、 searchhelpertitle、albums、 audio_genres_map、 thumbnails、
android_metadata、 audio_meta、 video、artist_info 、audio_playlists 、videothumbnails、artists 、audio_playlists_map、
artists_albums_map 、images

2、表的结构
对于Images表:主要存储images信息。表结构如下:

CREATE TABLE images (
_id INTEGER PRIMARY KEY, 
_data TEXT,
_size INTEGER,
_display_name TEXT,
mime_type TEXT,
title TEXT, 
date_added INTEGER, 
date_modified INTEGER,
description TEXT,
picasa_id TEXT,
isprivate INTEGER,
latitude DOUBLE, 
longitude DOUBLE, 
datetaken INTEGER, 
orientation INTEGER, 
mini_thumb_magic INTEGER, 
bucket_id TEXT, 
bucket_display_name TEXT );

`

各字段所表示意思,如图所示:

图片来自:Android MediaProvider数据库模式说明

所以在截屏监听数据的时候所读取的数据库返回值,分别为:

_data :图片据对路径

datetaken:取子EXIF照片拍摄事件,空的话为文件修改时间

private static final String[] MEDIA_PROJECTIONS =  {
      MediaStore.Images.ImageColumns.DATA,
      MediaStore.Images.ImageColumns.DATE_TAKEN };

在查询过程中构造的数据库代码为:

public final Cursor query (Uri uri, 
    String[] projection,
    String selection, 
    String[] selectionArgs, 
    String sortOrder)

`
其中对应的构造参数官方解释为:

uri The URI, using the content:// scheme, for the content to retrieve.

projection A list of which columns to return. Passing null will return all columns, which is inefficient.

selection A filter declaring which rows to return, formatted as an SQL WHERE clause (excluding the WHERE itself). Passing null will return all rows for the given URI.

selectionArgs You may include ?s in selection, which will be replaced by the values from selectionArgs, in the order that they appear in the selection. The values will be bound as Strings.

sortOrder How to order the rows, formatted as an SQL ORDER BY clause (excluding the ORDER BY itself). Passing null will use the default sort order, which may be unordered.


所以参数依次为:
所要查找的目标、所要的返回值、条件限制(类似sql中where)、匹配项、排序规则

所以这里的查询就显而易见了:获取最新图片数据库下data和datatoken列的数据

cursor = mContext.getContentResolver().query(
              contentUri,
              MEDIA_PROJECTIONS,
              null,
              null,
              MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"
      );

然而…并不能解释vivo手机为什么查找出来不是最新截图的图片的问题

Android在子线程中创建Handler为什么会抛出异常?

复习一下消息机制,如下代码:

new Thread() {
        Handler handler = null;
        [@Override](https://my.oschina.net/u/1162528)
        public void run() {
            handler = new Handler();
        }
    }.start();

如果执行会抛出异常:

Can't create handler inside thread Thread.currentThread() that has not called Looper.prepare()

这是为什么呢?

我们进入Handler的构造方法

    public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread " + Thread.currentThread()
                    + " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

从上面的代码我们很清新的可以知道mLooper 为null,那么就会抛出这样的异常,那么mLooper 为什么会为空呢?这是因为在线程中的Looper还未被创建,所以在Looper.myLooper()中sThreadLocal.get()就会返回null。

我们知道 Handler的作用是处理消息,将消息传递给MessageQueue,而MessageQueue存在于Looper中,如果没有Looper那么就没有MessageQueue,所以创建Handler时,Looper不能够为空。

所以以上代码可以进行一个修改:

private void test() {
    new Thread() {
        Handler handler = null;
        [@Override](https://my.oschina.net/u/1162528)
        public void run() {
            Looper.prepare();
            handler = new Handler();
            Looper.loop();
        }
    }.start();
}

其中 Looper.prepare();为当前线程创建Looper并绑定在ThreadLocal中
Looper.loop();执行消息循环,这样子 Handler就能够正常工作了。

基于Volley框架的返回数据的范型处理

在平时最普通的Volley的网络请求中,我们StringRequest是这样请求网络数据的:

StringRequest stringRequest = new StringRequest("http://www.baidu.com",
        new Response.Listener<String>() {
            @Override
            public void onResponse(String response) {
                Log.d("TAG", response);
            }
        }, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
        Log.e("TAG", error.getMessage(), error);
    }

注意在onResponse的时候是拿到的string类型,拿到string后对其再进行相关的解析,我们是否是可以对其直接封装然后拿到具体想要类型的model数据结构呢?所以对其网络请求架构进行一次封装,到达如下效果:

GetGoodDetailByGidRequest getGoodDetailByGidRequest = new GetGoodDetailByGidRequest(mCouponId,
           new RequestListener<List<CouponModel>>() {
               @Override
               public void onSuccess(List<CouponModel> result) {
               }
               @Override
               public void onError(Exception e) {
                   e.printStackTrace();
                   stopLoadingDialog();
               }
           });

这里我们在构造Request的时候指定了返回数据的类型,这样的话就方便了我们在写业务的时候直接使用解析好的数据结构,具体如何做到的呢?

一、让每个Request基于一个带有范型请求类

public abstract class BaseApiRequest<T>  

这里的T就是目标请求期望的model类
在具体实现的时候继承基类,并指定返回类型,下面是一个例子:

public class GetGoodDetailByGidRequest extends BaseApiRequest<List<CouponModel>> {
    public static final String url = CURL.GoodDetailURL;
    public GetGoodDetailByGidRequest(String goodId, RequestListener<List<CouponModel>> requestListener) {
            super(requestListener);
            this.mUrlParams.put("id", goodId);
    }
    @Override
    public String getBaseUrl() {
            return url;
        }
}

二、在基类中构造网络请求

  protected StringRequest getStringRequest() {
    return new StringRequest(requestMethod, getRequestUrl(),
            response -> parseJson(response),
            error -> requestListener.onError(error)) {
        @Override
        protected Map<String, String> getParams() {
            return mEntityParams;
        }
    };
}

在此处实现可以看到Request在基类中进行,然后分别处理返回结果

三、对返回结果进行解析

    private void parseJson(String response) {
    int responseCode = 0;
    int errorCode = 400;
    try {
        JSONObject jsonObject = new JSONObject(response);

        String resultString = jsonObject.getString("data");

        if (jsonObject.has("code")) {
            responseCode = jsonObject.getInt("code");
        }
        if (jsonObject.has("error")) {
            errorCode = jsonObject.getInt("error");
        }

        if (responseCode == 200 || errorCode == 0) {
            if (!TextUtils.isEmpty(response)) {
                Type type = getTType(requestListener.getClass());
                //泛型是实体或者List等类型
                T t = JsonUtils.fromJson(resultString, type);
                requestListener.onSuccess(t);
                return;
            }
            ToastUtils.showToast("Data is empty!");
        }
        ToastUtils.showToast("Response code is error.");
        requestListener.onError(new ParseError());
    } catch (JSONException e) {
        ToastUtils.showToast(e.toString());
        e.printStackTrace();
    }
}

这里是最关键的一步,由于和后端约定好相关返回字段,那么只需要解析字段中目标model的数据,其中比较重要的是这段代码

Type type = getTType(requestListener.getClass());
//泛型是实体或者List等类型
 T t = JsonUtils.fromJson(resultString, type);
 requestListener.onSuccess(t);

通过封装好的 JsonUtils将String转化为对应的model类型,我们知道json转实体对象的时候,需要指明其类type,那这里的type是如何获取到的呢?

其中getTType ()的具体实现为:

 public static Type getTType(Class<?> clazz) {
    //以Type的形式返回本类直接实现的接口.
    Type[] types = clazz.getGenericInterfaces();
    clazz.getInterfaces();
    if (types.length > 0) {
        //返回表示此类型实际类型参数的 Type 对象的数组
        Type[] interfacesTypes = ((ParameterizedType) types[0]).getActualTypeArguments();
        return interfacesTypes[0];
    }
    return null;
}

通过次方法能够获取到请求实现中所指明的请求类型,其中getGenericInterfaces等相关原理可以阅读:https://my.oschina.net/617669559/blog/3012228

所以对于

public class GetGoodDetailByGidRequest extends BaseApiRequest<List<CouponModel>>

那么获取到的就是List类型

四、通过Listener回调相关解析结果

拿到解析好的result并回调给构造Request方法中的listener使用

T t = JsonUtils.fromJson(resultString, type);
requestListener.onSuccess(t);

这样对整个网络请求后的返回数据直接进行解析方便多了。

总结:

1、本文最主要是对基本Request类进行改造,以达到不需要每次重复写解析返回的String数据

2、在获取目标的类的类型的时候,主要是去获取基类中的“T”类型

3、设计不仅适用用Volley同样适用于其他类似的网络请求框架

小弟不才,如有问题,欢迎指出。

Java反射中getGenericInterfaces和getInterfaces的解读

今天在做解析网络请求后得到的数据的转化的时候用到了:getGenericInterfaces这个方法。

 /**
  * 获取回调接口中 T 的具体类型
  *
  * @param clazz
  * @return
  */
   public static Type getTType(Class clazz) {
    //以Type的形式返回本类直接实现的接口.
    Type[] types = clazz.getGenericInterfaces();
    if (types.length > 0) {
        //返回表示此类型实际类型参数的 Type 对象的数组
        Type[] interfacesTypes = ((ParameterizedType) types[0]).getActualTypeArguments();
        return interfacesTypes[0];
    }
    return null;
}

其中回调接口为:

new RequestListener>() {
   @Override
    public void onSuccess(List result) {}

在解析数据的时候这样操作,目的是为了对所有返回的数据进行数据转化为所指定的类型:

Type type = getTType(requestListener.getClass());
     //泛型是实体或者List等类型
     T t = JsonUtils.fromJson(resultString, type);
     requestListener.onSuccess(t);`

类RequestListener为:

public interface RequestListener {
    void onSuccess(T result);
    void onError(Exception e);
}

使用Gson进行json的解析,T fromJson(String json, Type typeOfT);那么怎么才能获取到RequestListener中的的类型呢?
于是我们从接口获取参数化类型处理。

官方文档解释

getGenericInterfaces:

Returns the {@code Type}s representing the interfaces directly implemented by the class or interface represented by this object.释意:返回表示由此对象表示的类或接口直接实现的接口的{@code Type}。

getInterfaces:

Determines the interfaces implemented by the class or interface represented by this object.
释意:返回由此对象表示的类或接口实现的接口。

从解释上面来看出来了,差异在于“接口实现的接口的Type”,接下来用具体示例来解释区别

private class Food{
    String foodName;
}
private interface Eat{
    void eat(String things);
}
private interface Run{
    void run();
}
private class Dog implements Eat,Run{
    @Override
    public void run() { }
    @Override
    public void eat(String things) { }
}
private void main() {
    Class clazz = Dog.class;
    Type[] genericInterfaces = clazz.getGenericInterfaces();
    Class[] interfaces = clazz.getInterfaces();
}
运行结果
![](https://oscimg.oschina.net/oscnet/245442107557694aef0f07c25be0740187c.jpg)

我们可以看到,clazz.getGenericInterfaces()与clazz.getInterfaces()并没有任何差异。因为 并没有:“实现的接口的Type”

接下来看另一段代码,我们对Eat接口改造一下,增加一个参数化类型

private class Food{
    String foodName;
}
private interface Eat{
    void eat(T things);
}
private interface Run{
    void run();
}

private class Dog implements Eat,Run{
    @Override
    public void run() { }
    @Override
    public void eat(Food things) { }
}
private void main() {
    Class clazz = Dog.class;
    Type[] genericInterfaces = clazz.getGenericInterfaces();
    Class[] interfaces = clazz.getInterfaces();
}
运行结果:

关于位运算和HashMap中一个求最小2次幂的算法

今天在HashMap的内部源码的时候,看到这样一个算法:

/**
 * Returns a power of two size for the given target capacity.
 * 返回大于或等于 cap 的最小2次幂
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

第一眼看起来确实是一脸懵逼,通过只知道这是一个获取该数的大于或等于 cap 的最小2次幂,这么厉害,咋实现的呀?

1、数据在内存中如何存储?

我们知道”<<” 和 “>>” 分别代表 左移和右移位运算符号,表示 乘以2 和除以2(大多数时候适用),”>>>”还是第一次见,这是代表什么意思呢?说到这里我们不得不去了解一下数据是如何存储在内存中的:

在32位的计算机系统中,int型数据占几个字节? 4字节。其中每个字节有8个比特位,表示二进制位,位是计算机内部数据储存的最小单位。这是所有编程语言学习者都知道的。也就是说 int类型在内存中有4*8 == 32个比特为 所以如果以整形数10为例,那么它在内存中完整存储的形式为:

00000000 00000000 00000000 00001010 ->对应 1x2^3+0x2^2+1x2^1+0x2^0 =10

那么int类型表示最大的数是不是就是:

11111111 11111111 11111111 11111111 ->对应 1x2^31+1x2^30…1x2^1+1x2^0

但为我们知道int类型的最大值为:2^31-1,显然上面的答案不是正确的。

这是因为在所有被int类型占用的比特位中,左起第一个位(即最高位)就是符号位。int类型的符号位上,0表示正数,1表示负数。在32位操作系统下,其余后面31位是数值位。也就是说:

11111111 11111111 11111111 11111111 所代表的数字为:1x2^30+1x2^29…1x2^1+1x2^0 的相反数为:-(2^31-1)

这里需要注意的是,按原先的逻辑去理解的话

00000000 00000000 00000000 00000000 为+0

10000000 00000000 00000000 00000000 为-0

那他们表示的意义是一样的么?
实际上,在32位系统下int类型中,我们计算机已经强行规定了这种情况,数字0采用“+0”的表示方法,即 00000000 00000000 00000000 00000000;而“-0”这个特殊的数字被定义为了-2^31。

因此我们看到32位系统下int类型的取值范围中,负数部分比正数部分多了一个数字,正数的最大取值是2^31-1,而负数的最小取值是-2^31。正数部分之所以要减去1,是因为被数字0占用了“+0”,而负数部分不需要用来表示0,因此原本的“-0”就用来表示-2^31这个数字。

2、位运算如何进行?

至此我们明白了数据在计算机中的存储形式,那位运算具体怎么运行的呢?
以10和-10为例,其二进制完整表示为:00000000 00000000 00000000 00001010 和 10000000 00000000 00000000 00001010 为了便于观察,我们取后面8位:00001010

  • 对于符号位移

例如将10的二进制向左移1位:那么变成 0001010 0 == 20 原先二进制数的第一位被移除,而最后一位被舍弃。将10的二进制向右移1位 原先二进制数最后一位被移除,第一位补0,则变成 000101 ==5

如将-10的二进制向左移1位, 10000000 00000000 00000000 00001010则变成:

10000000 00000000 00000000 0010100 为-20

如将-10的二进制向右移1位, 10000000 00000000 00000000 00001010则变成 :

注意这里多了一个0-> 1 00000000 00000000 00000000 0000101 <-注意这里少了位

也就是说符号移动,会保留原来的符号位,不会因为右移左移而带走符号位。

  • 对于无符号位移

相反无符号位移会不关注符号位。
例如将-10向右无符号右移就会变成:

010000000 00000000 00000000 0000101 变成了一个很大的正数了!!

如果将-10无符号左移,则变成:

00000000 00000000 00000000 00001010 = 20

但是!!并没有无符号左移动这样一件事情!
跟右移运算不同的是,无符号左移和左移是一样的。因此java没有无符号左移运算。(<<<和<<<=将报错)

因为无符号右移运算需要考虑符号位的右移,而符号位只存在于二进制表示的最左边,最右边没有。所以不用区分无符号左移和左移运算。

3、关于返回大于或等于 cap 的最小2次幂的算法

我们以传入10为例子

由这张图看起来,算法很容易懂了,其实最主要的是为了去让各个位从高到低 从0变成1或者维持1不变,这样就能找到该数最小的2次幂

另外,需要注意一下的是,第一步 int n = cap - 1; 这个操作,执行这个操作的主要原因是为了防止在cap已经是2的n次幂的情况下,经过运算后得到的结果是cap的二倍的结果,例如如果n为l6,经过一系列运算之后,得到的结果是0001 1111,此时最后一步n+1 执行之后,就会返回32,有兴趣的可以自己进行尝试;

小弟不才,如有问题,欢迎指出。

参考来源:

https://blog.csdn.net/c10WTiybQ1Ye3/article/details/89411471
https://www.jianshu.com/p/927009730809

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×