跳到主要內容

Android DecorView 與 Activity 綁定原理分析

一年多以前,曾經以為自己對 View 的添加显示邏輯已經有所了解了,事後發現也只是懂了些皮毛而已。經過一年多的實戰,Android 和 Java 基礎都有了提升,是時候該去看看 DecorView 的添加显示。


概論


Android 中 Activity 是作為應用程序的載體存在,代表着一個完整的用戶界面,提供了一個窗口來繪製各種視圖,當 Activity 啟動時,我們會通過 setContentView 方法來設置一個內容視圖,這個內容視圖就是用戶看到的界面。那麼 View 和 activity 是如何關聯在一起的呢 ?


 上圖是 View 和 Activity 之間的關係。先解釋圖中一些類的作用以及相關關係:



  • Activity : 對於每一個 activity 都會有擁有一個 PhoneWindow。


  • PhoneWindow :該類繼承於 Window 類,是 Window 類的具體實現,即我們可以通過該類具體去繪製窗口。並且,該類內部包含了一個 DecorView 對象,該 DectorView 對象是所有應用窗口的根 View。

  • DecorView 是一個應用窗口的根容器,它本質上是一個 FrameLayout。DecorView 有唯一一個子 View,它是一個垂直 LinearLayout,包含兩個子元素,一個是 TitleView( ActionBar 的容器),另一個是 ContentView(窗口內容的容器)。


  • ContentView :是一個 FrameLayout(android.R.id.content),我們平常用的 setContentView 就是設置它的子 View 。




  • WindowManager : 是一個接口,裏面常用的方法有:添加View,更新View和刪除View。主要是用來管理 Window 的。WindowManager 具體的實現類是WindowManagerImpl。最終,WindowManagerImpl 會將業務交給 WindowManagerGlobal 來處理。



  • WindowManagerService (WMS) : 負責管理各 app 窗口的創建,更新,刪除, 显示順序。運行在 system_server 進程。


ViewRootImpl :擁有 DecorView 的實例,通過該實例來控制 DecorView 繪製。ViewRootImpl 的一個內部類 W,實現了 IWindow 接口,IWindow 接口是供 WMS 使用的,WSM 通過調用 IWindow 一些方法,通過 Binder 通信的方式,最後執行到了 W 中對應的方法中。同樣的,ViewRootImpl 通過 IWindowSession 來調用 WMS 的 Session 一些方法。Session 類繼承自 IWindowSession.Stub,每一個應用進程都有一個唯一的 Session 對象與 WMS 通信。


DecorView 的創建 


先從 Mainactivity 中的代碼看起,首先是調用了 setContentView;


