文档章节

Spring源码解析(六)——实例创建(上)

MarvelCode
 MarvelCode
发布于 06/19 19:56
字数 3014
阅读 17
收藏 1

前言

    经过前期所有的准备工作,Spring已经获取到需要创建实例的 beanName 和对应创建所需要信息 BeanDefinition,接下来就是实例创建的过程,由于该过程涉及到大量源码,所以将分为多个章节进行解读,这里面同样隐藏了 Bean 生命周期相关接口的执行。

 

源码解读

    首先来看看实例创建的源头。

public abstract class AbstractApplicationContext extends DefaultResourceLoader
        implements ConfigurableApplicationContext, DisposableBean {

    protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
        ......// 省略
        // 源头在这里
        beanFactory.preInstantiateSingletons();
    }
}

    源头就是“容器刷新”的最后一步了,展开来看这个方法,由 DefaultListableBeanFactory 实现。

public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
        implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {

    @Override
    public void preInstantiateSingletons() throws BeansException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Pre-instantiating singletons in " + this);
        }

        // 这里就是容器创建时候收集的所有 beanName
        // 见本类方法:registerBeanDefinition
        List<String> beanNames = new ArrayList<String>(this.beanDefinitionNames);

        // 遍历触发所有非惰性单例 bean的初始化...
        for (String beanName : beanNames) {
            // RootBeanDefinition可以看作是描述 bean的统一视图
            RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
            // 非抽象 & 单例 & 非惰性
            if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
                /**
                 * 是否为 FactoryBean类型:介绍下这个接口的方法
                 * - getObject:返回由FactoryBean创建的bean实例
                 * - getObjectType:返回 bean类型
                 * - isSingleton:判断是否为单例
                 */
                if (isFactoryBean(beanName)) {
                    final FactoryBean<?> factory = (FactoryBean<?>) getBean(FACTORY_BEAN_PREFIX + beanName);
                    boolean isEagerInit;
                    if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
                        isEagerInit = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
                            @Override
                            public Boolean run() {
                                return ((SmartFactoryBean<?>) factory).isEagerInit();
                            }
                        }, getAccessControlContext());
                    } else {
                        isEagerInit = (factory instanceof SmartFactoryBean &&
                                ((SmartFactoryBean<?>) factory).isEagerInit());
                    }
                    // 实现了 SmartFactoryBean可以指定是否直接初始化 
                    if (isEagerInit) {
                        // 1.关注此方法
                        getBean(beanName);
                    }
                } else {
                    // 1.关注此方法
                    getBean(beanName);
                }
            }
        }

        // 回调 SmartInitializingSingleton.afterSingletonsInstantiated
        // 仅限单例,在实例化后提供一些自定义的初始化操作
        for (String beanName : beanNames) {

            // 2.关注此方法
            Object singletonInstance = getSingleton(beanName);
            if (singletonInstance instanceof SmartInitializingSingleton) {
                final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
                if (System.getSecurityManager() != null) {
                    AccessController.doPrivileged(new PrivilegedAction<Object>() {
                        @Override
                        public Object run() {
                            smartSingleton.afterSingletonsInstantiated();
                            return null;
                        }
                    }, getAccessControlContext());
                } else {
                    smartSingleton.afterSingletonsInstantiated();
                }
            }
        }
    }
}

    这里遍历的就是容器创建时所扫描的所有需要创建的 beanName,然后根据之前封装的 GenericBeanDefinition 来创建 RootBeanDifinition,其中 GenericBeanDefinition 的封装见 默认标签解析 的<bean>解析封装,beanName 与 GenericBeanDefinition  映射关系注册见 默认标签解析 的<bean>注册操作

    接下来,重点分析 getBean 方法,这个方法内同样用到了 getSingleton,我们逐个来看。

 

getBean

