Skip to content

钩子专属

  • 首页
  • 安卓技术
  • 安卓教程

Category 安卓教程

  • 首页   /  
  • 分类归档: "安卓教程"
安卓技术, 安卓教程 6 月 21,2023

十四、EventBus

EventBus优点:开销小,代码优雅。将发送者和接受者解耦

在EventBus3.0之后,事件处理的方法可以随便取名,但是需要添加一个注解@Subscribe,并且要指定线程模型(默认为POSTING)
普通的方法是先注册(register),再post,才能接受到事件;
如果你使用postSticky发送事件,那么可以不需要先注册,也能接受到事件,也就是一个延迟注册的过程。
粘性事件就是为了解决这个问题,通过 postSticky 发送粘性事件,这个事件不会只被消费一次就消失,而是一直存在系统中,直到被 removeStickyEvent 删除掉。
那么只要订阅了该粘性事件的所有方法,只要被register 的时候,就会被检测到,并且执行。

注:只会接收到最近发送的一次粘性事件,之前的会接受不到

作者 oshook
安卓技术, 安卓教程 6 月 21,2023

十五、Activity与Fragment通信

1、fragment与fragment之间通信
方法一:

public class Fragment1 extends Fragment {

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View v=inflater.inflate(R.layout.fragment1,null);
    // 给fragment上的按钮添加点击事件  
    v.findViewById(R.id.btn_change).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
//拿到frgment---fragment之间的通信  
Fragment2 f2= (Fragment2) getActivity().getFragmentManager().findFragmentByTag("two");//two是fragment2的tag,
 
    f2.changeStr("我要改变世界");

}
  });

   return v;
 }
}

方法二:EventBus

2、activity传值给fragment
方法一:
在activity中建一个bundle,把要传的值存入bundle,然后通过fragment的setArguments(bundle)传到fragment,在fragment中,用getArguments接收
方法二:
在onAttach()方法中获取Activity的值,在onCreateView中使用
示例代码:

public class FragmentOne extends Fragment {

private Button btn;
private TextView text;
private String str;

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View v=inflater.inflate(R.layout.fragmentone,null);
    // 给fragment上的按钮添加点击事件
    text = v.findViewById(R.id.tv_fragmentone);
    btn = v.findViewById(R.id.getImgae);
    btn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {

        }
    });

    text.setText(str);


//return inflater.inflate(R.layout.fragmentone,container,false);
    return v;
}


@Override
public void onAttach(Context context) {
    super.onAttach(context);

    str = ((MainActivity)context).toFragmentone();
    Log.e("ss",str);

}
}

3、fragment传值给Activity
方法一:通过在fragment中实现接口的方式
1.fragment中准备回调接口 接口中声明传值的回调方法

2.在fragment中定义属性private MyListener myListener

3.重写fragment中的onAttach()方法:listener = (MyLisener)getActivity();

4.fragment触发事件时回传值

5.Activity中实现回调接口 重写回调方法获取回传的值并显示

示例代码:

public class FragmentOne extends Fragment {

private Button btn;
private TextView text;
private String str;

private MyListener ac;


@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View v=inflater.inflate(R.layout.fragmentone,null);
    // 给fragment上的按钮添加点击事件
    text = v.findViewById(R.id.tv_fragmentone);
    btn = v.findViewById(R.id.getImgae);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
    //通过调用在activity中实现的接口方法,吧数据传给Mainactivity
   ac.sendContent("dddd");

}
});


//return inflater.inflate(R.layout.fragmentone,container,false);
    return v;
}

//activity和fragment联系时候调用,fragment必须依赖activty

public void onAttach(Context context) {
    super.onAttach(context);

 ac = (MyListener) getActivity();//或者ac=(MainActivity) context;

}


//①定义回调接口
public interface MyListener{
    public void sendContent(String info);
}

}

在MainActivity中:

import com.example.wofu.aichi010.Fragment.FragmentOne.MyListener;//fragmentOne中的接口

