android 皮肤方案,Android布局流程及换肤原理

Android 的布局流程

不考虑AMS binder机制,那么Android 的布局流程的最开始的入口(把前面的当作一个黑盒子,那么后续动作的第一个入口),则是在ActivityThread的performLaunchActivity方法。activity的context也是在这个方法中初始化的。

ContextImpl appContext = createBaseContextForActivity(r);

Activity activity = null;

try {

java.lang.ClassLoader cl = appContext.getClassLoader();

//intent中有一些配置信息

activity = mInstrumentation.newActivity(

cl, component.getClassName(), r.intent);

StrictMode.incrementExpectedActivityCount(activity.getClass());

r.intent.setExtrasClassLoader(cl);

r.intent.prepareToEnterProcess();

if (r.state != null) {

r.state.setClassLoader(cl);

}

} catch (Exception e) {

if (!mInstrumentation.onException(activity, e)) {

throw new RuntimeException(

"Unable to instantiate activity " + component

+ ": " + e.toString(), e);

}

}

这里看的是布局的原理,不是启动的原理,所以紧接着,要接着看布局方面的,接着找源码,找到了window的赋值

Window window = null;

if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {

window = r.mPendingRemoveWindow;

r.mPendingRemoveWindow = null;

r.mPendingRemoveWindowManager = null;

}

这里能看到,将window赋值为r.mPendingRemoveWindow;

然后接着追r.mPendingRemoveWindow的赋值,能看到它的赋值为

if (r.activity.mWindowAdded) {

if (r.mPreserveWindow) {

// Hold off on removing this until the new activity's

// window is being added.

r.mPendingRemoveWindow = r.window;

r.mPendingRemoveWindowManager = wm;

// We can only keep the part of the view hierarchy that we control,

// everything else must be removed, because it might not be able to

// behave properly when activity is relaunching.

r.window.clearContentView();

} else {

wm.removeViewImmediate(v);

}

}

然后紧接着看 r.window的赋值,发现它的值,就是引入了activity中的一个成员变量window

r.window = r.activity.getWindow();

View decor = r.window.getDecorView();

目前这个属性还未有值,只是拿到这个赋值。

它的值真正的赋值,是在下一步中,及activity的attach中

appContext.setOuterContext(activity);

activity.attach(appContext, this, getInstrumentation(), r.token,

r.ident, app, r.intent, r.activityInfo, title, r.parent,

r.embeddedID, r.lastNonConfigurationInstances, config,

r.referrer, r.voiceInteractor, window, r.configCallback,

r.assistToken);

其中这个r指的是ActivityClientRecord,它里面的信息,是在上面提到的黑盒子里初始化的,目前看布局原理就先不考虑这个了。

在activity的attach方法中,可以看到mWindow的赋值

mWindow = new PhoneWindow(this, window, activityConfigCallback);

mWindow.setWindowControllerCallback(this);

mWindow.setCallback(this);

mWindow.setOnWindowDismissedCallback(this);

mWindow.getLayoutInflater().setPrivateFactory(this);

然后我们看布局放上去的方法,一般在activity的oncreate中的setcontentview中,点进去发现它调用的方法为

public void setContentView(@LayoutRes int layoutResID) {

getWindow().setContentView(layoutResID);

initWindowDecorActionBar();

}

而getwindow即mWindow,也就是PhoneWindow

所以,查看它的相应方法

@Override

public void setContentView(int layoutResID) {

// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window

// decor, when theme attributes and the like are crystalized. Do not check the feature

// before this happens.

if (mContentParent == null) {

installDecor();

} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {

mContentParent.removeAllViews();

}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {

final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,

getContext());

transitionTo(newScene);

} else {

mLayoutInflater.inflate(layoutResID, mContentParent);

}

mContentParent.requestApplyInsets();

final Callback cb = getCallback();

if (cb != null && !isDestroyed()) {

cb.onContentChanged();

}

mContentParentExplicitlySet = true;

}

在第一步,进行了installDecor()跟进去接着看

private void installDecor() {

mForceDecorInstall = false;

if (mDecor == null) {

mDecor = generateDecor(-1);

mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);

mDecor.setIsRootNamespace(true);

if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {

mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);

}

} else {

mDecor.setWindow(this);

}