public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {

    /**
     * 已实例化的单例缓存
     */
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

    /**
     * 与singletonObjects的不同之处,在实例创建时,就可以通过 getBean获取到,解决循环依赖
     */
    private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

    /**
     * 保存BeanName和创建bean的工厂之间的关系
     */
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

    /**
     * 放置正在被创建的单例
     */
    private final Set<String> singletonsCurrentlyInCreation =
            Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(16));

    /**
     * 至少创建一次的实例集合
     */
    private final Set<String> alreadyCreated =
            Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(256));

    @Override
    public Object getBean(String name) throws BeansException {
        return doGetBean(name, null, null, false);
    }

    /**
     * @param name         指定的 bean名称
     * @param requiredType bean 对应的 Class类型
     * @param args         通过构造器/工厂方法传入特定参数
     * @return 创建好的实例
     */
    protected <T> T doGetBean(final String name, final Class<T> requiredType,
                              final Object[] args, boolean typeCheckOnly) throws BeansException {

        // 别名转换:转为规范的 beanName(别名对应的真正 name)
        final String beanName = transformedBeanName(name);
        Object bean;

        // 尝试从手动注册的单例缓存中获取
        // 优先级( singletonObjects -> earlySingletonObjects )
        Object sharedInstance = getSingleton(beanName);
        if (sharedInstance != null && args == null) {
            ......// 省略日志

            // 这个方法下面会展开讲解,目前仅需要知道这个会返回实例即可
            bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
        }
        // 如果缓存中没有就走下面的分支
        else {
            // 循环依赖的处理:通过 ThreadLocal实现(维护了正在创建的 beanName)
            // 判断原理就是判断 beanName是否存在于指定的 ThreadLocal内
            if (isPrototypeCurrentlyInCreation(beanName)) {
                // 这里的检查只针对非 Singleton作用域
                // 因为单例有自身循环依赖的解决方案,见 getSingleton
                throw new BeanCurrentlyInCreationException(beanName);
            }

            BeanFactory parentBeanFactory = getParentBeanFactory();
            // 如果自身容器的 beanDefinitionMap不包含该 beanName,从父容器中查找.
            if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
                String nameToLookup = originalBeanName(name);
                if (args != null) {
                    parentBeanFactory.getBean(nameToLookup, args);
                } else {
                    return parentBeanFactory.getBean(nameToLookup, requiredType);
                }
            }

            // 默认的 typeCheckOnly为 false,即不需要类型检查
            if (!typeCheckOnly) {
                // 标识该 beanName正在被创建(alreadyCreated存储)
                markBeanAsCreated(beanName);
            }

            try {
                // 获取统一视图:包含了所有的创建所需信息
                final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
                // 校验如果是抽象类会抛出异常
                checkMergedBeanDefinition(mbd, beanName, args);

                // 是否指定了“depend-on”,如果有,则循环注册
                String[] dependsOn = mbd.getDependsOn();
                if (dependsOn != null) {
                    for (String dependsOnBean : dependsOn) {
                        // 检查是否有循环依赖
                        if (isDependent(beanName, dependsOnBean)) {
                            //....省略抛异常
                        }
                        // 这一步存储了依赖映射 Map<String, Set<String>>
                        // key-beanName,value-dependsOnBean集合
                        registerDependentBean(dependsOnBean, beanName);
                        // 然后创建 depend-on指定的 bean
                        getBean(dependsOnBean);
                    }
                }

                // 解决依赖 bean创建后,开始 bean实例真正的创建
                if (mbd.isSingleton()) {

                    // 单例:共享,通过 ObjectFactory创建
                    sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
                        @Override
                        public Object getObject() throws BeansException {
                            try {
                                // 通过 createBean创建实例,下节讲解
                                return createBean(beanName, mbd, args);
                            } catch (BeansException ex) {
                                destroySingleton(beanName);
                                throw ex;
                            }
                        }
                    });
                    // 这个方法下面会展开讲解,目前仅需要知道这个会返回实例即可
                    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
                } else if (mbd.isPrototype()) {
                    // 原型:每次都创建
                    Object prototypeInstance = null;
                    try {
                        // ThreadLocal赋值标识该 bean正在被创建
                        // 见上述 isPrototypeCurrentlyInCreation方法
                        beforePrototypeCreation(beanName);

                        // 通过 createBean创建实例,下节讲解
                        prototypeInstance = createBean(beanName, mbd, args);
                    } finally {
                        // 移除 ThreadLocal中该 beanName
                        afterPrototypeCreation(beanName);
                    }

                    // 这个方法下面会展开讲解,目前仅需要知道这个会返回实例即可
                    bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
                } else {
                    // 其他作用域处理,获取作用域名,转化成 Scope
                    String scopeName = mbd.getScope();
                    final Scope scope = this.scopes.get(scopeName);
                    if (scope == null) {
                        throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
                    }
                    try {
                        // 各作用域:共享,通过 ObjectFactory创建
                        Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() {
                            @Override
                            public Object getObject() throws BeansException {
                                // ThreadLocal赋值标识该 bean正在被创建
                                beforePrototypeCreation(beanName);
                                try {
                                    // 通过 createBean创建实例,下节讲解
                                    return createBean(beanName, mbd, args);
                                } finally {
                                    // 移除 ThreadLocal中该 beanName
                                    afterPrototypeCreation(beanName);
                                }
                            }
                        });

                        // 这个方法下面会展开讲解,目前仅需要知道这个会返回实例即可
                        bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
                    } //...catch操作
                }
            } //...catch操作
            ......
        }

        // 如果传入了requiredType,则进行转型工作
        if (requiredType != null && bean != null && !requiredType.isInstance(bean)) {
            try {
                return getTypeConverter().convertIfNecessary(bean, requiredType);
            } catch (TypeMismatchException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Failed to convert bean '" + name + "' to required type '" +
                            ClassUtils.getQualifiedName(requiredType) + "'", ex);
                }
                throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
            }
        }
        return (T) bean;
    }
}

    getBean 会调用 doGetBean 来进行实例创建,这种方法的命名在 Spring 到处可见,很多 xxx 的接口会将真正处理代码传给 doXxx 方法。我们来梳理下上面做了哪些事情:

  • beanName转换:首先会将传入的 name 转化为规范的 beanName,比如我们传入别名,Spring会根据映射关系将别名转为 bean最原始的 beanName;
  • 单例缓存:见 getSingleton 方法讲解,因为单例全局唯一,所以创建一次后就缓存起来,防止重复创建,一旦在缓存中找到就直接返回了;
  • 非单例循环依赖检测:通过将正创建的 beanName 放置于 ThreadLocal 中(见 beforePrototypeCreation),在创建结束后会将 ThreadLocal 中对应 beanName 移除(见 afterPrototypeCreation), 如果创建期间发现该 beanName 还在被创建,说明该实例依赖的其他实例又依赖了自己,会抛异常;
  • 父容器获取:触发父容器获取的条件是,自身容器中没有注册对应 beanName 的 BeanDefinition,所以优先级是“子容器->父容器”;
  • depends-on处理:有些实例的创建,需要依赖其他实例先创建,所有需要在创建自身前先将这些实例创建(递归调用 getBean);
  • 创建自身:有三个分支,SingletonPrototype、其他Scope,都是调用 createBean 来创建实例,这个方法会在下节解读。
  • 类型转换:这一步根据调用 getBean 时是否传入 requireType 来进行类型转换的。

    分析完主要的逻辑,我们来接着看上面代码中出现频次比较多的两个方法 “ getSingleton ” 和 “ getObjectForBeanInstance ”。

 

