現在的編程和二十年前有什麼不同?

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

@Autowired 註解詳解

前言

我們平時使用 Spring 時,想要 依賴注入 時使用最多的是 @Autowired 註解了,本文主要講解 Spring 是如何處理該註解並實現 依賴注入 的功能的。

正文

首先我們看一個測試用例:

User 實體類:

public class User {

    private Long id;
    private String name;

	// 省略 get 和 set 方法
}

測試類:

public class AnnotationDependencyInjectTest {

    /**
     * @Autowired 字段注入
     */
    @Autowired
    private User user;

    private City city;

    /**
     * @Autowired 方法注入
     */
    @Autowired
    public void initCity(City city) {
        this.city = city;
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(AnnotationDependencyInjectTest.class);
        context.refresh();
        AnnotationDependencyInjectTest bean = context.getBean(AnnotationDependencyInjectTest.class);
        // @Autowired 字段注入
        System.out.println(bean.user);
        // @Autowired 方法注入
        System.out.println(bean.city);
        UserHolder userHolder = context.getBean(UserHolder.class);
        // @Autowired 構造器注入
        System.out.println(userHolder.getUser());
        context.close();
    }

    @Bean
    public User user() {
        User user = new User();
        user.setId(1L);
        user.setName("leisurexi");
        return user;
    }

    @Bean
    public City city() {
        City city = new City();
        city.setId(1L);
        city.setName("北京");
        return city;
    }
    
    /**
     * @Autowired 構造函數注入
     */
    static class UserHolder {

        private User user;

        @Autowired
        public UserHolder(User user) {
            this.user = user;
        }

        public User getUser() {
            return user;
        }

        public void setUser(User user) {
            this.user = user;
        }
    }

}

上面分別展示了 @Autowired 註解的字段注入和方法注入,下面我們開始分析 Spring 是如何實現的。

首先使 @Autowired 註解生效的一個關鍵類是 AutowiredAnnotationBeanPostProcessor,該類實現了 InstantiationAwareBeanPostProcessorAdapter 抽象類;該抽象類就是一個適配器的作用提供了接口方法的默認實現,InstantiationAwareBeanPostProcessorAdapter 又實現了 SmartInstantiationAwareBeanPostProcessor 接口,同時實現該接口的 determineCandidateConstructors() 方法可以指定 bean 的候選構造函數;然後 SmartInstantiationAwareBeanPostProcessor 接口又繼承了 InstantiationAwareBeanPostProcessor 接口,該接口提供了 bean 實例化前後的生命周期回調以及屬性賦值前的後置處理方法,@Autowired 註解的屬性注入就是通過重寫該接口的 postProcessProperties() 實現的。這兩個接口都在 在 Spring IoC createBean 方法詳解 一文中有介紹過。下面我們看一下 AutowiredAnnotationBeanProcessor 的繼承關係圖:

關於 AutowiredAnnotationBeanPostProcessor 這個後置處理器是怎麼加入到 beanFactory 中的,我們在 Spring IoC component-scan 節點詳解 一文中介紹過主要是通過 AnnotationConfigUtils#registerAnnotationConfigProcessors() 實現的。

public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
    BeanDefinitionRegistry registry, @Nullable Object source) {
    
    // 忽略其它代碼
    
    // 註冊用於處理@Autowired、@Value、@Inject註解的後置處理器
    if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
        RootBeanDefinition def = new
            RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
        def.setSource(source);
        beanDefs.add(registerPostProcessor(registry, def,
                                           AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
    }
    // 忽略其它代碼
    
}

屬性和方法注入

AutowiredAnnotationBeanPostProcessor 中跟屬性注入有關的方法有兩個:postProcessMergedBeanDefinitionpostProcessPropertyValues

前者是 MergedBeanDefinitionPostProcessor 接口中的方法,定義如下:

public interface MergedBeanDefinitionPostProcessor extends BeanPostProcessor {

	/**
	 * 對指定bean的BeanDefinition合併后的處理方法回調
	 */
	void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName);

	/**
	 * @since 5.1
	 * 通知已重新設置指定beanName的BeanDefinition,如果實現該方法應該清除受影響的bean的所有元數據
	 */
	default void resetBeanDefinition(String beanName) {
	}

}

後者是 InstantiationAwareBeanPostProcessor 接口中的方法,定義如下:

public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {

	/**
	 * Bean 實例化前調用,返回非 {@code null} IoC 容器不會對 Bean 進行實例化 並且後續的生命周期回調方	  *	法不會調用,返回 {@code null} 則進行 IoC 容器對 Bean 的實例化
	 */
	@Nullable
	default Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
		return null;
	}

	/**
	 * Bean 實例化之後,屬性填充之前調用,返回 {@code true} 則進行默認的屬性填充步驟,返回 {@code 		 * false} 會跳過屬性填充階段。
	 */
	default boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
		return true;
	}

	/**
	 * Bean 實例化后屬性賦值前調用,PropertyValues 是已經封裝好的設置的屬性值,返回 {@code null} 繼續
	 * 使用現有屬性,否則會替換 PropertyValues。
	 * @since 5.1版本新加的和底下的方法一樣
	 */
	@Nullable
	default PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName)
			throws BeansException {
		return null;
	}

	/**
	 * 跟上面方法一樣的功能,只不過是5.1以前版本所使用的
	 * 返回 {@code null} 會跳過屬性填充階段
	 */
	@Deprecated
	@Nullable
	default PropertyValues postProcessPropertyValues(
			PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {

		return pvs;
	}

}

關於這兩個方法的調用時機,可以查看 Spring IoC createBean 方法詳解。

AutowiredAnnotationBeanPostProcessor#postProcessMergedBeanDefinition

首先執行的是 postProcessMergedBeanDefinition()

public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
    // 尋找需要注入的字段或方法,並封裝成 InjectionMetadata,見下文詳解
    InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
    // 檢查元數據中的註解信息
    metadata.checkConfigMembers(beanDefinition);
}

InjectionMetadata 就是注入的元信息描述,主要字段如下:

public class InjectionMetadata {
    
    // 需要依賴注入的目標類
    private final Class<?> targetClass;
    // 注入元素的集合
    private final Collection<InjectedElement> injectedElements;
    
    // 忽略其它代碼
    
}

InjectedElement 就是注入的元素,主要字段如下:

public abstract static class InjectedElement {
    
    // 注入的屬性或方法
    protected final Member member;
    // 需要注入的是否是字段
    protected final boolean isField;
    
}

AutowiredAnnotationBeanPostProcessor#findAutowiringMetadata

private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
    // Fall back to class name as cache key, for backwards compatibility with custom callers.
    String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
    // Quick check on the concurrent map first, with minimal locking.
    // 首先從緩存中獲取
    InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
    // 判斷是否需要刷新,即metadata為null或者metadata中存儲的targetClass和當前clazz不等
    if (InjectionMetadata.needsRefresh(metadata, clazz)) {
        // 這裏相當於是一個double check,防止多線程出現的併發問題
        synchronized (this.injectionMetadataCache) {
            metadata = this.injectionMetadataCache.get(cacheKey);
            if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                if (metadata != null) {
                    metadata.clear(pvs);
                }
                // 構建注入元信息,見下文詳解
                metadata = buildAutowiringMetadata(clazz);
                // 放入緩存中
                this.injectionMetadataCache.put(cacheKey, metadata);
            }
        }
    }
    // 返回注入元信息
    return metadata;
}

