百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

初级必备:单例模式的7个问题

bigegpt 2025-03-02 16:15 9 浏览

大家好,我是老田,今天给大家分享设计模式中的:单例模式。

稍微啰嗦两句

实话实说,关于单例模式,网上有N多个版本。你估计也看过很多版本。但看完了又能怎样?我技术群里的一位小伙伴,就因为一个单例模式,然后叫他回去等通知了。

先来问几个问题,看看你能回答上来几个:

1、说说单例模式的特点?

2、你知道单例模式的具体使用场景吗?

3、单例模式常见写法有几种?

4、怎么样保证线程安全?

5、怎么不会被反射攻击?

6、怎样保证不会被序列化和反序列化的攻击?

7、枚举为什么不会被序列化?

.....

定义

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

特点:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例
  • 4、隐藏所有的构造方法

目的:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

案例:一家企业只能有一个CEO,有多个了其实乱套了。

使用场景

需要确保任何情况下都绝对只有一个实例。

比如:ServletContextServletConfigApplicationContextDBTool等,都使用到了单列模式。

单例模式的写法

  • 饿汉式
  • 懒汉式(包含双重检查锁、静态内部类)
  • 注册式(以枚举为例)

饿汉式

从名字上就能看出,饿汉:饿了就得先吃饱,所以,一开始就搞定了。

饿汉式主要是使用了static,饿汉式也有两种写法,但本质可以理解为是一样的。

public class HungrySingleton{

 ? ?private static final HungrySingleton INSTANCE;
 ? ?static {
 ? ? ? ?INSTANCE=new HungrySingleton();
 ?  }
// ?  private static final HungrySingleton INSTANCE=new HungrySingleton();
 ? ?private HungrySingleton(){

 ?  }

 ? ?public static HungrySingleton getInstance(){
 ? ? ? ?return INSTANCE;
 ?  }
}

饿汉式有个致命的缺点:浪费空间,不需要也实例化。如果是成千上万个,也这么玩,想想有多恐怖。

于是,就会想到,能不能在使用的时候在实例化,从而引出了懒汉式。

懒汉式

顾名思义,就是需要的时候再创建,因为懒,你不调用我方法,我是不会干活的。

下面是懒汉式的Java代码实现:

public class LazySingleton {

 ? ?private static LazySingleton lazySingleton = null;

 ? ?private LazySingleton() {
 ?  }

 ? ?public static LazySingleton getInstance() {
 ? ? ? ?if (lazySingleton == null) {//01
 ? ? ? ? ? ?lazySingleton = new LazySingleton();//02
 ? ? ?  }
 ? ? ? ?return lazySingleton;
 ?  } 
}

进入getInstance方法,先判断lazySingleton是否为空,为空,则创建一个对象,然后返回此对象。

但是,问题来了:

两个线程同时进入getInstance方法,然后都去执行01这行代码,都是true,然后各自进去创建一个对象,然后返回自己创建的对象。

这岂不是不满足只有唯一 一个对象的了吗?所以这类存在线程安全的问题,那怎么解决呢?

第一印象肯定都是想到加锁。于是,就有了下面的线程安全的懒加载版本:

public class LazySingleton {

 ? ?private static LazySingleton lazySingleton = null;

 ? ?private LazySingleton() {
 ?  }

 ? ?//简单粗暴的线程安全问题解决方案
 ? ?//依然存在性能问题
 ?public synchronized static LazySingleton getInstance() {
 ? ? ? ?if (lazySingleton == null) {
 ? ? ? ? ? ?lazySingleton = new LazySingleton();
 ? ? ?  }
 ? ? ? ?return lazySingleton;
 ?  }
}

给getInstance方法加锁同步锁标志synchronized,但是又涉及到锁的问题了,同步锁是对系统性能有影响的,尽管JDK1.6后,对其做了优化,但它毕竟还是涉及到锁的开销。

每个线程调用getInstance方法时候,都会涉及到锁,所以又对此进行了优化成为了大家耳熟能详的双重检查锁。

双重检查锁

代码实现如下:


public class LazyDoubleCheckSingleton { 
 ? ?private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

 ? ?private LazyDoubleCheckSingleton() {
 ?  }

 ? ?public static LazyDoubleCheckSingleton getInstance() {
 ? ? ? ?if (lazyDoubleCheckSingleton == null) {//01
 ? ? ? ? ? ?synchronized (LazyDoubleCheckSingleton.class) {
 ? ? ? ? ? ? ? ?if (lazyDoubleCheckSingleton == null) {//02
 ? ? ? ? ? ? ? ? ? ?lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
 ? ? ? ? ? ? ?  }
 ? ? ? ? ?  }
 ? ? ?  }
 ? ? ? ?return lazyDoubleCheckSingleton;
 ?  }

}

这段代码中,在01行,如果不为空,就直接返回,这是第一次检查。如果为空,则进入同步代码块,02行又进行一次检查。

双重检查就是现实if判断、获取类对象锁、if判断。

上面这段代码,看似没问题,其实还是有问题的,比如:指令重排序(需要有JVM知识垫底哈)

指令重排是什么意思呢?

比如java中简单的一句

lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();

会被编译器编译成如下JVM指令:

memory =allocate(); //1:分配对象的内存空间

ctorInstance(memory); //2:初始化对象

instance =memory; //3:设置instance指向刚分配的内存地址

但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:

memory =allocate(); //1:分配对象的内存空间

instance =memory; //3:设置instance指向刚分配的内存地址

ctorInstance(memory); //2:初始化对象

为了防止指令重排序,所以,我们可以使用volatile来做文章(注意:volatile能防止指令重排序和线程可见性)。

于是,更好的版本就出来了。


public class LazyDoubleCheckSingleton {
 ? ?//使用volatile修饰
 ? ?private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null; 
 ? ?private LazyDoubleCheckSingleton() {
 ?  }

 ? ?public static LazyDoubleCheckSingleton getInstance() {
 ? ? ? ?if (lazyDoubleCheckSingleton == null) {
 ? ? ? ? ? ?synchronized (LazyDoubleCheckSingleton.class) {
 ? ? ? ? ? ? ? ?if (lazyDoubleCheckSingleton == null) {
 ? ? ? ? ? ? ? ? ? ?lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
 ? ? ? ? ? ? ?  }
 ? ? ? ? ?  }
 ? ? ?  }
 ? ? ? ?return lazyDoubleCheckSingleton;
 ?  }
}

尽管相比前面的版本,确实改进了很多,但依然有同步锁,还是会影响性能问题。于是,又进行优化为静态内部类方式:

静态内部类

下面是静态内部类的代码实现:

public class LazyStaticSingleton {


 ? ?private LazyStaticSingleton() {
 ?  }

 ? ?public static LazyStaticSingleton getInstance() {
 ? ? ? ?return LazyHolder.LAZY_STATIC_SINGLETON;
 ?  }

 ? ?//需要等到外部方法调用是猜执行
 ? ?//巧用内部类的特性
 ? ?//JVM底层执行,完美的规避了线程安全的问题
 ? ?private static class LazyHolder {
 ? ? ? ?private static final LazyStaticSingleton LAZY_STATIC_SINGLETON = new LazyStaticSingleton();
 ?  }
}

利用了内部类的特性,在JVM底层,能完美的规避了线程安全的问题,这种方式也是目前很多项目里喜欢使用的方式。

但是,还是会存在潜在的风险,什么风险呢?

可以使用 反射 暴力的串改,同样也会出现创建多个实例:

反射代码实现如下:

import java.lang.reflect.Constructor;

public class LazyStaticSingletonTest {
 ? ?public static void main(String[] args) {
 ? ? ? ?try {
 ? ? ? ? ? ?Class clazz = LazyStaticSingleton.class;
 ? ? ? ? ? ?Constructor constructor = clazz.getDeclaredConstructor(null);
 ? ? ? ? ? ?//强行访问
 ? ? ? ? ? ?constructor.setAccessible(true);
 ? ? ? ? ? ?Object object = constructor.newInstance();

 ? ? ? ? ? ?Object object1 = LazyStaticSingleton.getInstance();

 ? ? ? ? ? ?System.out.println(object == object1);
 ? ? ?  } catch (Exception ex) {
 ? ? ? ? ? ?ex.printStackTrace();
 ? ? ?  }
 ?  }
}