public class MainActivity extends BaseActivity implements RadioGroup.OnCheckedChangeListener,MyListener{

//fragmentOne中的接口,在这实现,以实现fragmentOne传值Mainactivity
public void sendContent(String info) {
if (info!=null && !"".equals(info)) {
Log.e("sss",info);
}else {

}
}


}
作者 oshook
安卓技术, 安卓教程 6 月 21,2023

十九、泛型

泛型

作用:可以解决数据类型的安全性问题
怎么说呢,所有类都可以向上转型为Object,但是真正用的时候就可以类型转错了。但是如果定义泛型就不会有这个问题。
比如Point《String》 p = new Point《String》()
这样只能输入字符串类型

泛型类定义格式

class 类《T》 {
修饰符 《T》 属性;
public 《T》 getter() ;

}

通过构造方法设置内容

class Point {  
    private T var  
    public Point(T var){  
        this.var = var
    }

}

定义多个泛型

class Point{    
    private T var;
    private K key;
}  

泛型的安全警告

在泛型应用中最好在声明类对象的时候指定好其内部的数据类型
如“Info”,如果未指定泛型,会将T设置为Object类型,这样就可以接受所有类型

通配符?

使用?可以接收任意的内容,但是此内容却无法修改
示例代码:

Info> i = new Info()  
i.setVar("nihao") //会报错  

受限泛型,方法和类都可以使用

《T extends Info》 :只能是Info或者其子类

泛型与子类继承的限制

一个类的子类可以通过对象多态性,为其父类实例化,但是在泛型操作中,子类的泛型类型是无法使用父类的泛型类型接受。
例如:Info不能使用Info
假设Info是一件商品,Info是全部商品

泛型其他应用

泛型接口的定义

interface Info {
    public T getVar()
}

泛型接口实现的两种方式

第一种: 示例代码

class InfoImpl implements Info{

}

第二种:

class InfoImpl implements Info{
}

泛型方法

跟类是不是泛型类没关系
示例代码:

class Demo{  
    public  T fun(T t){
        return t;
    }
}  
可以看着声明T是什么  

通过泛型方法返回泛型类的实例

public static  Info fun(){
    Info temp = new Info()
    temp.setVar(param)
    return temp
}

泛型数组

public static  void fun2(T param[]){
    for(T t:param){}
}  

使用 fun2(Integer i[])

泛型的嵌套使用

Demo> d = null
Info i = null
i = new Info("wold",11)
d = new Demo>(i)
作者 oshook
安卓技术, 安卓教程 6 月 21,2023

六、同步和死锁

重点:

1、 资源共享时需要进行同步操作

2、 程序中过多的同步会产生死锁

卖火车票,加延迟票数就出问题了
因为加入延迟操作,那么一个线程有可能在还没有对票数进行减操作之前,其他线程就已经将票数减少了,这样一来就会出现票数为负的情况

要解决这样的问题,就必须使用同步操作

同步是指多个操作在同一个时间段内只能一个线程进行,其他线程要等此线程完成之后才可以继续执行

死锁

死锁一般情况下就是表示在互相等待

简单示例

public class MainActivity extends AppCompatActivity {

private String obj1 = "obj1";
private String obj2 = "obj2";

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    new Thread(new Lock1()).start();
    new Thread(new Lock2()).start();
}

class Lock1 implements Runnable{