AutowiredAnnotationBeanPostProcessor#buildAutowiringMetadata

private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
    if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
        return InjectionMetadata.EMPTY;
    }
    // 判斷當前類或其字段或其方法是否標註了autowiredAnnotationTypes中的註解,沒有的話直接返回空的
    List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
    Class<?> targetClass = clazz;

    do {
        final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
        // 遍歷targetClass中的字段
        ReflectionUtils.doWithLocalFields(targetClass, field -> {
            // 獲取field上的@Autowired註解信息
            MergedAnnotation<?> ann = findAutowiredAnnotation(field);
            if (ann != null) {
                // 如果字段是靜態類型是不會進行注入的
                if (Modifier.isStatic(field.getModifiers())) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation is not supported on static fields: " + field);
                    }
                    return;
                }
                // 獲取@Autowired註解中的required屬性
                boolean required = determineRequiredStatus(ann);
                // 將裝成AutowiredFieldElement添加進currElements
                currElements.add(new AutowiredFieldElement(field, required));
            }
        });
        // 遍歷targetClass中的方法
        ReflectionUtils.doWithLocalMethods(targetClass, method -> {
            // 找到橋接方法
            Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
            // 判斷方法的可見性,如果不可見則直接返回
            if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                return;
            }
            // 獲取method上的@Autowired註解信息
            MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
            if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                // 如果是靜態方法是不會進行注入的
                if (Modifier.isStatic(method.getModifiers())) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation is not supported on static methods: " + method);
                    }
                    return;
                }
                // 方法注入沒有參數就違背了初衷,就是在脫褲子放屁
                if (method.getParameterCount() == 0) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Autowired annotation should only be used on methods with parameters: " + method);
                    }
                }
                // 獲取@Autowired註解中的required屬性
                boolean required = determineRequiredStatus(ann);
                // 將方法和目標類型封裝成屬性描述符
                PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
                // 封裝成AutowiredMethodElement添加進currElements
                currElements.add(new AutowiredMethodElement(method, required, pd));
            }
        });
        // 將currElements整個添加進elements
        elements.addAll(0, currElements);
        // 獲取targetClass的父類,進行下一次循環
        targetClass = targetClass.getSuperclass();
    }
    // 當targetClass為空或者targetClass等於Object.class時會退出循環
    while (targetClass != null && targetClass != Object.class);
    // 將elements和clazz封裝成InjectionMetadata返回
    return InjectionMetadata.forElements(elements, clazz);
}

上面代碼中的 findAutowiredAnnotation() 就是在遍歷 autowiredAnnotationTypes 屬性,看字段或者方法上的註解是否存在於 autowiredAnnotationTypes 中,或者其派生註解,找到第一個就返回,不會再繼續遍歷了。

public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
		implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
    
    private final Set<Class<? extends Annotation>> autowiredAnnotationTypes = new LinkedHashSet<>(4);
    
    public AutowiredAnnotationBeanPostProcessor() {
        this.autowiredAnnotationTypes.add(Autowired.class);
        this.autowiredAnnotationTypes.add(Value.class);
        try {
            this.autowiredAnnotationTypes.add((Class<? extends Annotation>)
                                              ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader()));
            logger.trace("JSR-330 'javax.inject.Inject' annotation found and supported for autowiring");
        }
        catch (ClassNotFoundException ex) {
            // JSR-330 API not available - simply skip.
        }
	}
    
}

AutowiredAnnotationBeanPostProcessor 類的構造函數中,我們可以發現 autowiredAnnotationTypes 默認添加了 @Autowired@Value 以及 @Inject (在 JSR-330 的jar包存在於當前環境時)。

至此,使用 @Autowired 修飾的字段和方法已經封裝成 InjectionMetadata 並放在 injectionMetadataCache 緩存中,便於後續使用。

AutowireAnnotationBeanPostProcessor#postProcessProperties

postProcessMergedBeanDefinition() 調用后 bean 就會進行實例化接着調用 postProcessProperties()

public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
    // 獲取緩存中的 InjectionMetadata
    InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
    try {
        // 進行屬性的注入
        metadata.inject(bean, beanName, pvs);
    }
    catch (BeanCreationException ex) {
        throw ex;
    }
    catch (Throwable ex) {
        throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
    }
    // 返回注入的屬性
    return pvs;
}

// InjectMetadata.java
public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 獲取檢查后的元素
    Collection<InjectedElement> checkedElements = this.checkedElements;
    // 如果checkedElements不為空就使用checkedElements,否則使用injectedElements
    Collection<InjectedElement> elementsToIterate =
        (checkedElements != null ? checkedElements : this.injectedElements);
    if (!elementsToIterate.isEmpty()) {
        // 遍歷elementsToIterate
        for (InjectedElement element : elementsToIterate) {
            if (logger.isTraceEnabled()) {
                logger.trace("Processing injected element of bean '" + beanName + "': " + element);
            }
            // AutowiredFieldElement、AutowiredMethodElement這兩個類繼承InjectionMetadata.InjectedElement,各自重寫了inject方法
            element.inject(target, beanName, pvs);
        }
    }
}

AutowiredFieldElement#inject

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 強轉成Field類型
    Field field = (Field) this.member;
    Object value;
    if (this.cached) {
        // 如果緩存過,直接使用緩存的值,一般第一次注入都是false
        value = resolvedCachedArgument(beanName, this.cachedFieldValue);
    }
    else {
        // 構建依賴描述符
        DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
        desc.setContainingClass(bean.getClass());
        Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
        Assert.state(beanFactory != null, "No BeanFactory available");
        // 獲取類型轉換器
        TypeConverter typeConverter = beanFactory.getTypeConverter();
        try {
            // 進行依賴解決,獲取符合條件的bean
            value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
        }
        catch (BeansException ex) {
            throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
        }
        // 加鎖
        synchronized (this) {
            // 如果沒有被緩存
            if (!this.cached) {
                // 找到了需要的bean || 該字段是必要的
                if (value != null || this.required) {
                    // 將依賴描述符賦值給cachedFieldValue
                    this.cachedFieldValue = desc;
                    // 註冊bean的依賴關係,用於檢測是否循環依賴
                    registerDependentBeans(beanName, autowiredBeanNames);
                    // 如果符合條件的bean只有一個
                    if (autowiredBeanNames.size() == 1) {
                        String autowiredBeanName = autowiredBeanNames.iterator().next();
                        // beanFactory含有名為autowiredBeanName的bean && 類型是匹配的
                        if (beanFactory.containsBean(autowiredBeanName) &&
                            beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
                            // 將該屬性解析到的bean的信息封裝成ShortcutDependencyDescriptor
                            // 之後可以通過調用resolveShortcut()來間接調beanFactory.getBean()快速獲取bean
                            this.cachedFieldValue = new ShortcutDependencyDescriptor(
                                desc, autowiredBeanName, field.getType());
                        }
                    }
                }
                else {
                    this.cachedFieldValue = null;
                }
                // 緩存標識設置為true
                this.cached = true;
            }
        }
    }
    // 如果找到了符合的bean,設置字段可訪問,利用反射設置值
    if (value != null) {
        ReflectionUtils.makeAccessible(field);
        field.set(bean, value);
    }
}