这段代码运行结果为false。

所以,上面说的双重检查锁的方式,通过反射,还是会存在潜在的风险。怎么办呢?

在《Effect java 》这本书中,作者推荐使用枚举来实现单例模式,因为枚举不能被反射。

枚举

下面是枚举式的单例模式的代码实现:

public enum EnumSingleton {
 ? ?INSTANCE;
 ? ?private Object data;

 ? ?public Object getData() {
 ? ? ? ?return data;
 ?  }

 ? ?public static EnumSingleton getInstance(){
 ? ? ? ?return INSTANCE;
 ?  }
}

我们把上面反射的那个代码,来测试这个枚举式单例模式。

public class EnumTest {
 ? ?public static void main(String[] args) {
 ? ? ? ?try {
 ? ? ? ? ? ?Class clazz = EnumSingleton.class;
 ? ? ? ? ? ?Constructor constructor = clazz.getDeclaredConstructor(null);
 ? ? ? ? ? ?//强行访问
 ? ? ? ? ? ?constructor.setAccessible(true);
 ? ? ? ? ? ?Object object = constructor.newInstance();

 ? ? ? ? ? ?Object object1 = EnumSingleton.getInstance();

 ? ? ? ? ? ?System.out.println(object == object1);
 ? ? ?  } catch (Exception ex) {
 ? ? ? ? ? ?ex.printStackTrace();
 ? ? ?  }
 ?  }
}

运行这段代码:

java.lang.NoSuchMethodException: com.tian.my_code.test.designpattern.singleton.EnumSingleton.()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at com.tian.my_code.test.designpattern.singleton.EnumTest.main(EnumTest.java:41)

还真的不能用反射来搞。如果此时面试官,问为什么呢?

为什么枚举不能被反射呢?

我们在反射的代码中

 ?Constructor constructor = clazz.getDeclaredConstructor(null);

这行代码是获取他的无参构造方法。并且,从错误日志中,我们也可以看到,错误出现就是在getConstructor0方法中,并且,提示的是没有找到无参构造方法。

很奇怪,枚举也是类,不是说如果我们不给类显示定义构造方法时候,会默认给我们创建一个无参构造方法吗?

于是,我想到了一个办法,我们可以使用jad这个工具去反编译的我们的枚举式单例的.class文件。

找到我们的class文件所在目录,然后我们可以执行下面这个命令:

C:\Users\Administrator>jad D:\workspace\my_code\other-local-demo\target\classes
com\tian\my_code\test\designpattern\singleton\EnumSingleton.class
Parsing D:\workspace\my_code\other-local-demo\target\classes\com\tian\my_code\t
st\designpattern\singleton\EnumSingleton.class... Generating EnumSingleton.jad

注意自己的目录哈。

然后打开EnumSingleton.jad 文件:

于是,我就想到了,那我们使用有参构造方法来创建:

public class EnumTest {
 ? ?public static void main(String[] args) {
 ? ? ? ?try {
 ? ? ? ? ? ?Class clazz = EnumSingleton.class; 
 ? ? ? ? ? ?Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);
 ? ? ? ? ? ?//强行访问
 ? ? ? ? ? ?constructor.setAccessible(true);
 ? ? ? ? ? ?Object object = constructor.newInstance("田维常",996);

 ? ? ? ? ? ?Object object1 = EnumSingleton.getInstance();

 ? ? ? ? ? ?System.out.println(object == object1);
 ? ? ?  } catch (Exception ex) {
 ? ? ? ? ? ?ex.printStackTrace();
 ? ? ?  }
 ?  }
}

再次运行这段代码,结果:

java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at com.tian.my_code.test.designpattern.singleton.EnumTest.main(EnumTest.java:45)

