每日一问:View.getContext() 的返回一定是 Activity 么?
坚持原创日更,短平快的 Android 进阶系列,敬请直接在微信公众号搜索:nanchen,直接关注并设为星标,精彩不容错过。
一般我们被问到这样的问题,通常来说,答案都是否定的,但一定得知道其中的原因,不然回答肯定与否又有什么意义呢。
首先,显而易见这个问题有不少陷阱,比如这个 View 是自己构造出来的,那肯定它的 getContext()
返回的是构造它的时候传入的 Context
类型。
它也可能返回的是 TintContextWrapper
那,如果是 XML 里面的 View 呢,会怎样?可能不少人也知道了另外一个结论:直接继承 Activity 的 Activity 构造出来的 View.getContext() 返回的是当前 Activity。但是:当 View 的 Activity 是继承自 AppCompatActivity,并且在 5.0 以下版本的手机上,View.getContext() 得到的并非是 Activity,而是 TintContextWrapper。
不太熟悉 Context
的继承关系的小伙伴可能也会很奇怪,正常来说,自己所知悉的 Context
继承关系图是这样的。
Activity.setContentView()
我们可以先看看 Activity.setContentView()
方法:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
不过是直接调用 Window
的实现类 PhoneWindow
的 setContentView()
方法。看看 PhoneWindow
的 setContentView()
是怎样的。
@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;
}
假如没有 FEATURE_CONTENT_TRANSITIONS
标记的话,就直接通过 mLayoutInflater.inflate()
加载出来。这个如果有 mLayoutInflater
的是在PhoneWindow
的构造方法中被初始化的。而 PhoneWindow
的初始化是在 Activity
的 attach()
方法中:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
// 此处省略部分代码...
}
所以 PhoneWindow
的 Context
实际上就是 Activity
本身。
在回到我们前面分析的 PhoneWindow
的 setContentView()
方法,如果有 FEATURE_CONTENT_TRANSITIONS
标记,直接调用了一个 transitionTo()
方法:
private void transitionTo(Scene scene) {
if (mContentScene == null) {
scene.enter();
} else {
mTransitionManager.transitionTo(scene);
}
mContentScene = scene;
}
在看看 scene.enter()
方法。
public void enter() {
// Apply layout change, if any
if (mLayoutId > 0 || mLayout != null) {
// empty out parent container before adding to it
getSceneRoot().removeAllViews();
if (mLayoutId > 0) {
LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
} else {
mSceneRoot.addView(mLayout);
}
}
// Notify next scene that it is entering. Subclasses may override to configure scene.
if (mEnterAction != null) {
mEnterAction.run();
}
setCurrentScene(mSceneRoot, this);
}
基本逻辑没必要详解了吧?还是通过这个 mContext
的 LayoutInflater
去 inflate
的布局。这个 mContext
初始化的地方是:
public static Scene getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) {
SparseArray<Scene> scenes = (SparseArray<Scene>) sceneRoot.getTag(
com.android.internal.R.id.scene_layoutid_cache);
if (scenes == null) {
scenes = new SparseArray<Scene>();
sceneRoot.setTagInternal(com.android.internal.R.id.scene_layoutid_cache, scenes);
}
Scene scene = scenes.get(layoutId);
if (scene != null) {
return scene;
} else {
scene = new Scene(sceneRoot, layoutId, context);
scenes.put(layoutId, scene);
return scene;
}
}
即 Context
来源于外面传入的 getContext()
,这个 getContext()
返回的就是初始化的 Context
也就是 Activity
本身。
AppCompatActivity.setContentView()
我们不得不看看 AppCompatActivity
的 setContentView()
是怎么实现的。
public void setContentView(@LayoutRes int layoutResID) {
this.getDelegate().setContentView(layoutResID);
}
@NonNull
public AppCompatDelegate getDelegate() {
if (this.mDelegate == null) {
this.mDelegate = AppCompatDelegate.create(this, this);
}
return this.mDelegate;
}
这个 mDelegate
实际上是一个代理类,由 AppCompatDelegate
根据不同的 SDK 版本生成不同的实际执行类,就是代理类的兼容模式:
/**
* Create a {@link android.support.v7.app.AppCompatDelegate} to use with {@code activity}.
*
* @param callback An optional callback for AppCompat specific events
*/
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return create(activity, activity.getWindow(), callback);
}
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (BuildCompat.isAtLeastN()) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
关于实现类 AppCompatDelegateImpl
的 setContentView()
方法这里就不做过多分析了,感兴趣的可以直接移步掘金上的 View.getContext() 里的小秘密 进行查阅。
不过这里还是要结合小缘的回答,简单总结一下:之所以能得到上面的结论是因为我们在 AppCompatActivity
里面的 layout.xml
文件里面使用原生控件,比如 TextView
、ImageView
等等,当在 LayoutInflater
中把 XML 解析成 View
的时候,最终会经过 AppCompatViewInflater
的 createView()
方法,这个方法会把这些原生的控件都变成 AppCompatXXX
一类。包含了哪些 View 呢?
- RatingBar
- CheckedTextView
- MultiAutoCompleteTextView
- TextView
- ImageButton
- SeekBar
- Spinner
- RadioButton
- ImageView
- AutoCompleteTextView
- CheckBox
- EditText
- Button
那么重点肯定就是在 AppCompat
这些开头的控件了,随便打开一个源码吧,比如 AppCompatTextView
。
public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
this.mBackgroundTintHelper = new AppCompatBackgroundHelper(this);
this.mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
this.mTextHelper = new AppCompatTextHelper(this);
this.mTextHelper.loadFromAttributes(attrs, defStyleAttr);
this.mTextHelper.applyCompoundDrawablesTints();
}
可以看到,关键是 super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
这行代码。我们点进去看看这个 wrap()
做了什么。
public static Context wrap(@NonNull Context context) {
if (shouldWrap(context)) {
Object var1 = CACHE_LOCK;
synchronized(CACHE_LOCK) {
if (sCache == null) {
sCache = new ArrayList();
} else {
int i;
WeakReference ref;
for(i = sCache.size() - 1; i >= 0; --i) {
ref = (WeakReference)sCache.get(i);
if (ref == null || ref.get() == null) {
sCache.remove(i);
}
}
for(i = sCache.size() - 1; i >= 0; --i) {
ref = (WeakReference)sCache.get(i);
TintContextWrapper wrapper = ref != null ? (TintContextWrapper)ref.get() : null;
if (wrapper != null && wrapper.getBaseContext() == context) {
return wrapper;
}
}
}
TintContextWrapper wrapper = new TintContextWrapper(context);
sCache.add(new WeakReference(wrapper));
return wrapper;
}
} else {
return context;
}
}
可以看到当,shouldWrap()
这个方法返回为 true 的时候,就会采用了 TintContextWrapper
这个对象来包裹了我们的 Context
。来看看什么情况才能满足这个条件。
private static boolean shouldWrap(@NonNull Context context) {
if (!(context instanceof TintContextWrapper) && !(context.getResources() instanceof TintResources) && !(context.getResources() instanceof VectorEnabledTintResources)) {
return VERSION.SDK_INT < 21 || VectorEnabledTintResources.shouldBeUsed();
} else {
return false;
}
}
很明显了吧?如果是 5.0 以前,并且没有包装的话,就会直接返回 true;所以也就得出了上面的结论:当运行在 5.0 系统版本以下的手机,并且 Activity
是继承自 AppCompatActivity
的,那么View
的 getConext()
方法,返回的就不是 Activity
而是 TintContextWrapper
。
还有其它情况么?
上面讲述了两种非 Activity
的情况:
- 直接构造
View
的时候传入的不是Activity
; - 使用
AppCompatActivity
并且运行在 5.0 以下的手机上,XML 里面的View
的getContext()
方法返回的是TintContextWrapper
。
那不禁让人想想,还有其他情况么?有。
我们直接从我前两天线上灰测包出现的一个 bug 说起。先说说 bug 背景,灰测包是 9.5.0,而线上包是 9.4.0,在灰测包上发生崩溃的代码是三个月前编写的代码,也就是说这可能是 8.43.0 或者 9.0.0 加入的代码,在线上稳定运行了 4 个版本以上没有做过任何修改。但在 9.5.0 灰测的时候,这里却出现了必现崩溃。
Fatal Exception: java.lang.ClassCastException: android.view.ContextThemeWrapper cannot be cast to android.app.Activity
at com.codoon.common.dialog.CommonDialog.openProgressDialog + 145(CommonDialog.java:145)
at com.codoon.common.dialog.CommonDialog.openProgressDialog + 122(CommonDialog.java:122)
at com.codoon.common.dialog.CommonDialog.openProgressDialog + 116(CommonDialog.java:116)
at com.codoon.find.product.item.detail.i$a.onClick + 57(ProductReceiveCouponItem.kt:57)
at android.view.View.performClick + 6266(View.java:6266)
at android.view.View$PerformClick.run + 24730(View.java:24730)
at android.os.Handler.handleCallback + 789(Handler.java:789)
at android.os.Handler.dispatchMessage + 98(Handler.java:98)
at android.os.Looper.loop + 171(Looper.java:171)
at android.app.ActivityThread.main + 6699(ActivityThread.java:6699)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run + 246(Zygote.java:246)
at com.android.internal.os.ZygoteInit.main + 783(ZygoteInit.java:783)
单看崩溃日志应该非常好改吧,出现了一个强转错误,原来是在我编写的 ProductReceiveCouponItem
类的 57 行调用项目中的通用对话框 CommonDialog
直接崩溃了。翻看 CommonDialog
的相关代码发现,原来是之前的同学在使用传入的 Context
的时候没有做类型验证,直接强转为了 Activity
。
// 得到等待对话框
public void openProgressDialog(String message, OnDismissListener listener, OnCancelListener mOnCancelistener) {
if (waitingDialog != null) {
waitingDialog.dismiss();
waitingDialog = null;
}
if (mContext == null) {
return;
}
if (((Activity) mContext).isFinishing()) {
return;
}
waitingDialog = createLoadingDialog(mContext, message);
waitingDialog.setCanceledOnTouchOutside(false);
waitingDialog.setOnCancelListener(mOnCancelistener);
waitingDialog.setCancelable(mCancel);
waitingDialog.setOnDismissListener(listener);
waitingDialog.show();
}
而我的代码通过 View.getContext()
传入的 Context
类型是 ContextThemeWrapper
。
// 领取优惠券
val dialog = CommonDialog(binding.root.context)
dialog.openProgressDialog("领取中...") // 第 57 行出问题的代码
ProductService.INSTANCE.receiveGoodsCoupon(data.class_id)
.compose(RetrofitUtil.schedulersAndGetData())
.subscribeNet(true) {
// 逻辑处理相关代码
}
看到了日志改起来就非常简单了,第一种方案是直接在 CommonDialog
强转前做一下类型判断。第二种方案是直接在我这里的代码中通过判断 binding.root.context
的类型,然后取出里面的 Activity
。
虽然 bug 非常好解决,但作为一名 Android 程序员,绝对不可以满足于仅仅解决 bug 上,任何事情都事出有因,这里为什么数月没有更改的代码,在 9.4.0 上没有问题,在 9.5.0 上就成了必现崩溃呢?
切换代码分支到 9.4.0,debug 发现,这里的 binding.root.context
返回的确实就是 Activity
,而在 9.5.0 上 binding.root.context
确实就返回的是 ContextThemeWrapper
,检查后确定代码没有任何改动。
分析出现 ContextThemeWrapper 的原因
看到 ContextThemeWrapper
,不由得想起了这个类使用的地方之一:Dialog
,熟悉 Dialog
的童鞋一定都知道,我们在构造 Dialog
的时候,会把 Context
直接变成 ContextThemeWrapper
。
public Dialog(@NonNull Context context) {
this(context, 0, true);
}
public Dialog(@NonNull Context context, @StyleRes int themeResId) {
this(context, themeResId, true);
}
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {
if (themeResId == ResourceId.ID_NULL) {
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
oh,在第三个构造方法中,通过构造的时候传入的 createContextThemeWrapper
总是 true
,所以它一定可以进到这个 if
语句里面去,把 mContext
强行指向了 Context
的包装类 ContextThemeWrapper
。所以这里会不会是由于这个原因呢?
我们再看看我们的代码,我这个 ProductReceiveCouponItem
实际上是一个 RecyclerView
的 Item,而这个相应的 RecyclerView
是显示在 DialogFragment
上的。熟悉 DialogFragment
的小伙伴可能知道,DialogFragment
实际上也是一个 Fragment
。而 DialogFragment
里面,其实是有一个 Dialog
的变量 mDialog
的,这个 Dialog
会在 onStart()
后通过 show()
展示出来。
在我们使用 DialogFragment
的时候,一定都会重写 onCreatView()
对吧,有一个 LayoutInflater
参数,返回值是一个 View
,我们不禁想知道这个 LayoutInflater
是从哪儿来的? onGetLayoutInflater()
,我们看看。
@Override
public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) {
if (!mShowsDialog) {
return super.onGetLayoutInflater(savedInstanceState);
}
mDialog = onCreateDialog(savedInstanceState);
if (mDialog != null) {
setupDialog(mDialog, mStyle);
return (LayoutInflater) mDialog.getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
return (LayoutInflater) mHost.getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
我们是以一个 Dialog
的形式展示,所以不会进入其中的 if
条件。所以我们直接通过了 onCreateDialog()
构造了一个 Dialog
。如果这个 Dialog
不为空的话,那么我们的 LayoutInflater
就会直接通过 Dialog
的 Context
构造出来。我们来看看 onCreateDialog()
方法。
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new Dialog(getActivity(), getTheme());
}
很简单,直接 new
了一个 Dialog
,Dialog
这样的构造方法上面也说了,直接会把 mContext
指向一个 Context
的包装类 ContextThemeWrapper
。
至此我们能做大概猜想了,DialogFragment
负责 inflate
出布局的 LayoutInflater
是由 ContextThemeWrapper
构造出来的,所以我们暂且在这里说一个结论:DialogFragment onCreatView() 里面这个 layout 文件里面的 View.getContext() 返回应该是 `ContextThemeWrapper。
但是!!!我们出问题的是 Item,Item 是通过 RecyclerView
的 Adapter
的 ViewHolder
显示出来的,而非 DialogFragent
里面 Dialog
的 setContentView()
的 XML 解析方法。看起来,分析了那么多,并没有找到问题的症结所在。所以得看看我们的 Adapter
是怎么写的,直接打开我们的 MultiTypeAdapter
的 onCreateViewHolder()
方法。
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (typeMap.get(viewType, TYPE_DEFAULT) == TYPE_ONE) {
return holders.get(viewType).createHolder(parent);
}
ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), viewType, parent, false);
return new ItemViewHolder(binding);
}
oh,在这里我们的 LayoutInflater.from()
接受的参数是 parent.getContext()
。parent
是什么?就是我们的 RecyclerView
,这个 RecyclerView
是从哪儿来的?通过 DialogFragment
的 LayoutInflater
给 inflate
出来的。所以 parent.getContext()
返回是什么?在这里,一定是 ContextThemeWrapper
。
也就是说,我们的 ViewHolder
的 rootView
也就是通过 ContextThemeWrapper
构造的 LayoutInflater
给 inflate
出来的了。所以我们的 ProductReceiveCouponItem
这个 Item 里面的 binding.root.context
返回值,自然也就是 ContextThemeWrapper
而不是 Activity
了。自然而然,在 CommonDialog
里面直接强转为 Activity
一定会出错。
那为什么在 9.4.0 上没有出现这个问题呢?我们看看 9.4.0 上 MultiTypeAdapter
的 onCreateViewHolder()
方法:
@Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ViewDataBinding binding = DataBindingUtil.inflate(mInflater, viewType, parent, false);
return new ItemViewHolder(binding);
}
咦,看起来似乎不一样,这里直接传入的是 mInflater
,我们看看这个 mInflater
是在哪儿被初始化的。
public MultiTypeAdapter(Context context) {
mInflater = LayoutInflater.from(context);
}
oh,在 9.4.0 的分支上,我们的 ViewHolder
的 LayoutInflater
的 Context
,是从外面传进来的。再看看我们 DialogFragment
中对 RecyclerView
的处理。
val rvAdapter = MultiTypeAdapter(context)
binding.recyclerView.run {
layoutManager = LinearLayoutManager(context)
val itemDecoration = DividerItemDecoration(context, DividerItemDecoration.VERTICAL_LIST)
itemDecoration.setDividerDrawable(R.drawable.list_divider_10_white.toDrawable())
addItemDecoration(itemDecoration)
adapter = rvAdapter
}
是吧,在 9.4.0 的时候,MultiTypeAdapter
的 ViewHolder
会使用外界传入的 Context
,这个 Context
是 Activity
,所以我们的Item 的 binding.root.context
返回为 Activity
。而在 9.5.0 的时候,同事重构了 MultiTypeAdapter
,而让其 ViewHolder
的 LayoutInflater
直接取的 parent.getContext()
,这里的情况即 ContextThemeWrapper
,所以出现了几个月没动的代码,在新版本上灰测却崩溃了。
总结
写了这么多,还是做一些总结。首先对题目做个答案: View.getContext() 的返回不一定是 Activity。
实际上,View.getContext()
和 inflate
这个 View
的 LayoutInflater
息息相关,比如 Activity
的 setContentView()
里面的 LayoutInflater
就是它本身,所以该 layoutRes
里面的 View.getContext()
返回的就是 Activity
。但在使用 AppCompatActivity
的时候,值得关注的是, layoutRes
里面的原生 View
会被自动转换为 AppCompatXXX
,而这个转换在 5.0 以下的手机系统中,会把 Context
转换为其包装类 TintThemeWrapper
,所以在这样的情况下的 View.getContext()
返回是 TintThemeWrapper
。
最后,从一个奇怪的 bug 中,给大家分享了一个简单的原因探索分析,也进一步验证了上面的结论。任何 bug 的出现,总是有它的原因,作为 Android 开发,我们不仅要处理掉 bug,更要关注到它的更深层次的原因,这样才能在代码层面就发现其它的潜在问题,以免带来更多不必要的麻烦。本文就一个简单的示例进行了此次试探的讲解,但个人技术能力有限,唯恐出现纰漏,还望有心人士指出。
文章部分来源于:View.getContext() 里的小秘密
每日一问:View.getContext() 的返回一定是 Activity 么?的更多相关文章
- 每日一问:简述 View 的绘制流程
Android 开发中经常需要用一些自定义 View 去满足产品和设计的脑洞,所以 View 的绘制流程至关重要.网上目前有非常多这方面的资料,但最好的方式还是直接跟着源码进行解读,每日一问系列一直追 ...
- 每日一问:Android 消息机制,我有必要再讲一次!
坚持原创日更,短平快的 Android 进阶系列,敬请直接在微信公众号搜索:nanchen,直接关注并设为星标,精彩不容错过. 我 17 年的 面试系列,曾写过一篇名为:Android 面试(五):探 ...
- 每日一问:谈谈 volatile 关键字
这是 wanAndroid 每日一问中的一道题,下面我们来尝试解答一下. 讲讲并发专题 volatile,synchronize,CAS,happens before, lost wake up 为了 ...
- 每日一问:LayoutParams 你知道多少?
前面的文章中着重讲解了 View 的测量流程.其中我提到了一句非常重要的话:View 的测量匡高是由父控件的 MeasureSpec 和 View 自身的 `LayoutParams 共同决定的.我们 ...
- 每日一问:Android 滑动冲突,你们都是怎样处理的
坚持原创日更,短平快的 Android 进阶系列,敬请直接在微信公众号搜索:nanchen,直接关注并设为星标,精彩不容错过. 在 Android 开发中,滑动冲突总是我们一个无法避免的话题.而对于解 ...
- 每日一问:讲讲 Java 虚拟机的垃圾回收
昨天我们用比较精简的文字讲了 Java 虚拟机结构,没看过的可以直接从这里查看: 每日一问:你了解 Java 虚拟机结构么? 今天我们必须来看看 Java 虚拟机的垃圾回收算法是怎样的.不过在开始之前 ...
- 每日一问:你了解 Java 虚拟机结构么?
对于从事 C/C++ 程序员开发的小伙伴来说,在内存管理领域非常头疼,因为他们总是需要对每一个 new 操作去写配对的 delete/free 代码.而对于我们 Android 乃至 Java 程序员 ...
- 自定义Dialog以及Dialog返回值到Activity
步骤: 1.定义自定义的Dialog的布局文件 2.写一个类MyDialog继承Dialog 3.Dialog 返回值到Activity的方法是定义一个接口,接口中定义返回值到Activity的方法, ...
- android 16 带返回值的activity
main.xml <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" andro ...
随机推荐
- TP5.0中使用trace调试
1.在项目 的配置文件config.php 配置, 2.在程序中使用trace: 3.在浏览器网页上打开 得到如下图所示:点击 “用户变量”,即可查看使用trace输出的变量 或者我们使用 trace ...
- 子函数内malloc分配内存,论如何改变指针参数所指内存,二级指针、三级指针的应用
工作中优化一段代码,代码中有一大段分配堆内存的内容,我觉得这段代码太长了,更适合放在子函数里面. 我把指针作为参数,然后在子函数中malloc分配内存,结果出现了问题,函数结束后,以参数传进来的指针并 ...
- ASP.NET MVC控制器Controller
控制器的定义 MVC模式下的控制器(Controller)主要负责响应用户的输入,并且在响应时可能的修改模型(Model). 之前的URL访问,通常是通过指定服务器的路径来实现,如访问URL:http ...
- flask相关使用
一.手动创建一个干净的含有蓝图的flask项目目录 在init.py中 from flask import Flaskmy_app=Flask(__name__)def create_app() ...
- Android短视频中如何实现720P磨皮美颜录制?
视频中磨皮.美颜功能已成为刚需,那么如何在Android短视频中实现720P磨皮美颜录制?本篇文章中,网易云信资深开发工程师将向大家介绍具体的操作方法. 相关阅读推荐 <短视频技术详解:Andr ...
- Flink UDF
本文会主要讲三种udf: ScalarFunction TableFunction AggregateFunction 用户自定义函数是非常重要的一个特征,因为他极大地扩展了查询的表达能力.本文除了介 ...
- Angular4.0从入门到实战打造在线竞拍网站学习笔记之一--组件
Angular4.0基础知识之组件 Angular4.0基础知识之路由 Angular4.0依赖注入 Angular4.0数据绑定&管道 最近搞到手了一部Angular4的视频教程,这几天正好 ...
- Azkaban学习之路(二)—— Azkaban 3.x 编译及部署
一.Azkaban 源码编译 1.1 下载并解压 Azkaban 在3.0版本之后就不提供对应的安装包,需要自己下载源码进行编译. 下载所需版本的源码,Azkaban的源码托管在GitHub上,地址为 ...
- Null作为参数的时候,Java编译器如何调用函数?
public class TestNull { public void method(Object o){ System.out.println("Object Version") ...
- 深度探索c++对象模型 第一章
1,声明与定义. //声明式如下: extern int x; //对象式(变量式)声明 std::size_t numDigits(int number); //函数式声明 class Wid ...