上面代碼中的 beanFactory.resolveDependency() 在 Spring IoC createBean 方法詳解 一文中有介紹過,這裏不再贅述;同樣 registerDependentBeans() 最終會調用 DefaultSingletonBeanRegistry.registerDependentBean() ,該方法在 Spring IoC getBean 方法詳解 一文中有介紹過,這裏也不再贅述。

AutowiredMethodElement#inject

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 檢查是否需要跳過
    if (checkPropertySkipping(pvs)) {
        return;
    }
    // 強轉成Method類型
    Method method = (Method) this.member;
    Object[] arguments;
    if (this.cached) {
        // Shortcut for avoiding synchronization...
        // 如果緩存過,直接調用beanFactory.resolveDependency()返回符合的bean
        arguments = resolveCachedArguments(beanName);
    }
    else {
        // 獲取參數數量
        int argumentCount = method.getParameterCount();
        arguments = new Object[argumentCount];
        // 創建依賴描述符數組
        DependencyDescriptor[] descriptors = new DependencyDescriptor[argumentCount];
        // 記錄用於自動注入bean的名稱集合
        Set<String> autowiredBeans = new LinkedHashSet<>(argumentCount);
        Assert.state(beanFactory != null, "No BeanFactory available");
        // 獲取類型轉換器
        TypeConverter typeConverter = beanFactory.getTypeConverter();
        // 遍歷參數
        for (int i = 0; i < arguments.length; i++) {
            // 將方法和參數的下標構建成MethodParameter,這裏面主要記錄了參數的下標和類型
            MethodParameter methodParam = new MethodParameter(method, i);
            // 將MethodParameter構建成DependencyDescriptor
            DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required);
            currDesc.setContainingClass(bean.getClass());
            descriptors[i] = currDesc;
            try {
                // 進行依賴解決,找到符合條件的bean
                Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter);
                if (arg == null && !this.required) {
                    arguments = null;
                    break;
                }
                arguments[i] = arg;
            }
            catch (BeansException ex) {
                throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex);
            }
        }
        // 這裏跟字段注入差不多,就是註冊bean的依賴關係,並且緩存每個參數的ShortcutDependencyDescriptor
        synchronized (this) {
            if (!this.cached) {
                if (arguments != null) {
                    DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, arguments.length);
                    registerDependentBeans(beanName, autowiredBeans);
                    if (autowiredBeans.size() == argumentCount) {
                        Iterator<String> it = autowiredBeans.iterator();
                        Class<?>[] paramTypes = method.getParameterTypes();
                        for (int i = 0; i < paramTypes.length; i++) {
                            String autowiredBeanName = it.next();
                            if (beanFactory.containsBean(autowiredBeanName) &&
                                beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) {
                                cachedMethodArguments[i] = new ShortcutDependencyDescriptor(descriptors[i], autowiredBeanName, paramTypes[i]);
                            }
                        }
                    }
                    this.cachedMethodArguments = cachedMethodArguments;
                }
                else {
                    this.cachedMethodArguments = null;
                }
                this.cached = true;
            }
        }
    }
    // 找到了符合條件的bean
    if (arguments != null) {
        try {
            // 設置方法可訪問,利用反射進行方法調用,傳入參數
            ReflectionUtils.makeAccessible(method);
            method.invoke(bean, arguments);
        }
        catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }
}

構造器注入

構造器注入就是通過調用 determineCandidateConstructors() 來返回合適的構造器。