提示很明显了,就是不让我们使用反射的方式创建枚举对象。

 ? ?public T newInstance(Object ... initargs)
 ? ? ? ?throws InstantiationException, IllegalAccessException,
 ? ? ? ? ? ? ? IllegalArgumentException, InvocationTargetException
 ?  {
 ? ? ? ?if (!override) {
 ? ? ? ? ? ?if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
 ? ? ? ? ? ? ? ?Class caller = Reflection.getCallerClass();
 ? ? ? ? ? ? ? ?checkAccess(caller, clazz, null, modifiers);
 ? ? ? ? ?  }
 ? ? ?  }
 ? ? ? ?//Modifier.ENUM就是用来判断是否为枚举的
 ? ? ? ?if ((clazz.getModifiers() & Modifier.ENUM) != 0)
 ? ? ? ? ? ?throw new IllegalArgumentException("Cannot reflectively create enum objects");
 ? ? ? ?ConstructorAccessor ca = constructorAccessor; ? // read volatile
 ? ? ? ?if (ca == null) {
 ? ? ? ? ? ?ca = acquireConstructorAccessor();
 ? ? ?  }
 ? ? ? ?@SuppressWarnings("unchecked")
 ? ? ? ?T inst = (T) ca.newInstance(initargs);
 ? ? ? ?return inst;
 ?  }

所以,到此,我们才算真正的理清楚了,为什么枚举不让反射的原因。


序列化破坏

我们以非线程安全的饿汉式来演示一下,看看序列化是如何破坏到了模式的。

public class ReflectTest {

 ? ?public static void main(String[] args) {
 ? ? ? ?// 准备两个对象,singleton1接收从输入流中反序列化的实例
 ? ? ? ?HungrySingleton singleton1 = null;
 ? ? ? ?HungrySingleton singleton2 = HungrySingleton.getInstance();
 ? ? ? ?try {
 ? ? ? ? ? ?// 序列化
 ? ? ? ? ? ?FileOutputStream fos = new FileOutputStream("HungrySingleton.txt");
 ? ? ? ? ? ?ObjectOutputStream oos = new ObjectOutputStream(fos);
 ? ? ? ? ? ?oos.writeObject(singleton2);
 ? ? ? ? ? ?oos.flush();
 ? ? ? ? ? ?oos.close();

 ? ? ? ? ? ?// 反序列化
 ? ? ? ? ? ?FileInputStream fis = new FileInputStream("HungrySingleton.txt");
 ? ? ? ? ? ?ObjectInputStream ois = new ObjectInputStream(fis);
 ? ? ? ? ? ?singleton1 = (HungrySingleton) ois.readObject();
 ? ? ? ? ? ?ois.close();

 ? ? ? ? ? ?System.out.println(singleton1);
 ? ? ? ? ? ?System.out.println(singleton2);
 ? ? ? ? ? ?
 ? ? ? ? ? ?System.out.println(singleton1 == singleton2);

 ? ? ?  } catch (Exception e) {
 ? ? ? ? ? ?e.printStackTrace();
 ? ? ?  }
 ?  }
}

运行结果:

com.tian.my_code.test.designpattern.singleton.HungrySingleton@7e6cbb7a
com.tian.my_code.test.designpattern.singleton.HungrySingleton@452b3a41
false

看到了吗?

使用序列化是可以破坏到了模式的,这种方式,可能很多人不是很清楚。

如何防止呢?

我们对非线程安全的饿汉式代码进行稍微修改:

public class HungrySingleton implements Serializable{

 ? ?private static final HungrySingleton INSTANCE;
 ? ?static {
 ? ? ? ?INSTANCE=new HungrySingleton();
 ?  } 
 ? ?private HungrySingleton(){

 ?  }

 ? ?public static HungrySingleton getInstance(){
 ? ? ? ?return INSTANCE;
 ?  }
 ? ?//添加了readResolve方法,并返回INSTANCE
 ? ?private Object readResolve方法,并返回(){
 ? ? ? ?return INSTANCE;
 ?  }
}