if (mContentParent == null) {

mContentParent = generateLayout(mDecor);

.....

.....

然后点进generateLayout(mDecor)

它就是各种操作,根据不同值得到不同的layoutResource(系统内置的一些)然后加载,假如得到一个layoutResource之后,则会

mDecor.startChanging();

mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

在进入,就能看到

mDecorCaptionView = createDecorCaptionView(inflater);

final View root = inflater.inflate(layoutResource, null);

if (mDecorCaptionView != null) {

if (mDecorCaptionView.getParent() == null) {

addView(mDecorCaptionView,

new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

}

mDecorCaptionView.addView(root,

new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));

} else {

// Put it below the color views.

addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

}

mContentRoot = (ViewGroup) root;

initializeElevation();

这个layoutResource还不是我们自己定义的layout,这是系统匹配出来的一个layoutResource

然后在通过addview的方式,也就把刚刚文件上的布局,放到了DecorView上了

**所以activity一打开,首先展示的就是这个layout**

其实看源码也相当于看一个树形结构,从一个分叉深入进去后,想要顺序看,还是得接着回到刚刚引入深入点的那个位置

然后将源码回退到phoneWindow中,接着往下看。看到

然后通过刚刚那一系列深入,能看出mContentParent其实也就是那个layout中的content。

mLayoutInflater.inflate(layoutResID, mContentParent);

然后点入inflate方法进去,看到很多inflate方法,但是最终,他都是会调到这个inflate里,即

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)

所以,我们这里的root也就是刚刚传入过来的content控件

所以,整体结构相当于

ce665f0d4122

image.png

然后在inflate方法中,接着看

// Temp is the root view that was found in the xml

final View temp = createViewFromTag(root, name, inflaterContext, attrs);

ViewGroup.LayoutParams params = null;

if (root != null) {

if (DEBUG) {

System.out.println("Creating params from root: " +

root);

}

// Create layout params that match root, if supplied

params = root.generateLayoutParams(attrs);

if (!attachToRoot) {

// Set the layout params for temp if we are not

// attaching. (If we are, we use addView, below)

temp.setLayoutParams(params);

}

}

attachToRoot这个参数,表示你是写代码add进去的view,还是从xml解析中进入的。通常写代码add的都是ture,从xml中进入的是false,所以,当为false的时候,会把params设置进去。

接着看createViewFromTag方法是怎么创建temp这个view的

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,

boolean ignoreThemeAttr) {

if (name.equals("view")) {

name = attrs.getAttributeValue(null, "class");

}

// Apply a theme wrapper, if allowed and one is specified.

if (!ignoreThemeAttr) {

final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);

final int themeResId = ta.getResourceId(0, 0);

if (themeResId != 0) {

context = new ContextThemeWrapper(context, themeResId);

}

ta.recycle();

}

try {

View view = tryCreateView(parent, name, context, attrs);

if (view == null) {

final Object lastContext = mConstructorArgs[0];

mConstructorArgs[0] = context;

try {

if (-1 == name.indexOf('.')) {

view = onCreateView(context, parent, name, attrs);

} else {

view = createView(context, name, null, attrs);

}

} finally {

mConstructorArgs[0] = lastContext;

}

}

return view;

} catch (InflateException e) {

throw e;

} catch (ClassNotFoundException e) {

final InflateException ie = new InflateException(

getParserStateDescription(context, attrs)

+ ": Error inflating class " + name, e);

ie.setStackTrace(EMPTY_STACK_TRACE);

throw ie;

} catch (Exception e) {

final InflateException ie = new InflateException(

getParserStateDescription(context, attrs)

+ ": Error inflating class " + name, e);

ie.setStackTrace(EMPTY_STACK_TRACE);

throw ie;

}

}

onCreateView里,最后创建调用的还是createView

紧接着看createview中

Objects.requireNonNull(viewContext);

Objects.requireNonNull(name);

Constructor extends View> constructor = sConstructorMap.get(name);

if (constructor != null && !verifyClassLoader(constructor)) {

constructor = null;

sConstructorMap.remove(name);

}

Class extends View> clazz = null;