getSingleton

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

    // 保存 beanName和创建该 bean工厂之间的关系
    // 如果都没有,会使用工厂创建,创建后放入 earlySingletonObjects
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

    // 提前曝光,为了解决循环依赖问题,在属性注入前放置
    private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);


    // 2.方法源码
    @Override
    public Object getSingleton(String beanName) {
        return getSingleton(beanName, true);
    }

    /**
     * @param beanName            指定的 bean规范名称
     * @param allowEarlyReference 是否创建早期引用(用于解决循环引用)
     * @return 注册的单例,如果没找到返回 null
     */
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        Object singletonObject = this.singletonObjects.get(beanName);
        // 判断该 bean是否在创建过程中
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            // 防并发,双重锁
            synchronized (this.singletonObjects) {
                // 缓存获取实例(还未创建完毕,提前曝光)
                singletonObject = this.earlySingletonObjects.get(beanName);
                if (singletonObject == null && allowEarlyReference) {
                    // 缓存获取工厂(用于调用 getObject获取实例)
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                        singletonObject = singletonFactory.getObject();
                        // earlySingletonObjects用于解决循环引用
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        // 移除 singletonFactories,说明 bean完全创建好
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }

    public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(beanName, "'beanName' must not be null");
        // 双重锁,当缓存中不存在才可以进行单例 bean的初始化
        synchronized (this.singletonObjects) {
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {

                // singletonsCurrentlyInDestruction:标识目前容器是否在销毁 Singletons
                // 容器关闭时、或容器启动异常时会置为 true
                if (this.singletonsCurrentlyInDestruction) {
                    ....// 省略抛异常
                }
                if (logger.isDebugEnabled()) {
                    logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
                }
                // 记录加载状态:该 bean正在被创建
                beforeSingletonCreation(beanName);
                boolean newSingleton = false;
                boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = new LinkedHashSet<Exception>();
                }
                try {
                    // 调用 ObjectFactory.getObject() 创建
                    singletonObject = singletonFactory.getObject();
                    newSingleton = true;
                } ...// 省略 catch处理
                finally{
                    if (recordSuppressedExceptions) {
                        this.suppressedExceptions = null;
                    }
                    // 移除加载状态: bean已被创建完毕(对应 beforeSingletonCreation)
                    afterSingletonCreation(beanName);
                }
                if (newSingleton) {
                    // 将 bean放入缓存
                    addSingleton(beanName, singletonObject);
                }
            }
            return (singletonObject != NULL_OBJECT ? singletonObject : null);
        }
    }

    protected void addSingleton(String beanName, Object singletonObject) {
        synchronized (this.singletonObjects) {
            // 将已创建的实例放入缓存 singletonObjects
            this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
            // 移除 singletonFactories和 earlySingletonObjects,说明 bean完全创建好
            this.singletonFactories.remove(beanName);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }

}

    这个方法有3个重载版本,逻辑不复杂,维护了几个缓存关系,还有就是 “提前曝光” 的逻辑。这里调用 ObjectFactory.getObject 获取实例,其实会回调 doGetBean 中匿名内部类实现:createBean,下章讲解。

 