    @Override
    public void run() {
        try {
            synchronized (obj1){
                Log.i("MainActivity1","Lock1 lock obj1");
                Thread.sleep(3000);
                //这时想获得obj2锁,但被Lock2占用了
                synchronized (obj2){
                    Log.i("MainActivity1","Lock1 lock obj2");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }

    }
}

class Lock2 implements Runnable{

    @Override
    public void run() {
        try {
            synchronized (obj2){
                Log.i("MainActivity1","Lock2 lock obj2");
                Thread.sleep(3000);
                synchronized (obj1){
                    Log.i("MainActivity1","Lock2 lock obj1");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}}

注意:死锁不一定导致崩溃,只是线程一直被阻塞着,没法结束,就一直销毁这app的内存资源
手写死锁的关键:关键是Thread.sleep(3000) ,睡3s好让其他线程获取需要的锁资源

作者 oshook
安卓技术, 安卓教程 6 月 21,2023

八、静态代理

定义:给目标对象提供一个代理对象,并由代理对象控制对目标对象的引用。
代理对象:起到中介作用

好处:新增额外功能不用改变真实对象类,比如真实对象仅仅买了个礼物,现在打算新增包装礼物功能,就可以用静态代理方式,创建代理对象,里面封装真实对象买礼物,额外做包装礼物功能

缺点:

  1. 由于在客户端(使用时)和真实主题之间增加了代理对象,因此会造成请求的处理速度变慢
  2. 1个静态代理只服务1种类型的目标对象,如果目标对象较多情况下,若采用静态代理,则会出现静态代理对象量多,代码量大,从而导致代码复杂问题

操作原理:创建抽象对象接口,即真实对象和代理对象都要实现的接口,然后在代理对象实现和真实对象实现一样的方法中,引用或者创建真实对象,调用真实对象的方法,然后代理对象额外做一些操作。

示例代码:
步骤1、创建抽象对象接口(Subject):声明你(真实对象)需要让代购(代理对象)帮忙做的事(买Mac)

interface Subject {
    void buyGift();
}

步骤2、创建真实对象

class RealSubject implements Subject {
    @Override
    public void buyGift() {
        System.out.println("我要买一份礼物");
    }
}

步骤3、创建代理对象,并通过代理类创建真实对象示例访问其方法,再额外加功能

class ProxySubject implements Subject {
    @Override
    public void buyGift() {
        RealSubject realSubject = new RealSubject();
        realSubject.buyGift();
        //额外操作
        System.out.println("礼物进行包装");
    }
}

步骤4、开始使用

public class StaticProxyDemo {

    public static void main(String[] args){
        Subject subject = new ProxySubject();
        subject.buyGift();
    }

}

参考:https://www.jianshu.com/p/a8aa6851e09e

作者 oshook
安卓技术, 安卓教程 6 月 21,2023

十、性能优化-优化Bitmap资源

背景:Android系统分配给每个应用程序的内存有限,而图片资源非常消耗内存(即Bitmap)
原因:若对Bitmap的使用与内存管理不当,则可能引发内存溢出,从而导致程序崩溃

Bitmap占多大的内存?
bitmap.getAllocationByteCount(); 或者
bitmap.getByteCount();

加载Bitmap的时候,通常要做这几件事

一、根据分辨率适配与缩放图片

  1. 设置多套图片资源
    如果项目大小允许,至少在xh/xxh/xxxh中都放置图片,然后通过BitmapFactory.decodeResource获取

  2. 缩放图片比例进行压缩,按图片分辨率需求加载,用inSampleSize
    示例代码:

     BitmapFactory.Options opts = new BitmapFactory.Options();
     opts.inSampleSize = 4;
     Bitmap bitmap = BitmapFactory.decodeFile(imageFile, opts);  
    

二、按需选择合适的解码方式:options.inPreferredConfig

优化原因:不同的图片解码方式对应的内存占用大小相差很大
知识点:

  1. ARGB_4444: 基本不用,因为图片比较失真

  2. 示例ARGB_8888: 32位的ARGB位图,由4个8位组成(即32位),每个像素占8位,即A=8,R=8,G=8,B=8。 8+8+8+8 = 32位= 4个字节。即1个像素占4个字节
    示例:若1张480*800的图片,格式是ARGB_8888,将占1536000字节,即1500KB的内存
    3.默认使用解码方式:ARGB_8888
    特别注意:PNG图片设置无效
    示例代码:

    BitmapFactory.Options options1 = new BitmapFactory.Options();
    options1.inPreferredConfig = Bitmap.Config.RGB_565;
    Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher,options1);

四、使用完毕后释放图片资源

优化原因:使用完毕后若不释放图片资源,容易造成内存泄漏,从而导致内存溢出
优化方案:
在 Android2.3.3(API 10)前,调用 Bitmap.recycle()方法
在 Android2.3.3(API 10)后,采用软引用(SoftReference)
示例代码:

    View view = findViewById(R.id.button);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher);
Drawable drawable = new BitmapDrawable(bitmap);
SoftReference drawableSoftReference = 
                                            new SoftReference(drawable);
    Drawable bgdrawable = drawableSoftReference.get();
if(bgdrawable != null) {
    view.setBackground(bgdrawable);
}

需要注意的是,在垃圾回收器对这个Java对象回收前,SoftReference类所提供的get方法会返回Java对象的强引用,一旦垃圾线程回收该Java对象之后,get方法将返回null。所以在获取软引用对象的代码中,一定要判断是否为null,以免出现NullPointerException异常导致应用崩溃

三、设置图片缓存

优化原因:重复加载图片资源消耗太多资源(cpu,内存与流量)
优化方案:使用三级缓存机制或者软引用
三级缓存机制:内存缓存 – 本地缓存(硬盘、数据库或文件) – 网络缓存

为什么要使用三级缓存:
假如每次启动的时候都从网络拉取图片的话,势必会消耗很多流量。在当前的状况下,对于非wifi用户来说,流量还是很贵的,一个很耗流量的应用,其用户数量级肯定要受到影响。
特别是,当我们想要重复浏览一些图片时,如果每一次浏览都需要通过网络获取,流量的浪费可想而知

什么是三级缓存:

网络缓存, 不优先加载, 速度慢,浪费流量
本地缓存, 次优先加载, 速度快
内存缓存, 优先加载, 速度最快

三级缓存原理:
首次加载App时,通过网络交互来获取图片,之后将图片保存到本地SD卡和内存中
之后运行App时,优先访问内存中的图片缓存,若内存中没有,则加载本地SD卡中的,如果SD卡中没有,则加载网络的
内存缓存示例代码
关键代码LruCache

/**
 * 三级缓存之内存缓存
 */
public class MemoryCacheUtils {

    // private HashMap mMemoryCache=new HashMap();//1.因为强引用,容易造成内存溢出,所以考虑使用下面弱引用的方法
    // private HashMap> mMemoryCache = new HashMap();//2.因为在Android2.3+后,系统会优先考虑回收弱引用对象,官方提出使用LruCache
    private LruCache mMemoryCache;

    public MemoryCacheUtils(){
        long maxMemory = Runtime.getRuntime().maxMemory()/8;//得到手机最大允许内存的1/8,即超过指定内存,则开始回收
        //需要传入允许的内存最大值,虚拟机默认内存16M,真机不一定相同
        mMemoryCache=new LruCache((int) maxMemory){
            //用于计算每个条目的大小
            @Override
            protected int sizeOf(String key, Bitmap value) {
                int byteCount = value.getByteCount();
                return byteCount;
            }
        };

    }

    /**
     * 从内存中读图片
     * @param url
     */
    public Bitmap getBitmapFromMemory(String url) {
        //Bitmap bitmap = mMemoryCache.get(url);//1.强引用方法
        /*2.弱引用方法
        SoftReference bitmapSoftReference = mMemoryCache.get(url);
        if (bitmapSoftReference != null) {
            Bitmap bitmap = bitmapSoftReference.get();
            return bitmap;
        }
        */
        Bitmap bitmap = mMemoryCache.get(url);
        return bitmap;

    }

    /**
     * 往内存中写图片
     * @param url
     * @param bitmap
     */
    public void setBitmapToMemory(String url, Bitmap bitmap) {
        //mMemoryCache.put(url, bitmap);//1.强引用方法
        /*2.弱引用方法
        mMemoryCache.put(url, new SoftReference(bitmap));
        */
        mMemoryCache.put(url,bitmap);
    }
}

图片三级缓存这个,现在基本都是用成熟的图片框架Glide或者picasso
针对Glide的skipMemoryCache(false)//启动内存缓存 diskCacheStrategy() //硬盘缓存策略

参考:https://www.jianshu.com/nb/21401973

作者 oshook
安卓技术, 安卓教程 6 月 21,2023

十一、性能优化–内存泄漏

背景:Android系统分配给每个应用程序的内存有限
内存泄漏的定义:本该被回收的对象不能被回收而停留在堆内存中
内存泄漏的原因:当一个对象已经不再被使用时,本该被回收但却因为另外一个正在使用的对象持有它的引用从而导致它无法被回收
内存泄漏导致的后果:会使应用崩溃

人为原因:无意识地持有对象引用,使得持有引用者的生命周期>被引用者的生命周期

原理:程序在申请内存后,当该内存不需使用但却无法被释放以及归还给程序的现象

Java垃圾回收器(GC)的内存释放 = 垃圾回收算法

分代收集算法

算法思路:

  1. 根据对象存活周期的不同将Java堆内存分为:新生代和老年代
  2. 每块区域特点:
    新生代:对象存活率较低与垃圾回收行为频率高(朝生夕灭的对象,比如方法的局部变量)
    老年代:对象存活率较高与垃圾回收行为频率低 (存活得比较久,但还是要死的对象,如类的成员变量)

3.新生代:采用复制算法,老年代:采用标记-清除、标记-整理算法
还有永久代:在方法区

常见的内存泄漏原因与解决方案

  1. 集合类
  2. Static关键字修饰的成员变量
  3. 非静态内部类/匿名类
  4. 资源对象使用后未关闭

集合类

泄漏原因:集合添加了某个对象,当对象为null时,因为集合仍引用着该对象,会导致该对象没法被回收,从而导致内存泄漏
示例代码:

// 通过 循环申请Object 对象 & 将申请的对象逐个放入到集合List
List objectList = new ArrayList();
   for (int i = 0; i 

解决方案:集合不再用时,清空集合对象 & 设置为null
示例代码:
objectList.clear();
objectList=null;

Static 关键字修饰的成员变量

背景:被Static关键字修饰的成员变量的生命周期=应用程序的生命周期
如Activity的context传给Static的成员变量
典型案例:单例模式
泄漏原因:
若1个对象已不需再使用 而单例对象还持有该对象的引用,那么该对象将不能被正常回收 从而 导致内存泄漏
示例错误单例代码:

// 创建单例时,需传入一个Context
// 若传入的是Activity的Context,此时单例 则持有该Activity的引用
// 由于单例一直持有该Activity的引用(直到整个应用生命周期结束),即使该Activity退出,该Activity的内存也不会被回收
// 特别是一些庞大的Activity,此处非常容易导致OOM

public class SingleInstanceClass {
    private static SingleInstanceClass instance;
    private Context mContext;
    private SingleInstanceClass(Context context) {
    this.mContext = context; // 传递的是Activity的context
}  
  
public SingleInstanceClass getInstance(Context context) {
    if (instance == null) {
        instance = new SingleInstanceClass(context);
        }
    return instance;
    }
}  

解决方案:应传递Application的Context,因Application的生命周期 = 整个应用的生命周期

非静态内部类 / 匿名类

根本原理: 非静态内部类 / 匿名内部类,运行时默认持有外部类的引用

泄露原因:若 非静态内部类所创建的实例 = 静态(其生命周期 = 应用的生命周期),会因 非静态内部类默认持有外部类的引用 而导致外部类无法释放,最终 造成内存泄露
即 外部类中 持有 非静态内部类的静态对象
示例代码:

    // 背景:
   a. 在启动频繁的Activity中,为了避免重复创建相同的数据资源,会在Activity内部创建一个非静态内部类的单例
   b. 每次启动Activity时都会使用该单例的数据

public class TestActivity extends AppCompatActivity {  

// 非静态内部类的实例的引用
// 注:设置为静态  
public static InnerClass innerClass = null; 
   
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);   

// 保证非静态内部类的实例只有1个
if (innerClass == null)
innerClass = new InnerClass();
}

// 非静态内部类的定义
private class InnerClass {
//...
}
}

// 造成内存泄露的原因:
// a. 当TestActivity销毁时,因非静态内部类单例的引用(innerClass)的生命周期 = 应用App的生命周期、持有外部类TestActivity的引用
// b. 故 TestActivity无法被GC回收,从而导致内存泄漏    

解决方案:

  1. 将非静态内部类设置为:静态内部类(静态内部类默认不持有外部类的引用)
  2. 尽量 避免 非静态内部类所创建的实例 = 静态

多线程:AsyncTask、实现Runnable接口、继承Thread类

泄漏原因:
当 工作线程正在处理任务 & 外部类需销毁时, 由于 工作线程实例 持有外部类引用,将使得外部类无法被垃圾回收器(GC)回收,从而造成 内存泄露
泄漏示例代码:

 /** 
 * 方式1:新建Thread子类(内部类)
 */  
public class MainActivity extends AppCompatActivity {

public static final String TAG = "carson:";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 通过创建的内部类 实现多线程
new MyThread().start();

}
// 自定义的Thread子类
private class MyThread extends Thread{
@Override
public void run() {
try {
Thread.sleep(5000);
Log.d(TAG, "执行了多线程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

   /** 
 * 方式2:匿名Thread内部类
 */ 
 public class MainActivity extends AppCompatActivity {

public static final String TAG = "carson:";

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 通过匿名内部类 实现多线程
new Thread() {
@Override
public void run() {
try {
Thread.sleep(5000);
Log.d(TAG, "执行了多线程");
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}.start();
}
}


/** 
  * 分析:内存泄露原因
  */ 
  // 工作线程Thread类属于非静态内部类 / 匿名内部类,运行时默认持有外部类的引用
  // 当工作线程运行时,若外部类MainActivity需销毁
  // 由于此时工作线程类实例持有外部类的引用,将使得外部类无法被垃圾回收器(GC)回收,从而造成 内存泄露  

其他:对Context的引用不要超过它本身的生命周期

参考:https://www.jianshu.com/nb/21401973

作者 oshook
安卓技术, 安卓教程 6 月 21,2023

十二、性能优化-绘制优化和布局优化

绘制性能的好坏主要影响:应用中的页面显示速度
优化方向:

  1. 降低View.onDraw()的复杂度
  2. 避免过度绘制,即布局优化

一、降低View.onDraw()的复杂度

二、布局优化

布局性能的好坏主要影响:应用中页面的显示速度

具体优化方案

1、选择耗费性能较少的布局

性能耗费低的布局 = 功能简单 = FrameLayout、LinearLayout
性能耗费高的布局 = 功能复杂 = RelativeLayout
注:

  1. 嵌套所耗费的性能 > 单个布局本身耗费的性能
  2. 即 完成需求时:宁选择 1个耗费性能高的布局,也不采用嵌套多个耗费性能低的布局

2、减少布局的层级(嵌套)

抽取公共布局时《merge》,子view定位是跟引用《merge》时的父布局有关

3、提高布局的复用性

使用 布局标签 《include》

4、减少初次测量和绘制时间

优化方案:使用 布局标签《ViewStub》 & 尽可能少用布局属性 wrap_content
所以在已知宽高为固定值时,不使用wrap_content

参考:https://www.jianshu.com/nb/21401973

作者 oshook
安卓技术, 安卓教程 6 月 21,2023

十三、数据存储

1、SharedPreferences:以键值对的形式存储在xml中,
应用场景:轻量级存储以及持久性存储
2、SQLite数据库:嵌入式数据库、使用SQL语言
应用场景:结构性数据
3、文件存储:通过Java IO操作写入文件中
量大的数据、文件缓存
4、ContentProvider: Android的四大组件之一,仅作为传输数据的媒介
进程间数据共享、交换
5、网络存储:通过网络获取远程服务器的数据
与远程应用共享,交互。更新数据

作者 oshook
安卓技术, 安卓教程 6 月 21,2023

四、动态代理

原理:

  1. 设计动态代理类(DymanicProxy)时,不需要显式实现与目标对象类(RealSubject)相同的接口,而是将这种实现推迟到程序运行时由JVM来实现。(即在使用时再创建动态代理类和实例)
  2. 通过Java反射机制method.invoke(),通过调用动态代理类对象方法,从而自动调用目标对象的方法

优点:
只需要1个动态代理类就可以解决创建多个静态代理的问题,避免重复,多余代码
缺点:

  1. 效率低
    相比静态代理中直接调用目标对象(真实对象)方法,动态代理则需要通过Java反射机制从而间接调用目标对象方法
  2. 应用场景局限
    因为Java的单继承特性(每个代理类都继承了Proxy类),即只能针对接口创建代理类,不能针对类创建代理类

注:即只能动态代理实现了接口的类,该类可以extends Buy,但是后面必须implements某个接口,代理的实例也是接口的实例。

实例:对礼物进行包装
步骤1:声明处理器类(关键)

//声明处理器类
class DynamicProxy implements InvocationHandler {

    private Object proxyObject;

    //生成动态代理对象
    public Object newProxyInstance(Object subject){
        this.proxyObject = subject;
        return Proxy.newProxyInstance(subject.getClass().getClassLoader(),subject.getClass().getInterfaces(),this);
    }

    //动态代理对象调用目标对象任何方法前,都会调用处理器类的invoke方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        //做额外操作
        System.out.println("包装礼物");
        result = method.invoke(proxyObject,args);
        return result;
    }
}

步骤2:声明目标对象类的抽象接口

interface SubjectBuy{

    void buyGift();
}

步骤3:声明目标对象类Buy1和Buy2

class Buy1 extends Buy implements SubjectBuy {

    @Override
    public void buyGift() {
        System.out.println("我要买mac");
    }


}

class Buy2 implements SubjectBuy {

    @Override
    public void buyGift() {
        System.out.println("我要买手表");
    }
}

class Buy{
    public void buy(){
        System.out.println("我要买买买");
    }
}

步骤4:通过动态代理对象,调用目标对象的方法

public class DynamicProxyDemo {


    public static void main(String[] args){
        DynamicProxy dynamicProxy = new DynamicProxy();

        Buy1 buy1 = new Buy1();
        SubjectBuy subjectBuy = (SubjectBuy) (dynamicProxy.newProxyInstance(buy1));
        subjectBuy.buyGift();

        Buy2 buy2 = new Buy2();
        SubjectBuy subjectBuy2 = (SubjectBuy)dynamicProxy.newProxyInstance(buy2);
        subjectBuy2.buyGift();
    }

}  

步骤4中注意点:首先接口是特殊的类,newProxyInstance返回的object只能向上转型为接口的对象。因为该object本身是有subject.getClass().getInterfaces()所具有的接口生成的。

源码分析

一、如何创建动态代理类及其实例对象

方法:Proxy.newProxyInstance(subject.getClass().getClassLoader(),subject.getClass().getInterfaces(),this)
解析:Proxy.newProxyInstance()作用:根据指定的类装载器、一组接口 & 调用处理器 生成动态代理类实例,并最终返回

//作用:根据指定的类装载器、一组接口 & 调用处理器 生成动态代理类及其对象实例,并最终返回
  
public static Object newProxyInstance(ClassLoader loader, Class>[]interfaces,InvocationHandler h) throws IllegalArgumentException { 
  // 参数说明:
// 参数1:指定产生代理对象的类加载器,需要将其指定为和目标对象同一个类加载器
// 参数2:指定目标对象的实现接口
// 即要给目标对象提供一组什么接口。若提供了一组接口给它,那么该代理对象就默认实现了该接口,这样就能调用这组接口中的方法
// 参数3:指定InvocationHandler对象。即动态代理对象在调用方法时,会关联到哪个InvocationHandler对象

...  
// 仅贴出核心代码

    // 1. 通过 为Proxy类指定类加载器对象 & 一组interface,从而创建动态代理类
    // >>关注3
    Class cl = getProxyClass(loader, interfaces); 
    
    // 2. 通过反射机制获取动态代理类的构造函数,其参数类型是调用处理器接口类型
    Constructor cons = cl.getConstructor(constructorParams); 
    
    // 3. 通过动态代理类的构造函数 创建 代理类实例(传入调用处理器对象
    return (Object) cons.newInstance(new Object[] { h }); 

// 特别注意 
// 1. 动态代理类继承 Proxy 类 & 并实现了在Proxy.newProxyInstance()中提供的一系列接口(接口数组)
// 2. Proxy 类中有一个映射表
  // 映射关系为:(,(,) )
  // 即:1级key = 类加载器,根据1级key 得到 2级key = 接口数组
  // 因此:1类加载器对象 + 1接口数组 = 确定了一个代理类实例
...


}

总结:通过调用处理器类对象的Proxy.newProxyInstance()创建动态代理类及其实例对象(需传入目标类对象)
具体过程:
1.通过为Proxy类指定类加载器和一组接口,从而创建动态代理类的字节码,再根据字节码创建动态代理类
代码:Class cl = getProxyClass(loader, interfaces);

  1. 通过反射机制获取动态代理类的构造函数,其参数类型是调用处理器接口类型
    代码:Constructor cons = cl.getConstructor(constructorParams);
    3.通过动态代理类的构造函数创建代理类实例(传入调用处理器对象)
    代码:(Object) cons.newInstance(new Object[] { h })

如何通过调用动态代理对象方法,从而调用目标对象方法?

Proxy.newProxyInstance()生成了一个动态代理类 及其实例。
该动态代理类记为 :Proxy0Proxy0源码:

    
// 继承:Java 动态代理机制的主类:java.lang.reflect.Proxy
// 实现:与目标对象一样的接口(即上文例子的Subject接口)
public final class $Proxy0 extends Proxy implements Subject {

    // 构造函数
        public ProxySubject(InvocationHandler invocationhandler)   
        {   
        super(invocationhandler);   
    }  
    
     // buybuybuy()是目标对象实现接口(Subject)中的方法
     // 即$Proxy0类必须实现
     // 所以在使用动态代理类对象时,才可以调用目标对象的同名方法(即上文的buybuybuy())
     public final void buybuybuy() {
        try {
            super.h.invoke(this, m3, null); 
            // 该方法的逻辑实际上是调用了父类Proxy类的h参数的invoke()
            // h参数即在Proxy.newProxyInstance(ClassLoader loader, Class>[]interfaces,InvocationHandler h)传入的第3个参数InvocationHandler对象
            // 即调用了调用处理器的InvocationHandler.invoke()
            // 而复写的invoke()利用反射机制:Object result=method.invoke(proxied,args)
            // 从而调用目标对象的的方法 ->>关注4
        return;
        } catch (Error e) {

        } catch (Throwable throwable) {
             throw new UndeclaredThrowableException(throwable);
    }
}

这里源码沾的有点乱,具体看别人的https://www.jianshu.com/p/5dc416ea58a2

此类源码能说明很多问题

  1. 动态代理类已经继承了Proxy父类,所以动态代理类没法再代理实体父类,只能代理接口,只能代理实现接口的类
  2. 动态代理类会实现目标类的所实现的接口,这样就具有目标类的相同实现的方法,然后在实现方法中调用的是super.h.invoke(this, m3, null)。刚好是处理器类重写的invoke方法,而复写的invoke()利用反射机制:Object result=method.invoke(proxied,args)—>从而调用了目标对象的方法

总结:

  1. 动态代理类实现了与目标类一样的接口,并实现了需要目标类对象需要调用的方法
  2. 该方法的实现逻辑 = 调用父类 Proxy类的 h.invoke()
  3. 在 InvocationHandler.invoke() 中通过反射机制,从而调用目标类对象的方法

参考:https://www.jianshu.com/p/5dc416ea58a2

作者 oshook

Copyright © 2023 | 粤ICP备14006518号-3