protected void onCreate(Bundle savedInstanceState) { 
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

該方法是父類 AppCompatActivity 的方法,最終會調用 AppCompatDelegateImpl 的 setContentView 方法:


// AppCompatDelegateImpl  
public void setContentView(int resId) {
this.ensureSubDecor();
ViewGroup contentParent
= (ViewGroup)this.mSubDecor.findViewById(16908290);
contentParent.removeAllViews();
LayoutInflater.from(
this.mContext).inflate(resId, contentParent);
this.mOriginalWindowCallback.onContentChanged();
}

ensureSubDecor 從字面理解就是創建 subDecorView,這個是根據主題來創建的,下文也會講到。創建完以後,從中獲取 contentParent,再將從 activity 傳入的 id xml 布局添加到裏面。不過大家注意的是,在添加之前先調用 removeAllViews() 方法,確保沒有其他子 View 的干擾。


    private void ensureSubDecor() { 
if (!this.mSubDecorInstalled) {
this.mSubDecor = this.createSubDecor();
......
}
......
}

 最終會調用 createSubDecor() ,來看看裏面的具體代碼邏輯:


 private ViewGroup createSubDecor() { 
// 1、獲取主題參數,進行一些設置,包括標題,actionbar 等
TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
if (!a.hasValue(styleable.AppCompatTheme_windowActionBar)) {
a.recycle();
throw new IllegalStateException("You need to use a Theme.AppCompat theme (or descendant) with this activity.");
}
else {
if (a.getBoolean(styleable.AppCompatTheme_windowNoTitle, false)) {
this.requestWindowFeature(1);
}
else if (a.getBoolean(styleable.AppCompatTheme_windowActionBar, false)) {
this.requestWindowFeature(108);
}

if (a.getBoolean(styleable.AppCompatTheme_windowActionBarOverlay, false)) {
this.requestWindowFeature(109);
}

if (a.getBoolean(styleable.AppCompatTheme_windowActionModeOverlay, false)) {
this.requestWindowFeature(10);
}

this.mIsFloating = a.getBoolean(styleable.AppCompatTheme_android_windowIsFloating, false);
a.recycle();
// 2、確保優先初始化 DecorView
this.mWindow.getDecorView();
LayoutInflater inflater
= LayoutInflater.from(this.mContext);
ViewGroup subDecor
= null;
// 3、根據不同的設置來對 subDecor 進行初始化
if (!this.mWindowNoTitle) {
if (this.mIsFloating) {
subDecor
= (ViewGroup)inflater.inflate(layout.abc_dialog_title_material, (ViewGroup)null);
this.mHasActionBar = this.mOverlayActionBar = false;
}
else if (this.mHasActionBar) {
TypedValue outValue
= new TypedValue();
this.mContext.getTheme().resolveAttribute(attr.actionBarTheme, outValue, true);
Object themedContext;
if (outValue.resourceId != 0) {
themedContext
= new ContextThemeWrapper(this.mContext, outValue.resourceId);
}
else {
themedContext
= this.mContext;
}

subDecor
= (ViewGroup)LayoutInflater.from((Context)themedContext).inflate(layout.abc_screen_toolbar, (ViewGroup)null);
this.mDecorContentParent = (DecorContentParent)subDecor.findViewById(id.decor_content_parent);
this.mDecorContentParent.setWindowCallback(this.getWindowCallback());
if (this.mOverlayActionBar) {
this.mDecorContentParent.initFeature(109);
}

if (this.mFeatureProgress) {
this.mDecorContentParent.initFeature(2);
}

if (this.mFeatureIndeterminateProgress) {
this.mDecorContentParent.initFeature(5);
}
}
}
else {
if (this.mOverlayActionMode) {
subDecor
= (ViewGroup)inflater.inflate(layout.abc_screen_simple_overlay_action_mode, (ViewGroup)null);
}
else {
subDecor
= (ViewGroup)inflater.inflate(layout.abc_screen_simple, (ViewGroup)null);
}

if (VERSION.SDK_INT >= 21) {
ViewCompat.setOnApplyWindowInsetsListener(subDecor,
new OnApplyWindowInsetsListener() {
public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
int top = insets.getSystemWindowInsetTop();
int newTop = AppCompatDelegateImpl.this.updateStatusGuard(top);
if (top != newTop) {
insets
= insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), newTop, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
}

return ViewCompat.onApplyWindowInsets(v, insets);
}
});
}
else {
((FitWindowsViewGroup)subDecor).setOnFitSystemWindowsListener(
new OnFitSystemWindowsListener() {
public void onFitSystemWindows(Rect insets) {
insets.top
= AppCompatDelegateImpl.this.updateStatusGuard(insets.top);
}
});
}
}

if (subDecor == null) {
throw new IllegalArgumentException("AppCompat does not support the current theme features: { windowActionBar: " + this.mHasActionBar + ", windowActionBarOverlay: " + this.mOverlayActionBar + ", android:windowIsFloating: " + this.mIsFloating + ", windowActionModeOverlay: " + this.mOverlayActionMode + ", windowNoTitle: " + this.mWindowNoTitle + " }");
}
else {
if (this.mDecorContentParent == null) {
this.mTitleView = (TextView)subDecor.findViewById(id.title);
}

ViewUtils.makeOptionalFitsSystemWindows(subDecor);
ContentFrameLayout contentView
= (ContentFrameLayout)subDecor.findViewById(id.action_bar_activity_content);
ViewGroup windowContentView
= (ViewGroup)this.mWindow.findViewById(16908290);
if (windowContentView != null) {
while(windowContentView.getChildCount() > 0) {
View child
= windowContentView.getChildAt(0);
windowContentView.removeViewAt(
0);
contentView.addView(child);
}

windowContentView.setId(
-1);
contentView.setId(
16908290);
if (windowContentView instanceof FrameLayout) {
((FrameLayout)windowContentView).setForeground((Drawable)
null);
}
}
// 將 subDecor 添加到 DecorView 中
this.mWindow.setContentView(subDecor);
contentView.setAttachListener(
new OnAttachListener() {
public void onAttachedFromWindow() {
}

public void onDetachedFromWindow() {
AppCompatDelegateImpl.
this.dismissPopups();
}
});
return subDecor;
}
}
}

上面的代碼總結來說就是在做一件事,就是創建 subDecor。攤開來說具體如下:


1、根據用戶選擇的主題來設置一些显示特性,包括標題,actionbar 等。


2、根據不同特性來初始化 subDecor;對 subDecor 內部的子 View 進行初始化。


3、最後添加到 DecorView中。