再次运行上那段序列化测试的代码,其结果如下:

com.tian.my_code.test.designpattern.singleton.HungrySingleton@452b3a41
com.tian.my_code.test.designpattern.singleton.HungrySingleton@452b3a41
true

嘿嘿,这样我们是不是就避免了只创建了一个实例?

答案:否

在类ObjectInputStream的readObject()方法中调用了另外一个方法readObject0(false)方法。在readObject0(false)方法中调用了checkResolve(readOrdinaryObject(unshared))方法。

在readOrdinaryObject方法中有这么一段代码:

Object obj;
try { 
 ? ? //是否有构造方法,有构造放就创建实例
 ? ? ?obj = desc.isInstantiable() ? desc.newInstance() : null;
 } catch (Exception ex) {
 ... 
 }
//判断单例类是否有readResolve方法
if (desc.hasReadResolveMethod()) {
 ? ?Object rep = desc.invokeReadResolve(obj); 
}

//invokeReadResolve方法中
if (readResolveMethod != null) { 
 ? ?//调用了我们单例类中的readResolve,并返回该方法返回的对象
 ? ?//注意:是无参方法
 ? ? return readResolveMethod.invoke(obj, (Object[]) null);
}

绕了半天,原来他是这么玩的,上来就先创建一个实例,然后再去检查我们的单例类是否有readResolve无参方法,我们单例类中的readResolve方法

private Object readResolve(){
 ? ? ? ?return INSTANCE;
}

结论

我们重写了readResolve()无参方法,表面上看是只创建了一个实例,其实只创建了两个实例。

紧接着,面试官继续问:枚举式单例能不能被序列化破坏呢?

枚举式单例能不能被序列化破坏呢?

答案:不能被破坏,请看我慢慢给你道来。

don't talk ,show me the code。

我们先来验证一下是否真的不能被破坏,请看代码:

public class EnumTest {

 ? ?public static void main(String[] args) {
 ? ? ? ?// 准备两个对象,singleton1接收从输入流中反序列化的实例
 ? ? ? ?EnumSingleton singleton1 = null;
 ? ? ? ?EnumSingleton singleton2 = EnumSingleton.getInstance();
 ? ? ? ?try {
 ? ? ? ? ? ?// 序列化
 ? ? ? ? ? ?FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
 ? ? ? ? ? ?ObjectOutputStream oos = new ObjectOutputStream(fos);
 ? ? ? ? ? ?oos.writeObject(singleton2);
 ? ? ? ? ? ?oos.flush();
 ? ? ? ? ? ?oos.close();

 ? ? ? ? ? ?// 反序列化
 ? ? ? ? ? ?FileInputStream fis = new FileInputStream("EnumSingleton.obj");
 ? ? ? ? ? ?ObjectInputStream ois = new ObjectInputStream(fis);
 ? ? ? ? ? ?singleton1 = (EnumSingleton) ois.readObject();
 ? ? ? ? ? ?ois.close();

 ? ? ? ? ? ?System.out.println(singleton1);
 ? ? ? ? ? ?System.out.println(singleton2);

 ? ? ? ? ? ?System.out.println(singleton1 == singleton2);

 ? ? ?  } catch (Exception e) {
 ? ? ? ? ? ?e.printStackTrace();
 ? ? ?  }
 ?  }
}

运行结果:

INSTANCE
INSTANCE
true

确实,枚举式单例是不会被序列化所破坏,那为什么呢?总得有个证件理由吧。

在类ObjectInputStream的readObject()方法中调用了另外一个方法readObject0(false)方法。在readObject0(false)方法中调用了checkResolve(readOrdinaryObject(unshared))方法。

 case TC_ENUM:
 ? ?return checkResolve(readEnum(unshared));

在readEnum方法中