getObjectForBeanInstance

public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
    
    protected Object getObjectForBeanInstance(
            Object beanInstance, String name, String beanName, RootBeanDefinition mbd) {

        // name以"&"开头,但却不是 FactoryBean类型,抛出异常
		if (BeanFactoryUtils.isFactoryDereference(name) && !(beanInstance instanceof FactoryBean)) {
			throw new BeanIsNotAFactoryException(transformedBeanName(name), beanInstance.getClass());
		}
        // name不以"&"开头,也不是 FactoryBean类型,直接返回
		if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) {
			return beanInstance;
		}

        Object object = null;
        if (mbd == null) {
            // 尝试从缓存中(factoryBeanObjectCache)获取
            object = getCachedObjectForFactoryBean(beanName);
        }
        if (object == null) {
            // 到这里的就只有 FactoryBean类型了,强转
            FactoryBean<?> factory = (FactoryBean<?>) beanInstance;
            // 获取统一视图,一般都是传入的,即 mbd != null
            if (mbd == null && containsBeanDefinition(beanName)) {
                mbd = getMergedLocalBeanDefinition(beanName);
            }
            // 从 FactoryBean中获取实例
            object = getObjectFromFactoryBean(factory, beanName, !synthetic);
        }
        return object;
    }

    protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
        // 单例且缓存(singletonObjects)有此实例
        if (factory.isSingleton() && containsSingleton(beanName)) {
            // getSingletonMutex返回的是 singletonObjects
            synchronized (getSingletonMutex()) {
                // 缓存中(factoryBeanObjectCache)有此实例
                Object object = this.factoryBeanObjectCache.get(beanName);
                if (object == null) {
                    // 调用 FactoryBean.getObject()
                    object = doGetObjectFromFactoryBean(factory, beanName);
                    // 锁的是 singletonObjects,factoryBeanObjectCache可能被其他线程并发放入
                    Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
                    if (alreadyThere != null) {
                        object = alreadyThere;
                    } else {
                        if (object != null && shouldPostProcess) {
                            try {
                                // 这一步由子类 AbstractAutowireCapableBeanFactory实现
                                // bean生命周期:BeanPostProcessor.postProcessAfterInitialization
                                object = postProcessObjectFromFactoryBean(object, beanName);
                            } ...// 省略 catch
                        }
                        // 只有单例才会放入缓存中(factoryBeanObjectCache)
                        this.factoryBeanObjectCache.put(beanName, (object != null ? object : NULL_OBJECT));
                    }
                }
                return (object != NULL_OBJECT ? object : null);
            }
        } else {
            // 调用 FactoryBean.getObject()
            Object object = doGetObjectFromFactoryBean(factory, beanName);
            if (object != null && shouldPostProcess) {
                try {
                    // 这一步由子类 AbstractAutowireCapableBeanFactory实现
                    // bean生命周期:BeanPostProcessor.postProcessAfterInitialization
                    object = postProcessObjectFromFactoryBean(object, beanName);
                } ...// 省略catch
            }
            return object;
        }
    }

    private Object doGetObjectFromFactoryBean(final FactoryBean<?> factory, final String beanName)
            throws BeanCreationException {

        Object object;
        try {
            if (System.getSecurityManager() != null) {
                AccessControlContext acc = getAccessControlContext();
                try {
                    object = AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
                        @Override
                        public Object run() throws Exception {
                            // 调用 FactoryBean.getObject
                            return factory.getObject();
                        }
                    }, acc);
                } catch (PrivilegedActionException pae) {
                    throw pae.getException();
                }
            } else {
                // 调用 FactoryBean.getObject
                object = factory.getObject();
            }
        } ...// 省略 catch处理

        // 实例为空处理
        if (object == null && isSingletonCurrentlyInCreation(beanName)) {
            throw new BeanCurrentlyInCreationException(
                    beanName, "FactoryBean which is currently in creation returned null from getObject");
        }
        return object;
    }

}

    这个方法主要值针对 FactoryBean 这种类型的处理,通过回调 FactoryBean.getObject 来获取实例。

    FactoryBean:通过 beanName 获取的是 FactoryBean.getObject,在 beanName 前面添加 “&” 符号获取 FactoryBean 本身。

    ObjectFactory:就是一个简单的实例工厂,通过 beanName 获取对应创建出的实例。

 