try {

Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

if (constructor == null) {

// Class not found in the cache, see if it's real, and try to add it

clazz = Class.forName(prefix != null ? (prefix + name) : name, false,

mContext.getClassLoader()).asSubclass(View.class);

if (mFilter != null && clazz != null) {

boolean allowed = mFilter.onLoadClass(clazz);

if (!allowed) {

failNotAllowed(name, prefix, viewContext, attrs);

}

}

constructor = clazz.getConstructor(mConstructorSignature);

constructor.setAccessible(true);

sConstructorMap.put(name, constructor);

} else {

// If we have a filter, apply it to cached constructor

if (mFilter != null) {

// Have we seen this name before?

Boolean allowedState = mFilterMap.get(name);

if (allowedState == null) {

// New class -- remember whether it is allowed

clazz = Class.forName(prefix != null ? (prefix + name) : name, false,

mContext.getClassLoader()).asSubclass(View.class);

boolean allowed = clazz != null && mFilter.onLoadClass(clazz);

mFilterMap.put(name, allowed);

if (!allowed) {

failNotAllowed(name, prefix, viewContext, attrs);

}

} else if (allowedState.equals(Boolean.FALSE)) {

failNotAllowed(name, prefix, viewContext, attrs);

}

}

}

首先 clazz = Class.forName(prefix != null ? (prefix + name) : name, false,

mContext.getClassLoader()).asSubclass(View.class);是通过名字找到的clazz

然后在 constructor = clazz.getConstructor(mConstructorSignature);

static final Class>[] mConstructorSignature = new Class[] {

Context.class, AttributeSet.class};

所以,在自定义控件中,这个两个参数的构造函数一定不能少,它的初始化都是调用了这个两个参数的方法。随后就把这个构造方法和名字缓存起来了,放入了sConstructorMap

然后在这个方法的下面(源码没放全,毕竟都挺多的,放进去通篇都是源码了)

try {

final View view = constructor.newInstance(args);

if (view instanceof ViewStub) {

// Use the same context when inflating ViewStub later.

final ViewStub viewStub = (ViewStub) view;

viewStub.setLayoutInflater(cloneInContext((Context) args[0]));

}

return view;

} finally {

mConstructorArgs[0] = lastContext;

}

通过newInstance得到这个view,然后进行return

所以,如果要实现换肤,这里是一条思路,可以在其中做些事情,通过自定义layoutInfalter,在createView方法中进行某些操作

还有一处,在createViewFromTag方法中,在view==null判断前,还有View view = tryCreateView(parent, name, context, attrs);操作,这个方法里用到了各种factory

public final View tryCreateView(@Nullable View parent, @NonNull String name,

@NonNull Context context,

@NonNull AttributeSet attrs) {

if (name.equals(TAG_1995)) {

// Let's party like it's 1995!

return new BlinkLayout(context, attrs);

}

View view;

if (mFactory2 != null) {

view = mFactory2.onCreateView(parent, name, context, attrs);

} else if (mFactory != null) {

view = mFactory.onCreateView(name, context, attrs);

} else {

view = null;

}

if (view == null && mPrivateFactory != null) {

view = mPrivateFactory.onCreateView(parent, name, context, attrs);

}

return view;

}

从中可以很清楚的看到,当我们设置了factory或者factory2后(这俩工厂2继承1,然后2中的参数带有父控件,1不带),就可以直接对view进行初始化,也就可以隔断前面创建view的过程了。

所以,我们也可以通过设置factory来完成换肤,工厂内的oncreateview方法,抄源码然后做响应的自己需求的修改就好了。记得这个工厂的设置要放在setcontentview之前。很显而易见。。。

所以,View temp的创建这里,可以延伸出两种换肤方案。

资源文件

在C++层,会干一个事情,生成一个resource.arsc文件,类似于数据库文件,整个这个文件是一个二进制的文件。在这个文件中,标示了res这个路径下的每一个资源所对应的信息。

ce665f0d4122

image.png

所以在我们加载一个apk包的时候,会有一个类去负责读这个文件的信息和这个res目录下的信息。

现在接着回ActivityThread里。。。找到handleBindApplication方法,找到资源加载的过程都在什么时间点

首先注意到这个仪表信息类,这个东西还是很重要的。很多事情要通过它来完成

mInstrumentation = new Instrumentation();

mInstrumentation.basicInit(this);

然后初始化application

// Allow disk access during application and provider setup. This could

// block processing ordered broadcasts, but later processing would

// probably end up doing the same disk access.

Application app;

final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();

final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();

try {

// If the app is being launched for full backup or restore, bring it up in

// a restricted environment with the base application class.

app = data.info.makeApplication(data.restrictedBackupMode, null);

// Propagate autofill compat state

app.setAutofillOptions(data.autofillOptions);

// Propagate Content Capture options

app.setContentCaptureOptions(data.contentCaptureOptions);

mInitialApplication = app;

// don't bring up providers in restricted mode; they may depend on the

// app's custom Application class

if (!data.restrictedBackupMode) {

if (!ArrayUtils.isEmpty(data.providers)) {

installContentProviders(app, data.providers);

}

}

// Do this after providers, since instrumentation tests generally start their

// test thread at this point, and we don't want that racing.

try {

mInstrumentation.onCreate(data.instrumentationArgs);

}

catch (Exception e) {

throw new RuntimeException(

"Exception thrown in onCreate() of "

+ data.instrumentationName + ": " + e.toString(), e);

}

try {

mInstrumentation.callApplicationOnCreate(app);

} catch (Exception e) {

if (!mInstrumentation.onException(app, e)) {

throw new RuntimeException(

"Unable to create application " + app.getClass().getName()

+ ": " + e.toString(), e);

}

}

} finally {

// If the app targets < O-MR1, or doesn't change the thread policy

// during startup, clobber the policy to maintain behavior of b/36951662

if (data.appInfo.targetSdkVersion < Build.VERSION_CODES.O_MR1

|| StrictMode.getThreadPolicy().equals(writesAllowedPolicy)) {

StrictMode.setThreadPolicy(savedPolicy);

}

}

将application初始化之后,然后通过仪表类mInstrumentation调用它的oncreate方法 mInstrumentation.callApplicationOnCreate(app);

从创建application中点进去

ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);