private Enum readEnum(boolean unshared) throws IOException {
 ? ? ? ?if (bin.readByte() != TC_ENUM) {
 ? ? ? ? ? ?throw new InternalError();
 ? ? ?  }
 ? ? ? ?Class cl = desc.forClass();
 ? ? ? ?if (cl != null) {
 ? ? ? ? ? ?try {
 ? ? ? ? ? ? ? ?@SuppressWarnings("unchecked")
 ? ? ? ? ? ? ? ?//重点
 ? ? ? ? ? ? ? ?Enum en = Enum.valueOf((Class)cl, name);
 ? ? ? ? ? ? ? ?result = en;
 ? ? ? ? ? ? ? ?//...其他代码省略
 ? ? ? ? ?  }
 ? ? ?  }
}
public static > T valueOf(Class enumType,
 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?String name) {
 ? ? ? //enumType.enumConstantDirectory()返回的是一个HashMap
 ? ? ? //通过HashMap的get方法获取
 ? ? ? ?T result = enumType.enumConstantDirectory().get(name);
 ? ? ? ?if (result != null)
 ? ? ? ? ? ?return result;
 ? ? ? ?if (name == null)
 ? ? ? ? ? ?throw new NullPointerException("Name is null");
 ? ? ? ?throw new IllegalArgumentException(
 ? ? ? ? ? ?"No enum constant " + enumType.getCanonicalName() + "." + name);
}
//返回一个HashMap
 Map enumConstantDirectory() {
 ? ? ? ?if (enumConstantDirectory == null) {
 ? ? ? ? ? ?T[] universe = getEnumConstantsShared();
 ? ? ? ? ? ?if (universe == null)
 ? ? ? ? ? ? ? ?throw new IllegalArgumentException(
 ? ? ? ? ? ? ? ? ? ?getName() + " is not an enum type");
 ? ? ? ? ? ?//使用的是HashMap
 ? ? ? ? ? ?Map m = new HashMap<>(2 * universe.length);
 ? ? ? ? ? ?for (T constant : universe)
 ? ? ? ? ? ? ? ?m.put(((Enum)constant).name(), constant);
 ? ? ? ? ? ?enumConstantDirectory = m;
 ? ? ?  }
 ? ? ? ?return enumConstantDirectory;
}

所以,枚举式单例模式是使用了Map,Map的key就是我们枚举类中的INSTANCE。由于Map的key的唯一性,然后就缔造出唯一实例。江湖上也把这个枚举式单例模式叫做注册式单例模式

在Spring中也是有大量使用这种注册式单例模式,IOC容器就是典型的代表。

总结

本文讲述了单例模式的定义、单例模式常规写法。单例模式线程安全问题的解决,反射破坏、反序列化破坏等。

注意:不要为了套用设计模式,而使用设计模式。而是要,在业务上遇到问题时,很自然地联想单设计模式作为一种捷径方法。

单例模式的优缺点

优点

在内存中只有一个实例,减少内存开销。可以避免对资源的多重占用。设置全局访问点,严格控制访问。

缺点

没有借口,扩展性很差。如果要扩展单例对象,只有修改代码,没有其他途径。

单例模式是 不符合开闭原则的。

最后

单例模式的重点知识总结:

  • 私有化构造器
  • 保证线程安全
  • 延迟加载
  • 防止反射攻击
  • 防止序列化和反序列化的破坏

相关推荐

5分钟调色大片的方法(5分钟调色大片的方法有哪些)

哈喽大家好。在大家印象中一定觉得ps非常难学非常难。大家不要着急,小编的教学都是针对ps零基础的同学的,而且非常实用哦。只要大家跟着图文练习一两遍,保证大家立马学会~!好了,废话少说,下面开始我们今天...

闪白特效原来是这么用的(闪白特效怎么使用)

作者|高艳侠订阅|010-86092062闪白特效是影视作品中应用比较多的效果之一,那么具体该在哪些场景使用闪白特效?具体该如何操作?下面就以AdobePremiere(以下简称PR)为例,...

ppt常用小图标去哪里找?3个矢量素材网站推荐!

ppt是一个注重可视化表达的演示载体,除了高清图片,ppt中另一类常用的素材是各种小图标,也叫矢量图标,巧妙运用小图标能提升整体美观度和表现力,那么ppt常用小图标去哪里找呢?为方便各位快速找到合适的...