public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName) throws BeanCreationException {

    // Quick check on the concurrent map first, with minimal locking.
    // 首先從緩存中獲取
    Constructor<?>[] candidateConstructors = this.candidateConstructorsCache.get(beanClass);
    // 緩存為空
    if (candidateConstructors == null) {
        // Fully synchronized resolution now...
        // 這裏相當於double check
        synchronized (this.candidateConstructorsCache) {
            candidateConstructors = this.candidateConstructorsCache.get(beanClass);
            if (candidateConstructors == null) {
                Constructor<?>[] rawCandidates;
                try {
                    // 獲取beanClass的所有構造函數
                    rawCandidates = beanClass.getDeclaredConstructors();
                }
                catch (Throwable ex) {
                    throw new BeanCreationException(beanName, "Resolution of declared constructors on bean Class [" + beanClass.getName() +"] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex);
                }
                // 存放標註了@Autowired註解的構造器
                List<Constructor<?>> candidates = new ArrayList<>(rawCandidates.length);
                // 存放標註了@Autowired註解,並且required為true的構造器
                Constructor<?> requiredConstructor = null;
                Constructor<?> defaultConstructor = null;
                for (Constructor<?> candidate : rawCandidates) {
                    // 獲取構造器上的@Autowired註解信息
                    MergedAnnotation<?> ann = findAutowiredAnnotation(candidate);
                    if (ann == null) {
                        // 如果沒有從候選者找到註解,則嘗試解析beanClass的原始類(針對CGLIB代理)
                        Class<?> userClass = ClassUtils.getUserClass(beanClass);
                        if (userClass != beanClass) {
                            try {
                                Constructor<?> superCtor =
userClass.getDeclaredConstructor(candidate.getParameterTypes());
                                ann = findAutowiredAnnotation(superCtor);
                            }
                            catch (NoSuchMethodException ex) {
                                // Simply proceed, no equivalent superclass constructor found...
                            }
                        }
                    }
                    if (ann != null) {
                        // 如果requiredConstructor不為空,代表有多個標註了@Autowired且required為true的構造器,此時Spring不知道選擇哪個拋出異常
                        if (requiredConstructor != null) {
                            throw new BeanCreationException(beanName, "Invalid autowire-marked constructor: " + candidate +". Found constructor with 'required' Autowired annotation already: " + requiredConstructor);
                        }
                        // 獲取@Autowired註解的reuired屬性的值
                        boolean required = determineRequiredStatus(ann);
                        if (required) {
                            // 如果當前候選者是@Autowired(required = true),則之前不能存在其他使用@Autowire註解的構造函數,否則拋異常
                            if (!candidates.isEmpty()) {
                                throw new BeanCreationException(beanName,"Invalid autowire-marked constructors: " + candidates +". Found constructor with 'required' Autowired annotation: " + candidate);
                            }
                            // required為true將當前構造器賦值給requiredConstructor
                            requiredConstructor = candidate;
                        }
                        // 將當前構造器加入進候選構造器中
                        candidates.add(candidate);
                    }
                    // 沒有標註了@Autowired註解且參數長度為0,賦值為默認構造器
                    else if (candidate.getParameterCount() == 0) {
                        defaultConstructor = candidate;
                    }
                }
                // 有標註了@Autowired註解的構造器
                if (!candidates.isEmpty()) {
                    // Add default constructor to list of optional constructors, as fallback.
                    // 沒有標註了@Autowired且required為true的構造器
                    if (requiredConstructor == null) {
                        // 默認構造器不為空
                        if (defaultConstructor != null) {
                            // 將默認構造器加入進候選構造器中
                            candidates.add(defaultConstructor);
                        }
                    }
                    // 將候選者賦值給candidateConstructors
                    candidateConstructors = candidates.toArray(new Constructor<?>[0]);
                }
                // 只有1個構造器 && 參數長度大於0(非默認構造器),只能用它當做候選者了
                else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {
                    candidateConstructors = new Constructor<?>[] {rawCandidates[0]};
                }
                // 只有1個構造器 && 參數長度大於0,只能用它當做候選者了
                else if (nonSyntheticConstructors == 2 && primaryConstructor != null &&
                         defaultConstructor != null && !primaryConstructor.equals(defaultConstructor)) {
                    candidateConstructors = new Constructor<?>[] {primaryConstructor, defaultConstructor};
                }
                else if (nonSyntheticConstructors == 1 && primaryConstructor != null) {
                    candidateConstructors = new Constructor<?>[] {primaryConstructor};
                }
                // 返回一個空的Constructor
                else {
                    candidateConstructors = new Constructor<?>[0];
                }
                // 緩存候選的構造器
                this.candidateConstructorsCache.put(beanClass, candidateConstructors);
            }
        }
    }
    // 如果候選構造器長度大於0,直接返回,否則返回null
    return (candidateConstructors.length > 0 ? candidateConstructors : null);
}

關於 SmartInstantiationAwareBeanPostProcessor 接口的調用時機,在 Spring IoC createBean 方法詳解 一文中有介紹過,這裏就不再贅述了。

總結

本文主要介紹了 Spring 對 @Autowired 註解的主要處理過程,結合前面的 Spring IoC getBean 方法詳解 和 Spring IoC createBean 方法詳解 文章一起看才能更好的理解。

最後,我模仿 Spring 寫了一個精簡版,代碼會持續更新。地址:https://github.com/leisurexi/tiny-spring。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

痞子衡嵌入式:鏈接函數到8字節對齊地址或可進一步提升i.MXRT1xxx內核執行性能

  大家好,我是痞子衡,是正經搞技術的痞子。今天痞子衡給大家分享的是i.MXRT上進一步提升代碼執行性能的經驗

  今天跟大家聊的這個話題還是跟痞子衡最近這段時間參与的一個基於i.MXRT1170的大項目有關,痞子衡在做其中的開機動畫功能,之前寫過一篇文章 《降低刷新率是定位LCD花屏显示問題的第一大法》 介紹了開機動畫功能的實現以及LCD显示注意事項,在此功能上,痞子衡想進一步測試從芯片上電到LCD屏显示第一幅完整圖像的時間,這個時間我們暫且稱為1st UI時間,該時間的長短對項目有重要意義。

  痞子衡分別測試了代碼在XIP執行下和在TCM里執行下的1st UI時間,得到的結果竟然是XIP執行比TCM執行還要快50ms,這是怎麼回事?這完全顛覆了我們的理解,i.MXRT上TCM是與內核同頻的,Flash速度遠低於TCM。如果是XIP執行,即使有I-Cache加速,也最多與TCM執行一樣快,怎麼可能做到比TCM執行快這麼多。於是痞子衡便開始深挖這個奇怪的現象,然後發現了進一步提升代碼執行性能的秘密。

一、引出計時差異問題

  痞子衡的開機動畫程序是基於 \SDK_2.x.x_MIMXRT1170-EVK\boards\evkmimxrt1170\jpeg_examples\sd_jpeg 例程的,只是去了SD卡和libjpeg庫相關代碼。工程有兩個build,一個是TCM里執行(即debug),另一個是XIP執行(即flexspi_nor_debug)。

  項目板上的Flash型號是MX25UW51345G,痞子衡將其配成Octal mode, DDR, 166MHz用於啟動。項目板上還有兩個LED燈,痞子衡在LED燈上飛了兩根線,連同POR引腳一起連上示波器,用於精確測量1st UI各部分時間組成。

  示波器通道1連接POR引腳,表明1st UI時間起點;通道2連接LED1 GPIO,表明ROM啟動時間(進入用戶APP的時間點);通道3連接LED2 GPIO,做兩次電平變化,分別是1st圖像幀開始和結束的時間點。翻轉LED GPIO代碼位置如下:

void light_led(uint32_t ledIdx, uint8_t ledVal);

void SystemInit (void) {
    // 將LED1置1,標示ROM啟動時間
    light_led(1, 1);

    SCB->CPACR |= ((3UL << 10*2) | (3UL << 11*2));

    // ...
}

void APP_InitDisplay(void)
{
    // ...

    g_dc.ops->enableLayer(&g_dc, 0);

    // 將LED2置1,標示1st圖像幀開始時間點
    light_led(2, 1);
}

int main(void)
{
    BOARD_ConfigMPU();
    BOARD_InitBootPins();
    BOARD_BootClockRUN();
    BOARD_ResetDisplayMix();

    APP_InitDisplay();

    while (1)
	{
	    // ...
	}
}

static void APP_BufferSwitchOffCallback(void *param, void *switchOffBuffer)
{
    s_newFrameShown = true;

    // 將LED2置0,標示1st圖像幀結束時間點
    light_led(2, 0);
}

  上圖是痞子衡抓到的波形(30Hz,XIP),痞子衡一共做了四次測試,分別是30Hz LCD刷新率下的XIP/TCM以及60Hz LCD刷新率下的XIP/TCM,結果如下錶所示。表中的Init Time一欄表示的是開機動畫程序代碼執行時間(從SystemInit()函數開始執行到APP_InitDisplay()函數結束的時間),可以看到TCM執行比XIP執行慢近50ms,這便是奇怪問題所在。

代碼位置 LCD刷新率 POR Time Boot Time Init Time Launch Time
XIP 30Hz 3.414ms 10.082ms 34.167ms + 153ms 32.358ms
TCM 30Hz 3.414ms 10.854ms 33.852ms + 203ms 32.384ms
XIP 60Hz 3.414ms 9.972ms 18.142ms + 153ms 16.166ms
TCM 60Hz 3.414ms 10.92ms 17.92ms + 203ms 16.104ms

二、定位計時差異問題

  對於開機動畫代碼,XIP執行比TCM執行快這個結果,痞子衡是不相信的,於是痞子衡便用二分法逐步查找,發現時間差異是BOARD_InitLcdPanel()函數里的DelayMs()調用引起的,這些人為插入的延時是LCD屏控制器手冊里的要求,總延時時間應該是153ms,但是這個函數的執行在XIP下(153ms)和TCM里(203ms)時間不同。

static void BOARD_InitLcdPanel(void)
{
    // ...

#if (DEMO_PANEL ==  DEMO_PANEL_TM103XDKP13)
    // ...

    /* Power LCD on */    
    GPIO_PinWrite(LCD_RESET_GPIO, LCD_RESET_GPIO_PIN, 1);
    DelayMs(2);
    GPIO_PinWrite(LCD_RESET_GPIO, LCD_RESET_GPIO_PIN, 0);
    DelayMs(5);
    GPIO_PinWrite(LCD_RESET_GPIO, LCD_RESET_GPIO_PIN, 1);
    DelayMs(6);
    GPIO_PinWrite(LCD_STBYB_GPIO, LCD_STBYB_GPIO_PIN, 1);
    DelayMs(140);
#endif
    // ...
}

  所以現在的問題就是為何在TCM里執行DelayMs(153)需要203ms,而XIP執行下是精確的。讓我們進一步查看DelayMs()函數的原型,這個函數其實調用的是SDK_DelayAtLeastUs()函數,SDK_DelayAtLeastUs()函數從命名上看就很有意思,AtLeast即保證軟延時一定能滿足用戶設置的時間,但也可能超過這個時間。為何是AtLeast設計,其實這裏就涉及到Cortex-M7內核一個很重要的特性 – 指令雙發射,軟件延時的本質是靠CPU執行指令來消耗時間,但是CPU拿指令到底是單發射還是雙發射有一定的不確定性,因此無法做到精確,如果以全雙發射來計算,就能得出最小延時時間。

#define DelayMs                  VIDEO_DelayMs

#if defined(__ICCARM__)
static void DelayLoop(uint32_t count)
{
    __ASM volatile("    MOV    R0, %0" : : "r"(count));
    __ASM volatile(
        "loop:                          \n"
        "    SUBS   R0, R0, #1          \n"
        "    CMP    R0, #0              \n"
        "    BNE    loop                \n");
}
#endif

void SDK_DelayAtLeastUs(uint32_t delay_us, uint32_t coreClock_Hz)
{
    assert(0U != delay_us);
    uint64_t count = USEC_TO_COUNT(delay_us, coreClock_Hz);
    assert(count <= UINT32_MAX);

#if (__CORTEX_M == 7)
    count = count / 3U * 2U;
#else
    count = count / 4;
#endif
    DelayLoop(count);
}

void VIDEO_DelayMs(uint32_t ms)
{
    SDK_DelayAtLeastUs(ms * 1000U, SystemCoreClock);
}

  分析到現在,問題已經轉化成為何XIP下執行指令雙發射概率比TCM里執行指令雙發射概率更大,關於這個現象並沒有在ARM官方文檔里查找到相關信息,DelayLoop()循環里只是3條指令,XIP下執行肯定是在Cache line里,這跟在TCM里執行並沒有什麼區別。讓我們再去看看兩個工程的map文件,找到DelayLoop()函數鏈接地址,這個函數在兩個測試工程下鏈接地址對齊不一樣,這意味着測試條件不完全相同,或許這是一個解決問題的線索。

  XIP執行工程(flexspi_nor_debug),DelayLoop()函數地址8字節對齊:

*******************************************************************************
*** ENTRY LIST
***

Entry                       Address   Size  Type      Object
-----                       -------   ----  ----      ------
DelayLoop               0x3000'3169    0xa  Code  Lc  fsl_common.o [1]

  TCM執行工程(debug工程),DelayLoop()函數地址4字節對齊:

*******************************************************************************
*** ENTRY LIST
***

Entry                       Address   Size  Type      Object
-----                       -------   ----  ----      ------
DelayLoop                    0x314d    0xa  Code  Lc  fsl_common.o [1]

三、找到計時差異本質

  前面找到DelayLoop()函數鏈接地址差異是一個線索,那我們就針對這個線索做測試,不再讓鏈接器自動分配DelayLoop()函數地址,改為在鏈接文件里指定地址去鏈接,下面代碼是IAR環境下的示例,我們使用debug工程(即在TCM執行)來做測試。

  C源文件中在DelayLoop()函數定義前加#pragma location = “.myFunc”,即將該函數定義為.myFunc的段,然後在鏈接文件icf中用place at語句指定.myFunc段到固定地址m_text_func_start處開始鏈接:

#if defined(__ICCARM__)
#pragma location = ".myFunc"
static void DelayLoop(uint32_t count)
{
    // ...
}
#endif
define symbol m_text_func_start        = 0x00004000;

place at address mem: m_text_func_start     { readonly section .myFunc };

define symbol m_text_start             = 0x00002400;
define symbol m_text_end               = 0x0003FFFF;

place in TEXT_region                        { readonly };

  根據鏈接起始地址m_text_func_start的不同,我們得到了不同的結果,如下錶所示。至此真相大白,造成DelayMs()函數執行時間不同的根本原因不是XIP/TCM執行差異,而是鏈接地址對齊差異,8字節對齊的函數更容易觸發CM7指令雙發射,相比4字節對齊的函數在性能上能提升24.8% 。

m_text_func_start值 鏈接地址對齊 函數調用語句 實際執行時間
0x00004000 8n字節 DelayMs(100) 100ms
0x00004002 2字節,未能鏈接 N/A N/A
0x00004004 4字節 DelayMs(100) 133ms
0x00004008 8字節 DelayMs(100) 100ms

  現在我們得到了一個有趣的結論,Cortex-M7上將函數鏈接到8字節對齊的地址有利於指令雙發射,這就是進一步提升代碼執行性能的秘密。

  至此,i.MXRT上進一步提升代碼執行性能的經驗痞子衡便介紹完畢了,掌聲在哪裡~~~

歡迎訂閱

文章會同時發布到我的 博客園主頁、CSDN主頁、微信公眾號 平台上。

微信搜索”痞子衡嵌入式“或者掃描下面二維碼,就可以在手機上第一時間看了哦。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

淺談Unity的腳本執行順序

一、添加腳本的順序

這是一張官方的腳本順序圖

 

一般,當我們把腳本綁定在遊戲對象上時,或者點擊綁定好的腳本的reset按鈕時,會調用Reset()

當我們初始化一個對象時,會先調用Awake()在調用OnEnable()

GameObject.instantiate(o);

start()在第一次調用update()前調用,有時候start()會在Awake(),OnEnable()執行完后立刻執行,處於同一幀

onDisable()和onDestroy()的執行順序有點特別:

第一種情況:

  對於不同對象上的不同的腳本,執行是沒有先後規律的,且必須成對執行(調用完了onDisable()必須再調用onDestroy())

  eg:

gameobject1:
    sc1.cs
    sc2.cs
gameobject2:
    sc3.cs
    sc4.cs
gameobject3:
    sc5.cs
    sc6.cs

gameobject1.sc1.onDisable()->gameobject1.sc1.onDestroy()->gameobject3.sc5.onDisable()->

gameobject3.sc5.onDestroy()

[如果按前面的規則的話,應該先執行gameobject3的腳本,但這裏不是]

第二種情況:

在同一個對象的不同的腳本上,這兩個方法按照腳本

在m_component中的索引按順序執行的,而且不是成對執行,索引自小到大執行,最早加入的最早調用onDisable()和onDestroy()

gemeobject2.sc3.onDisable()->gameobject2.sc4.onDisable()->

gameobject2.sc3.onDestroy->gameobject2.sc4.onDestroy()

這裏這些腳本先依次調用onDisable()才調用onDestroy()。

二、如何自定義多個腳本的執行順序

 

 划重點:在unity裏面可以點擊edit->project settings->Script->Execution Order中自定義腳本執行順序,

當我們沒有設置腳本執行順序時,腳本按照Default Time的標準執行(你可以把unity裏面hireachy裏面添加對象和在inspector裏面添加腳本

視作一個棧,按照先進先出原則,優先執行后添加的對象的先添加的Script。

可以單擊+號添加對象到執行列表中

1是腳本的執行順序,當你的值execution order越小時,越先執行,在default time上方的在執行default time前執行,在default time下方

的在default time后執行

上面的設置,是通過修改每一個腳本對應的meta文件(去自己的代碼目錄看一下)的excution order來實現的。

 

 三、腳本執行順序的本質

在我們的場景文件.unity,場景文件是一個YAML文檔

 

 每一個腳本對應一個fileid,fileid越小越有先執行

 4.運用

如果你的腳本A會用到另一個腳本B中的某一個對象c,為了防止你在B初始化之前去調用c,出現空指針,你必須讓腳本B在A之後掛載(這樣B就會先初始化),

還有一個很重要的地方,你必須把對象c的初始化寫在腳本B的Awake()方法裏面,因為start()並不是在初始化就立刻調用的,而是在當前更新一幀時,start()

會在update()第一次調用前調用一次,這樣的話如果你把對象c的初始化寫在腳本B的start()方法裏面,很有可能所有腳本的awake()都跑完了,對象c卻沒有初始化,

這樣就出現了空指針。因此,把對象c的初始化在腳本B的awake()方法,而把調用對象c寫在腳本A的start()足夠安全

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

pytorch入門2.2構建回歸模型初體驗(開始訓練)

pytorch入門2.x構建回歸模型系列:
pytorch入門2.0構建回歸模型初體驗(數據生成)
pytorch入門2.1構建回歸模型初體驗(模型構建)
pytorch入門2.2構建回歸模型初體驗(開始訓練)

經過上面兩個部分,我們完成了數據生成、網絡結構定義,下面我們終於可以小試牛刀,訓練模型了!
首先,我們先定義一些訓練時要用到的參數:

EPOCH = 1000  # 就是要把數據用幾遍
LR = 0.1  # 優化器的學習率,類似爬山的時候應該邁多大的步子。
# BATCH_SIZE=50

其次,按照定義的模型類實例化一個網絡:

if torch.cuda.is_available():  # 檢查機器是否支持GPU計算,如果支持GPU計算,那麼就用GPU啦,快!
    model = LinearRegression().cuda()  # 這裏的這個.cuda操作就是把模型放到GPU上
else:
    model = LinearRegression()  # 如果不支持,那麼用cpu也可以哦
# 定義損失函數,要有個函數讓模型的輸出知道他做的對、還是錯,對到什麼程度或者錯到什麼程度,這就是損失函數。
loss_fun = nn.MSELoss()  # loss function
# 定義優化器,就是告訴模型,改如何優化內部的參數、還有該邁多大的步子(學習率LR)。
optimizer = torch.optim.SGD(model.parameters(), lr=LR)  # opimizer

下面終於可以開始訓練了,但是訓練之前解釋一下EPOCH,比如我們有300個樣本,訓練的時候我們不會把300個樣本放到模型裏面訓練一遍,就停止了。即在模型中我們每個樣本不會只用一次,而是會使用多次。這300個樣本到底要用多少次呢,就是EPOCH的值的意義。

for epoch in range(EPOCH):
    # 此處類似前面實例化模型是,我們把模型放到GPU上來跑道理是一樣的。此處,我們要把變量放到GPU上,跑的快!如果不行, 那就放到CPU上吧。
    # 其中x是輸入數據,y是訓練集的groundtruth。為什麼要有y呢?因為我們要知道我們算的對不對,到底有多對(這裏由損失函數控制)
    if torch.cuda.is_available():
        x = Variable(x_train).cuda()
        y = Variable(y_train).cuda()
    else:
        x = Variable(x_train)
        y = Variable(y_train)
    # 我們把x丟進模型,得到輸出y。哇,是不是好簡單,這樣我們就得到結果了呢?但是不要高興的太早,我們只是把輸入數據放到一個啥都不懂(參數沒有訓練)的模型中,得到的結果肯定不準啊。不準的結果怎麼辦,看下一步。
    out = model(x)
    # 拿到模型輸出的結果,我們就要看看模型算的準不準,就是計算損失函數了。
    loss = loss_fun(out,y)
    # 好了好了,我已經知道模型算的準不準了,那麼就該讓模型自己去朝着好的方向優化了。模型,你已經是個大孩子了,應該會自己優化的。
    optimizer.zero_grad()  # 在優化之前,我們首先要清空優化器的梯度。因為每次循環都要靠這個優化器呢,不能翻舊賬,就只算這次我們怎麼優化。
    loss.backward()  # 優化開始,首先,我們要把算出來的誤差、損失倒着傳回去。(是你們這些模塊給我算的這個值,現在這個值有錯誤,錯了這麼多,返回給你們,你們自己看看自己錯哪了)

    optimizer.step()  # 按照優化器的方式,一步一步優化吧。

    if (epoch+1)%100==0:  # 中間每循環100次,偷偷看看結果咋樣。
        print('Epoch[{}/{}],loss:{:.6f}'.format(epoch+1,EPOCH,loss.data.item()))

上面我們訓練了1000(EPOCH=1000)次,應該差不多了。是時候看看訓練的咋樣啦!其實我們已經知道訓練的咋樣了,就是上面輸出的損失值,只不過是在訓練集上的。
下面我們就要看看在測試集上表現咋樣呢?

model.eval()  # 開啟模型的測試模式
# 拿到測試集中x的值,放到GPU上
if  torch.cuda.is_available():
    x = x_test.cuda()
#通過把x的值輸入模型,得到預測結果
predict = model(x)
# 那預測結果的值取出來,因為預測結果是封裝好的,現在h只要它的值。
predict = predict.cpu().data.numpy()
#畫個圖看看,到底擬合成啥樣了?
plt.plot(x.cpu().numpy(),y_test.cpu().numpy(),'ro',label='original data')
plt.plot(sorted(x.cpu().numpy()),sorted(predict),label='fitting line')
plt.show()

看看圖,結果還湊合吧,要想結果更好需要進一步對模型的結構、超參數進行設置,我們之後在學。
到此為止,我們用pytorch就已經建立完,並且訓練完一個線性回歸模型了,我們可以回顧下,多看幾遍,仔細回想一下這裏面到底發生了什麼。
完整的代碼地址如下:github

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

11萬就能買到舒適性堪比天籟的SUV 你覺得值不值

T90最大的賣點來了,都說自主品牌的動力系統調教的不好,T90直接使用了日產MR20的2。0L自然吸氣發動機,最大功率144馬力,最大扭矩198牛·米,匹配CVT變速箱(低配為6擋手動)。這套動力系統目前也用在天籟和逍客上面搭載,看到這些,你還會擔心T90的動力系統么。

12月25日晚上,東風日產啟辰中型SUV—T90正式上市,價格區間為10.98-15.48萬元。

T90也是小編比較期待的一款自主中型SUV,因為此車有着時尚的外觀,精緻的內飾,舒適的座椅,同時和日產有着千絲萬縷的聯繫,所以小編一直很關注它,直到看到它的正式售價,心裏的石頭也終於落地了,畢竟在價格上,啟辰沒有“逗我們玩”。

T90定位中型轎跑型SUV,目前是啟辰的旗艦車型。T90的車身尺寸為4793*1865*1592mm(不同車型高度略有差異),軸距為2765mm,身材在同級別車型處於正常水準。T90的外觀和寶馬X6、歌詩圖有點類似,都是有着溜背的造型,再加上車子較長,所以T90的側面看起來非常的修長,惹人喜愛。

T90的設計理念為“風雕美學”,這是啟辰第一次運用這個理念設計出來的車型,所以T90的整體外觀看起來會使人眼前一亮,同時別緻的前進氣格柵也為T90加分不少。

內飾也是很討人喜歡的地方,中控台都採用了軟性材質覆蓋,摸起來很柔軟,手感很好,同時做工也比較精細,看得出裝配工藝還是很嚴格的。中控台採用了分辨率高達1920*768的12.3英寸的大屏幕,不僅大氣,同時畫面也比較清晰,運行很流暢,客戶體驗效果很好。

座椅也是T90的亮點,因為T90的尺寸足夠大,所以它的內部空間還是非常寬敞的,尤其是T90採用了和天籟同級別的人體工程學座椅,而移動大沙發是天籟的綽號,所以T90座椅的舒適程度可想而知了。不過遺憾的就是溜背的造型會犧牲一定的後排頭部空間,那些大個子乘客可能要盡量坐到前排了。

T90最大的賣點來了,都說自主品牌的動力系統調教的不好,T90直接使用了日產MR20的2.0L自然吸氣發動機,最大功率144馬力,最大扭矩198牛·米,匹配CVT變速箱(低配為6擋手動)。這套動力系統目前也用在天籟和逍客上面搭載,看到這些,你還會擔心T90的動力系統么?

說了這麼多,大家一定會說T90哪款車的性價比最高了。除了低配和頂配兩款車,T90的同一級別車型的自動和手動車型差價一萬元,相當於多花10000元買一台CVT變速箱,這個價格在同級別屬於正常水平。

其中最低配手動辰尚版配備了主/副駕氣囊、無鑰匙啟動/進入、剎車輔助、多功能方向盤、中控大屏、藍牙電話、後座出風口,配置比較實用,基本可以滿足日常行車需求。唯一遺憾的就是沒有ESp。

手動風尚版和手動辰尚版差價8000元,但是只是多了這兩個配置,性價比較低。而CVT風尚版比手動風尚版貴10000元,多了CVT變速箱,這也可以得出CVT風尚版的性價比也是比較低的。

智尚版僅僅比風尚版貴8000元,但是卻多了這麼多的配置,不得不說性價比真的很高。

頂配CVT領尚版比次頂配CVT智尚版貴1.9萬元,但是只是多了后駐車雷達、腰部支撐調節、前排座椅加熱、LED近光燈、車內氛圍燈、併線輔助、全景攝像頭和更大的輪轂,總體來看性價比並不高。

如果資金不充足,最低配手動辰尚版性價比非常高,如果想要一些配置,那麼智尚版的性價比非常高。總體來說啟辰的質量口碑很好,在加上T90和日產的關係,所以T90也是一款不錯的車型,不過受制於啟辰的知名度限制,能不能熱銷,還是要看T90的造化了。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

28歲能買這三款車 最能彰顯你與眾不同!

值得一提的是2。0T車型的百公里加速成績在6。93秒左右。在同級中表現優秀。編者點評:速派外觀設計自成一格,而且動力和油耗表現不錯,1。4T自動擋車型的車主口碑油耗在7。3左右,而1。8T車型則為9。5L。它的底盤調教側重於舒適性,濾振效果也不錯。

中型車在過去被賦予了太多的商務定義

所以設計一向都以大氣、沉穩為主

因為老闆們做生意

總是要求座駕夠穩重

這樣做生意能有許多便利

(圖為大型車 大眾輝騰)

而現在中型車已不再完全向商務性傾斜

因為買家的年齡逐漸下降!

那麼今天我們就來聊聊28歲到30歲之間買什麼樣的合資中型車,最能體現年輕人的個性、與眾不同!

一汽馬自達-阿特茲

指導價:17.58-23.98萬

阿特茲的外觀設計盡顯東方設計之靈性,在日系中型車中是最有個性的!它極具動感的外觀、內飾設計讓許多年輕消費者為之買單。

它採用了帶創馳藍天技術的動力總成,2.0L和2.5L發動機均採用直噴的供油方式,還帶有雙可變氣門正時控制系統!

而車身長寬高為4870*1840*1450mm,軸距達2830mm。車身尺寸屬於中等水平,而它的乘坐空間其實也充足夠用。

編者點評:

阿特茲的運動底子好,這是它的一大優勢,在轉向精準性、動力響應性、底盤運動性方面都有不錯的表現!而且車主反饋它的平均百公里油耗在8.1-8.9L左右(含2.0L和2.5L車型),這麼低的油耗是一系列創馳藍天技術協調運作的結果!

上汽大眾斯柯達-速派

指導價:16.98-27.68萬

鋒利、前衛的外觀,讓速派吸引了很多年輕、熱愛個性的消費者關注!它的車身長寬高為4861*1865*1489mm,軸距達2841mm。

動力系統方面,速派採用了1.4T、1.8T、2.0T三款渦輪增壓發動機,搭配5擋手動、7擋雙離合變速箱。值得一提的是2.0T車型的百公里加速成績在6.93秒左右!在同級中表現優秀。

編者點評:

速派外觀設計自成一格,而且動力和油耗表現不錯,1.4T自動擋車型的車主口碑油耗在7.3左右,而1.8T車型則為9.5L。它的底盤調教側重於舒適性,濾振效果也不錯。

廣汽本田-雅閣

指導價:16.98-27.98萬

雅閣在改款之後,走起了前衛、運動路線,這樣的設計讓它的銷量一下子得到了提升!它的車身尺寸為:4915*1845*1470mm,軸距為2775mm。雖然軸距比起阿特茲和速派不佔優勢,但是雅閣的車身長度卻是最長的,實際的空間表現也不差。

雅閣搭載2.0L、2.4L兩款發動機,搭配CVT變速箱 ,而且還推出了混合動力車型。

編者點評:

雅閣擁有炫麗的外觀、充足的空間,而且對比起凱美瑞、天籟這兩款車,雅閣更具有運動性。它的2.4L車型百公里加速成績在8.68秒左右,口碑油耗則為9.1L,在這兩方面表現都比較良好。

最後總結:

這三款車當中,操控性最好的車型當屬阿特茲,而且它也擁有着更加運動化的外觀。而雅閣和速派的舒適性表現則更好。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

可能不到10萬就買到1.5T自動7座SUV,你還要考慮銳界?

內飾設計感更強中控台採用了對稱式的布局,中控屏周圍加以黑色鋼琴烤漆面板點綴,鍍鉻飾條包邊,整體的設計感很強,8英寸的中控大屏是比較搶眼的配置,除了一般多媒體功能外還支持導航和Carlife手機映射等功能,觸控的空調按鈕操作方便更顯檔次感,而且還會新增發動機啟停,胎壓監測、行車記錄儀等配置,省去了後面加裝的麻煩。

前言

作為長安首款7座SUV,CX70上市已經有大半年的時間了,實惠的價格能買到大7座SUV,寬敞的空間布局也比較符合現在家庭用戶的需求,但是其比較單一的1.6L+5MT的動力總成,對那些不會開手動擋或者對動力有追求的消費者來說,就比較糾結了,最終1.5T+6AT的解決方案出來了,能讓你滿意嗎?

外觀

更加霸氣

前臉的進氣格柵變化是比較明顯的,大小不一的鍍鉻飾塊錯落有致的分佈在中網上,看上去精緻了許多,門把手和側護板上增加了鍍鉻裝飾,提升了不少檔次感,外擴的后保險杠造型讓車身長度增加了25mm,並且覆蓋了金屬裝飾板,既起到保護作用,又使硬朗的造型中透露着細膩感。

內飾

設計感更強

中控台採用了對稱式的布局,中控屏周圍加以黑色鋼琴烤漆面板點綴,鍍鉻飾條包邊,整體的設計感很強,8英寸的中控大屏是比較搶眼的配置,除了一般多媒體功能外還支持導航和Carlife手機映射等功能,觸控的空調按鈕操作方便更顯檔次感,而且還會新增發動機啟停,胎壓監測、行車記錄儀等配置,省去了後面加裝的麻煩。

空間

一如既往的大

2780mm的軸距,加上調整后的保險杠樣式使其車身增加了25mm至4705mm,在同級別競爭對手來說有一定的優勢,採用2+3+2的座椅布局,雙色拼接的座椅設計視覺效果出色。

動力

新的組合

動力方面新增了一款1.5T發動機,最大功率為150馬力,在同排量車型中來說表現中規中矩,與之匹配的是6擋手動,或者是來自愛信的6擋手自一體變速箱,想購買該款車型的消費者終於等來了渦輪增壓車型。

總結:從產品力來看,長安CX70終於補齊了沒有自動擋車型的短板,在逐漸激烈的7座SUV市場里提升了不少的戰鬥力,但具體性能表現如何,還得上市后才見分曉,還有價格上小編推測應該是很有吸引力的一個價格。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

年輕人都想要的運動溜背SUV 居然只要5萬多

隔音經不起考驗在低速時,瑞風S2的隔音還是不錯的,但是上到高速以後,風噪、路噪和發動機噪音都會傳到車廂內。總體來講,瑞風S2的隔音較為一般。後排表現有好也有壞瑞風S2的後排腿部空間充裕,但是頭部空間會顯得局促。

前言

瑞風S2作為S3的小弟,雖然銷量上沒有大哥那麼火爆,但是綜合實力同樣不可小覷。實際表現到底如何,下面編者我就給大家好好說一下。

江淮汽車-瑞風S2

指導價:5.88-7.68萬

外觀光鮮亮麗

瑞風S2的車身尺寸為4135*1750*1568mm,軸距為2490mm。得益於車身較寬,整個前臉看上去很飽滿有力,中間幾條鍍鉻裝飾條把車頭點綴得很閃亮。

車側的設計有一點點怪異,主要是門把手的設計不在一條直線上而是呈一定角度向後揚,輪轂的樣式則很像一股旋風。

尾門的設計不夠大氣,同時導致地台的開起角度偏高。兩大燈之間用了凸起的線條來連接顯得突兀,隱藏式的排氣讓尾部下方很乾凈。

內飾平平無奇

一眼看去,瑞風S2的中控台給人滿滿的塑料感,紅色與黑色的搭配使用並沒有帶來一種亮麗的效果,反而更顯廉價。車門上的銀色護板設計算是內飾中為數不多的亮點。

動力數據優於同級對手

瑞風S2隻有一副1.5L自然吸氣發動機,最大輸出113馬力和146牛米。這個數據無論是哈弗H1還是長安CS15,都無法與之匹敵。與這副發動機匹配的是5擋手動變速箱和CVT變速箱,整體的動力表現一般,因為這個級別的SUV多是搭載1.5L發動機,導致先天動力不夠充足,而瑞風S2的車身更為龐大,所以在高轉時的發力不太好,在2000到3000轉是車子的最佳動力區間。

底盤表現中規中矩

毫無意外地,瑞風S2的后懸架也是使用扭力梁式非獨立懸架。在面對震動時的過濾表現得不錯,但是當路面較為複雜,需要去連續吸收和回彈時,瑞風S2總是餘震不斷,不夠利索。

隔音經不起考驗

在低速時,瑞風S2的隔音還是不錯的,但是上到高速以後,風噪、路噪和發動機噪音都會傳到車廂內。總體來講,瑞風S2的隔音較為一般。

後排表現有好也有壞

瑞風S2的後排腿部空間充裕,但是頭部空間會顯得局促。中央的凸起不高,座椅雖然寬大,但是包裹性不強。

油耗與保養費用

多位車主反映瑞風S2手動擋車型的百公里綜合油耗為8.1L,而自動擋車型為8.9L。6萬公里的總保養費用上,手動擋車型為4639元,而自動擋車型為4585元。

油耗上的表現偏高,而保養費用則在合理範圍內。

配置分析

瑞風S2的2017款車型全系標配ESp,這點很厚道。

手動擋車型我會推薦買指導價為5.88萬的17款手動舒適型,即最低配。因為這個配置已經包含了一些實用配置,不過沒有倒車雷達,這個可以自己後期加裝,沒必要為此加多4000元上手動豪華型。

自動擋車型我會建議買指導價為7.08萬的15款CVT豪華型,即自動擋的最低配。雖然是最低配,但也已經配有了主副駕駛座安全氣囊、中控彩色大屏、ESp和倒車雷達等實用配置。需要注意的是,15款車型的車標還是五角星而非英文字母。

編者總結

瑞風S2價格較低,不過即將迎來寶駿510的挑戰,恐怕也會有些難以抵擋。但是,作為這個價格區間中,少有的帶自動擋的車型,相信瑞風S2還是可以俘獲不少消費者的心。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

蘋果碳中和實驗:拆解舊iPhone、回收稀土的機器人

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心