app = mActivityThread.mInstrumentation.newApplication(

cl, appClass, appContext);

appContext.setOuterContext(app);

这里先初始化来一个contextImpl,接着点进去

static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,

String opPackageName) {

if (packageInfo == null) throw new IllegalArgumentException("packageInfo");

ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,

null, opPackageName);

context.setResources(packageInfo.getResources());

return context;

}

在这里有一句很重要的话

context.setResources(packageInfo.getResources());

从包信息中获取资源,也就是从apk中去拿资源

然后接着看里面具体是怎么获取资源的,点进去(LoadedApk类的getResources方法)

public Resources getResources() {

if (mResources == null) {

final String[] splitPaths;

try {

splitPaths = getSplitPaths(null);

} catch (NameNotFoundException e) {

// This should never fail.

throw new AssertionError("null split not found");

}

mResources = ResourcesManager.getInstance().getResources(null, mResDir,

splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,

Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),

getClassLoader());

}

return mResources;

}

然后从mResources的初始化中接着进去(到了ResourceManager类的getResources方法)

public @Nullable Resources getResources(@Nullable IBinder activityToken,

@Nullable String resDir,

@Nullable String[] splitResDirs,

@Nullable String[] overlayDirs,

@Nullable String[] libDirs,

int displayId,

@Nullable Configuration overrideConfig,

@NonNull CompatibilityInfo compatInfo,

@Nullable ClassLoader classLoader) {

try {

Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");

final ResourcesKey key = new ResourcesKey(

resDir,

splitResDirs,

overlayDirs,

libDirs,

displayId,

overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy

compatInfo);

classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();

return getOrCreateResources(activityToken, key, classLoader);

} finally {

Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);

}

}

重要的是 return getOrCreateResources(activityToken, key, classLoader);获取或者创建资源,再点进去

找到再往下的一个入口

final ActivityResources activityResources =

getOrCreateActivityResourcesStructLocked(activityToken);

再深入

private ActivityResources getOrCreateActivityResourcesStructLocked(

@NonNull IBinder activityToken) {

ActivityResources activityResources = mActivityResourceReferences.get(activityToken);

if (activityResources == null) {

activityResources = new ActivityResources();

mActivityResourceReferences.put(activityToken, activityResources);

}

return activityResources;

}

(看源码中,看到在集合类里去取我们需要的东西,这个集合类刚开始一般都为空,一个套路,所以可以先暂时不看这个,往下看)

ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);

if (resourcesImpl != null) {

if (DEBUG) {

Slog.d(TAG, "- using existing impl=" + resourcesImpl);

}

return getOrCreateResourcesForActivityLocked(activityToken, classLoader,

resourcesImpl, key.mCompatInfo);

}

发现resourcesImpl还是从集合中去找,紧接着继续往下,找到了其初始化的位置