总结

    通过上面的源码可以看出,Spring 创建实例的时候用到了很多缓存映射,通过这些避免了重复的创建,节省了时间。接下来就是 createBean 具体的创建逻辑了。

© 著作权归作者所有

共有 人打赏支持
MarvelCode
粉丝 47
博文 29
码字总数 61421
作品 0
南京
程序员
SpringCloud Eureka 源码解析 —— 应用实例注册发现(六)之全量获取

SpringCloud Eureka 源码解析 —— 应用实例注册发现(六)之全量获取 Harries Blog™2017-12-311 阅读 ACESpringAppcacheAPIbuildAtombug 摘要: 原创出处 http ://www. ioc oder.cn/ Eureka...

Harries Blog™
2017/12/31
0
0
SpringMVC源码解析(一)——初始化

前言 本系列文章顺延“Spring源码解析”,是在“父容器”创建完成后,对“子容器”(SpringMVC)创建,以及请求处理的解析。 源码解读 说起 SpringMVC,DispatcherServlet 应该是最熟悉的类之...

MarvelCode
06/26
0
0
Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密...

小致dad
08/03
0
0
spring源码-bean之初始化-1

  一、spring的IOC控制反转:控制反转——Spring通过一种称作控制反转(IOC)的技术促进了松耦合。当应用了IOC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建...

小不点丶
08/09
0
0
Spring源码解析系列之IOC容器(一)

前言 实际上我所有的博客都是原来对原来印象笔记里笔记内容的加工,关于Spring源码自己已经解析了很多遍,但是时间长总是忘记,写一篇博客权当加强记忆,也算再次学习下大师们的设计思想,思...

后厂村老司机
06/02
0
0

没有更多内容

加载失败,请刷新页面

加载更多

web打印控件 LODOP的详细api

web打印控件 LODOP的详细api

wangxujun59
25分钟前
1
0
从一次小哥哥与小姐姐的转账开始, 浅谈分布式事务从理论到实践

分布式事务是个业界难题,在看分布式事务方案之前,先从单机数据库事务开始看起。 什么是事务 事务(Transaction)是数据库系统中一系列操作的一个逻辑单元,所有操作要么全部成功要么全部失...

中间件小哥
28分钟前
5
0
荣登Github日榜!微信最新开源MMKV

MMKV 开源当日即登Github Trending日榜,三日后荣登周榜。MMKV 在腾讯内部开源半年,得到公司内部团队的广泛应用和一致好评。 MMKV 是基于 mmap 内存映射的移动端通用 key-value 组件,底层序...

腾讯开源
37分钟前
2
0
前端取色工具:jcpicker

http://annystudio.com/software/colorpicker/#jcp-download

轻量级赤影
39分钟前
1
0
Swift - 将图片保存到相册

import Photos func loadImage(image:UIImage) { UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveImage(image:didFinishSavingWithError:contextInfo:)), ni......

west_zll
45分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部