添加的具體代碼如下:此處是通過調用 


 // AppCompatDelegateImpl   this.mWindow.getDecorView(); 

// phoneWindow public final View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}


private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
// 生成 DecorView mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
// 這樣 DecorView 就持有了window mDecor.setWindow(this);
}
......
}


protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use // the context we have. Otherwise we want the application context, so we don't cling to the // activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}

到此,DecorView 的創建就講完了。可是我們似乎並沒有看到 DecorView 是被添加的,什麼時候對用戶可見的。


 WindowManager


View 創建完以後,那 Decorview 是怎麼添加到屏幕中去的呢?當然是 WindowManager 呢,那麼是如何將 View 傳到 WindowManager 中呢。


看 ActivityThread 中的 handleResumeActivity 方法:


// ActivityThread
public
void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
......
final int forwardBit = isForward
? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;

// If the window hasn't yet been added to the window manager,
// and this guy didn't finish itself or start another activity,
// then go ahead and add the window.
boolean willBeVisible = !a.mStartedActivity;
if (!willBeVisible) {
try {
willBeVisible
= ActivityManager.getService().willActivityBeVisible(
a.getActivityToken());
}
catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
if (r.window == null && !a.mFinished && willBeVisible) {
r.window
= r.activity.getWindow();
View decor
= r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm
= a.getWindowManager();
WindowManager.LayoutParams l
= r.window.getAttributes();
a.mDecor
= decor;
l.type
= WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode
|= forwardBit;
......
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded
= true;
wm.addView(decor, l);
}
else {
// The activity will get a callback for this {@link LayoutParams} change
// earlier. However, at that time the decor will not be set (this is set
// in this method), so no action will be taken. This call ensures the
// callback occurs with the decor set.
a.onWindowAttributesChanged(l);
}
}

// If the window has already been added, but during resume
// we started another activity, then don't yet make the
// window visible.
} else if (!willBeVisible) {
if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
r.hideForNow
= true;
}

// Get rid of anything left hanging around.
cleanUpPendingRemoveWindows(r, false /* force */);

// The window is now visible if it has been added, we are not
// simply finishing, and we are not starting another activity.
if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
if (r.newConfig != null) {
performConfigurationChangedForActivity(r, r.newConfig);
if (DEBUG_CONFIGURATION) {
Slog.v(TAG,
"Resuming activity " + r.activityInfo.name + " with newConfig "
+ r.activity.mCurrentConfig);
}
r.newConfig
= null;
}
if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward);
WindowManager.LayoutParams l
= r.window.getAttributes();
if ((l.softInputMode
& WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
!= forwardBit) {
l.softInputMode
= (l.softInputMode
& (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
| forwardBit;
if (r.activity.mVisibleFromClient) {
ViewManager wm
= a.getWindowManager();
View decor
= r.window.getDecorView();
wm.updateViewLayout(decor, l);
}
}

r.activity.mVisibleFromServer
= true;
mNumVisibleActivities
++;
if (r.activity.mVisibleFromClient) {
          
// 這裏也會調用addview
r.activity.makeVisible();
}
}

r.nextIdle
= mNewActivities;
mNewActivities
= r;
if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r);
Looper.myQueue().addIdleHandler(
new Idler());
}

上面的代碼主要做了以下幾件事:


1、獲取到 DecorView,設置不可見,然後通過 wm.addView(decor, l) 將 view 添加到 WindowManager;


2、在某些情況下,比如此時點擊了輸入框調起了鍵盤,就會調用 wm.updateViewLayout(decor, l) 來更新 View 的布局。


3、這些做完以後,會調用 activity 的  makeVisible ,讓視圖可見。如果此時 DecorView 沒有添加到 WindowManager,那麼會添加。 


// Activity
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm
= getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded
= true;
}
mDecor.setVisibility(View.VISIBLE);
}

 接下來,看下 addview 的邏輯。 WindowManager 的實現類是 WindowManagerImpl,而它則是通過 WindowManagerGlobal 代理實現 addView 的,我們看下 addView 的方法:


// WindowManagerGlobal   
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
// ......

root
= new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);

mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
}
catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index,
true);
}
throw e;
}
}

在這裏,實例化了 ViewRootImpl 。同時調用 ViewRootImpl 的 setView 方法來持有了 DecorView。此外這裏還保存了 DecorView ,Params,以及 ViewRootImpl 的實例。


現在我們終於知道為啥 View 是在 OnResume 的時候可見的呢。


 ViewRootImpl


實際上,View 的繪製是由 ViewRootImpl 來負責的。每個應用程序窗口的 DecorView 都有一個與之關聯的 ViewRootImpl 對象,這種關聯關係是由 WindowManager 來維護的。