有什么好用的截图录屏工具?试试这9款

经常有朋友反馈苦于缺乏截屏和录屏的趁手工具,本期我们分享几个相当好用的截屏和录屏工具,希望能帮到大家。ScreenToGifScreenToGif是一款免费且开源的录屏工具。此款工具最大的特点是可以...

配色苦手福音!专业快速色环配色PS插件

今天橘子老师给的大家介绍的是一款快速配色的插件,非常强大配色苦手福音来啦!(获取方式见文末)【插件介绍】配色在后期设计中占有主导地位,好的配色能让作品更加抢眼Coolorus这款专业的配色插件,能够...

如何用PS抠主体?(ps怎么抠主体)

1.主体法抠图-抠花苞和花梗导入一张荷花苞的照片,点击上图中顶部“选择”菜单栏,下拉单击“主体”。可以看到,只有花苞被选中,但是花梗并没有被选中。接下来单击上图中左侧工具栏的“快速选择工具”,上图中顶...

2799元的4K电视,有保障吗?(买4k电视机哪个品牌好)

在上一期《电脑报》的3·15专题报道中,我们揭露了一款不靠谱的42英寸4K智能电视——TCLD42A561U。这款售价2699元的4K智能电视不仅4K画质方面存在严重问题,而且各种功能和应用体验也不理...

苹果电脑的Touch Bar推出一段时间了 这款工具可以帮你开发适用于它的APP

距离苹果推出带有TouchBar的MacBookPro已经有一段时间了,除了那些像Adobe、Google和Microsoft大公司在开发适用于TouchBar的应用之外,其实还有很多独立的开...

如魔法般吸取颜色的桌灯(如魔法般吸取颜色的桌灯叫什么)

色彩为生活带来的感官刺激,逐渐被视为理所当然。一盏桌灯运用它的神奇力量,将隐藏于物件中的颜色逐一释放,成为装点环境的空间魔法师。ColorUp是一款可以改变颜色的吸色台灯,沿用传统灯泡的造型,融入了拾...

一篇文章带你用jquery mobile设计颜色拾取器

【一、项目背景】现实生活中,我们经常会遇到配色的问题,这个时候去百度一下RGB表。而RGB表只提供相对于的颜色的RGB值而没有可以验证的模块。我们可以通过jquerymobile去设计颜色的拾取器...

ps拾色器快捷键是什么?(ps2019拾色器快捷键)

ps拾色器快捷键是什么?文章末尾有获取方式,按照以下步骤就能自动获得!学会制作PS特效需要一定程度的耐心和毅力。初学者可以从基本的工具和技术开始学习,逐渐提高他们的技能水平。同时,观看更多优秀的特效作...

免费开源的 Windows 截图录屏工具,支持 OCR 识别和滚动截图等

功能很强大、安装很小巧的免费截图、录屏工具,提供很多使用的工具来帮我么能解决问题,推荐给大家。关于ShareXShareX是一款免费的windows工具,起初是一个小巧的截图工具,经过多年的迭...

入门到精通系列PS教程:第13篇 · 拾色器、颜色问题说明及补充

入门到精通系列PS教程:第13篇·拾色器、颜色问题说明及补充作者|侯潇问题说明我的第12篇教程里,有个小问题没有说清楚。要说是错误,又不算是错误,只是没有说准确。写完那篇教程后,因为已经到了深...

PS冷知识:用吸管工具吸取屏幕上的任意颜色

今天,我们给大家介绍PS中的一个冷知识:用吸管工具可以吸取屏幕上的任意颜色。其实,操作起来是非常简单的。大多数情况下,我们认为,PS的吸管工具只能吸取PS软件作图区域范围内的颜色,最多加上画布四周的...

Windows 11 将提供内置颜色选择器工具

Windows11内置了颜色选择器,可以扫描并识别屏幕上的颜色并生成颜色代码。此外,微软还利用人工智能技术,让屏幕上的文本扫描和选择变得更加便捷。这两项功能均已在SnippingToolv1...