// If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.

ResourcesImpl resourcesImpl = createResourcesImpl(key);

if (resourcesImpl == null) {

return null;

}

此处通过放入一个key来创建了资源,这个key暂时还不知道是干什么的,然后我们接着深入这个方法里面去查看

private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {

final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);

daj.setCompatibilityInfo(key.mCompatInfo);

final AssetManager assets = createAssetManager(key);

if (assets == null) {

return null;

}

final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);

final Configuration config = generateConfig(key, dm);

final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);

if (DEBUG) {

Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);

}

return impl;

}

会发现它里面给我们造了一个AssetManager,然后接着进入AssetManager的初始化中createAssetManager(key);

/**

* Creates an AssetManager from the paths within the ResourcesKey.

*

* This can be overridden in tests so as to avoid creating a real AssetManager with

* real APK paths.

* @param key The key containing the resource paths to add to the AssetManager.

* @return a new AssetManager.

*/

@VisibleForTesting

@UnsupportedAppUsage

protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {

final AssetManager.Builder builder = new AssetManager.Builder();

// resDir can be null if the 'android' package is creating a new Resources object.

// This is fine, since each AssetManager automatically loads the 'android' package

// already.

if (key.mResDir != null) {

try {

builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,

false /*overlay*/));

} catch (IOException e) {

Log.e(TAG, "failed to add asset path " + key.mResDir);

return null;

}

}

if (key.mSplitResDirs != null) {

for (final String splitResDir : key.mSplitResDirs) {

try {

builder.addApkAssets(loadApkAssets(splitResDir, false /*sharedLib*/,

false /*overlay*/));

} catch (IOException e) {

Log.e(TAG, "failed to add split asset path " + splitResDir);

return null;

}

}

}

if (key.mOverlayDirs != null) {

for (final String idmapPath : key.mOverlayDirs) {

try {

builder.addApkAssets(loadApkAssets(idmapPath, false /*sharedLib*/,

true /*overlay*/));

} catch (IOException e) {

Log.w(TAG, "failed to add overlay path " + idmapPath);

// continue.

}

}

}

if (key.mLibDirs != null) {

for (final String libDir : key.mLibDirs) {

if (libDir.endsWith(".apk")) {

// Avoid opening files we know do not have resources,

// like code-only .jar files.

try {

builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/,

false /*overlay*/));

} catch (IOException e) {

Log.w(TAG, "Asset path '" + libDir +

"' does not exist or contains no resources.");

// continue.

}

}

}

}

return builder.build();

}

builder.addApkAssets这个api的调用,就帮我们实现了资源文件的路径的加载(里面就是一个数据结构列表的添加,但是通过构造ApkAssets对象,其内部是native实现)

* The main implementation is native C++ and there is very little API surface exposed here. The APK

* is mainly accessed via {@link AssetManager}.

它中间最重要的是ApkAssets这个类,从这个类的注视中也能看出,它主要有native实现,通过构造方法中在进入nativeLoad

所以,也能得出,这个apk加载好以后,生成resources.arsc文件。所以换肤的策略,也可以使用assceManager这个api进行这个皮肤apk路径的添加,他就会把这个apk里的resources.arsc以及res信息加载进我们的apk中去

所以,我们的app就相当于一个宿主apk,然后皮肤的apk就相当于插件apk。(addApkAssets这个方法中加入插件apk相关信息)

它的整体封装结构为

ce665f0d4122

image.png

所以,我们平常在使用resource的时候,其内部就是调用到resourcesImpl里,然后调到mAssets这个量里面的方法。

所以我们AssetManager只干一个事情,就是它通过resource.arsc这个表找到对应的资源,然后再通过读流去取这些数据。所以ResourceImpl Resources相对AssetManager都是一个包装,包着它,顺便加了一些其他数据。从AssetManager内的一些加载方法中可以看出,它根据一些信息去获取这些资源,那么我们也就可以使用它来实现换肤的一些操作

所以,可以看出换肤的基本步骤为

制作皮肤包APK

收集XML数据

利用view的生产对象的过程Factory2接口(注意setFactory2中的实现,默认这个设置只能用一次,其内部的一个标识就会改变。所以可以通过反射去修改它的这个标识)

记录需要换肤的属性

读取皮肤包内容(记得先加载进来)不同版本,具体的api操作变了,记得区分

执行换肤操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值