先看 ViewRootImpl 的 setView 方法,該方法很長,我們將一些不重要的點註釋掉:


   /** 
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView
= view;
......

mAdded
= true;
int res; /* = WindowManagerImpl.ADD_OKAY; */

// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.

requestLayout();
......
}
}
}

這裏先將 mView 保存了 DecorView 的實例,然後調用 requestLayout() 方法,以完成應用程序用戶界面的初次布局。


 public void requestLayout() { 
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested
= true;
scheduleTraversals();
}
}

因為是 UI 繪製,所以一定要確保是在主線程進行的,checkThread 主要是做一個校驗。接着調用 scheduleTraversals 開始計劃繪製了。


void scheduleTraversals() { 
if (!mTraversalScheduled) {
mTraversalScheduled
= true;
mTraversalBarrier
= mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable,
null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

這裏主要關注兩點:


mTraversalBarrier : Handler 的同步屏障。它的作用是可以攔截 Looper 對同步消息的獲取和分發,加入同步屏障之後,Looper 只會獲取和處理異步消息,如果沒有異步消息那麼就會進入阻塞狀態。也就是說,對 View 繪製渲染的處理操作可以優先處理(設置為異步消息)。


mChoreographer: 編舞者。統一動畫、輸入和繪製時機。也是這章需要重點分析的內容。


mTraversalRunnable :TraversalRunnable 的實例,是一個Runnable,最終肯定會調用其 run 方法:


final class TraversalRunnable implements Runnable { 
@Override
public void run() {
doTraversal();
}
}

doTraversal,如其名,開始繪製了,該方法內部最終會調用 performTraversals 進行繪製。


  void doTraversal() { 
if (mTraversalScheduled) {
mTraversalScheduled
= false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {
Debug.startMethodTracing(
"ViewAncestor");
}

performTraversals();

if (mProfile) {
Debug.stopMethodTracing();
mProfile
= false;
}
}
}

到此,DecorView 與 activity 之間的綁定關係就講完了,下一章,將會介紹 performTraversals 所做的事情,也就是 View 繪製流程。 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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



網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!



※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光



※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!




Orignal From: Android DecorView 與 Activity 綁定原理分析

留言

這個網誌中的熱門文章

架構設計 | 異步處理流程,多種實現模式詳解

本文源碼:GitHub·點這裏 || GitEE·點這裏 一、異步處理 1、異步概念 異步處理不用阻塞當前線程來等待處理完成,而是允許後續操作,直至其它線程將處理完成,並回調通知此線程。 必須強調一個基礎邏輯,異步是一種設計理念,異步操作不等於多線程,MQ中間件,或者消息廣播,這些是可以實現異步處理的方式。 同步處理和異步處理相對,需要實時處理並響應,一旦超過時間會結束會話,在該過程中調用方一直在等待響應方處理完成並返回。同步類似電話溝通,需要實時對話,異步則類似短信交流,發送消息之後無需保持等待狀態。 2、異步處理優點 雖然異步處理不能實時響應,但是處理複雜業務場景,多數情況都會使用異步處理。 異步可以解耦業務間的流程關聯,降低耦合度; 降低接口響應時間,例如用戶註冊,異步生成相關信息表; 異步可以提高系統性能,提升吞吐量; 流量削峰即把請求先承接下來,然後在異步處理; 異步用在不同服務間,可以隔離服務,避免雪崩; 異步處理的實現方式有很多種,常見多線程,消息中間件,發布訂閱的廣播模式,其根據邏輯在於先把請求承接下來,放入容器中,在從容器中把請求取出,統一調度處理。 注意 :一定要監控任務是否產生積壓過度情況,任務如果積壓到雪崩之勢的地步,你會感覺每一片雪花都想勇闖天涯。 3、異步處理模式 異步流程處理的實現有好多方式,但是實際開發中常用的就那麼幾種,例如: 基於接口異步響應,常用在第三方對接流程; 基於消息生產和消費模式,解耦複雜流程; 基於發布和訂閱的廣播模式,常見系統通知 異步適用的業務場景,對數據強一致性的要求不高,異步處理的數據更多時候追求的是最終一致性。 二、接口響應異步 1、流程描述 基於接口異步響應的方式,有一個本地業務服務,第三方接口服務,流程如下: 本地服務發起請求,調用第三方服務接口; 請求包含業務參數,和成功或失敗的回調地址; 第三方服務實時響應流水號,作為該調用的標識; 之後第三方服務處理請求,得到最終處理結果; 如果處理成功,回調本地服務的成功通知接口; 如果處理失敗,回調本地服務的失敗通知接口; 整個流程基於部分異步和部分實時的模式,完整處理; 注意 :如...

.NET Core前後端分離快速開發框架(Core.3.0+AntdVue)

.NET Core前後端分離快速開發框架(Core.3.0+AntdVue) 目錄 引言 時間真快,轉眼今年又要過去了。回想今年,依次開源發布了 Colder.Fx.Net.AdminLTE(254Star) 、 Colder.Fx.Core.AdminLTE(335Star) 、 DotNettySocket(82Star) 、 IdHelper(47Star) ,這些框架及組件都是本着以實際出發,實事求是的態度,力求提高開發效率(我自己都是第一個使用者),目前來看反響不錯。但是隨着前端和後端技術的不斷變革,尤其是前端,目前大環境已經是前後端完全分離為主的開發模式,在這樣的大環境和必然趨勢之下,傳統的MVC就顯得有些落伍了。在這樣的背景下,一款前後端分離的.NET開發框架就顯得尤為必要,由此便定了框架的升級目標: 前後端分離 。 首先後端技術的選擇,從目前的數據來看,.NET Core的發展遠遠快於.NET Framework,最簡單的分析就是Colder.Fx.Core.AdminLTE發布比Colder.Fx.Net.AdminLTE晚,但是星星卻後來居上而且比前者多30%,並且這個差距在不斷擴大,由點及面的分析可以看出我們廣大.NET開發人員學習的熱情和积極向上的態度,並不是某些人所認為的那麼不堪( 走自己的路,讓別人說去吧 )。大環境上微軟积極擁抱開源,大力發展.NET Core, 可以說前途一片光明。因此後端決定採用 .NET Core3.0 ,不再浪費精力去支持.NET Framework。 然後是前端技術選擇,首選是三大js框架選擇,也是從實際出發,Vue相對其它而言更加容易上手,並且功能也毫不遜色,深得各種大小公司喜歡,如果偏要說缺點的話,那就是對TS支持不行,但是即將發布Vue3.0肯定會改變這一缺陷。選擇了Vue之後,然後就是UI框架的選擇了,這裏的選擇更多了,我選擇了Ant Design Vue,理由便是簡潔方便,十分符合我的設計理念。 技術選型完畢之後便...

台北市住宅、社區建創儲能設備 最高可獲600萬元補助

為了推廣分散式發電,台北市環保局預計補助1億元供住宅社區設置創能、儲能設備,計有3種方案可供選擇。環保局說明,每案補助額度不超過建制總經費49%,社區每案最高可獲200萬至600萬元補助,住宅每案補助上限100萬元,5月1日起開放申請。 環保局說明,台北市溫室氣體排放量7成以上來自住商部門,其中以使用電力造成間接溫室氣體排放為大宗,台北市平均年用電量約159.86億度,1度電約等同排放0.5公斤二氧化碳,若想達成2050年淨零排放目標,僅靠節能減碳無法達成,必須發展綠色創能、儲能,並且參考歐洲、日本的做法,採分散式發電方式,推廣到社區、住家、商辦,達到供電自給自足目標。 因此,環保局推出「台北市住宅社區創能儲能及節能補助計畫」,補助對象為台北市轄內房屋所有權人及社區管理委員會,補助方案共計3種,每一申請人或每一場址僅能獲1次補助,每案補助額度不超過建置總經費49%為限,5月1日到7月31日開放申請,但補助經費用完即停止申請。 環保局說明,方案A補助對象以社區為主,公共區域申請創能儲能及節能項目,每案補助上限新台幣600萬元;方案B分為住宅或社區公共區域申請創能搭配儲能項目(創能或儲能方案不得單獨申請),社區每案補助上限新台幣400萬元,住宅每案補助上限100萬元。方案C補助對象也是社區,公共區域申請節能項目,每案補助上限新台幣200萬元。 網頁設計 最專業,超強功能平台可客製,窩窩以「數位行銷」「品牌經營」「網站與應用程式」「印刷品設計」等四大主軸,為每一位客戶客製建立行銷脈絡及洞燭市場先機,請問 台中電動車 哪裡在賣比較便宜可以到台中景泰電動車門市去看看總店:臺中市潭子區潭秀里雅潭路一段102-1號。 電動車補助 推薦評價好的 iphone維修 中心擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢住家的頂樓裝 太陽光電 聽說可發揮隔熱功效一線推薦東陽能源擁有核心技術、產品研發、系統規劃設置、專業團隊的太陽能發電廠商。 網頁設計 一頭霧水該從何著手呢? 回頭車 貨運收費標準宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念 台中搬家公司 教你幾個打包小技巧,輕鬆整理裝箱!還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多...