随着B站逐渐崛起,其开源弹幕项目DanmakuFlameMaster应用场景也越来越多。我也是在一次偶然机会下发现了这个项目,被其惊艳的效果震撼。以前我就对弹幕技术很感兴趣,可能是因为B站动漫看多,几乎每一部番都是漫天的弹幕乱飞,如果哪部剧没有弹幕反而觉得不适应;久而久之就愈发倾向钻研其原理。
看到效果后,我猜想绘制原理应该是创建一个定时器作为全部弹幕的时间参考,然后每条弹幕出现的位置都以这个定时器去计算x、y值,然后定时任务定期postInvalidate,弹幕画布重新绘制onDraw;弹幕如此之多,应该有缓存机制,也许建立了一个弹幕池让出现过的弹幕缓存起来,新弹幕可以复用旧弹幕item。
先这么假设吧,然后验证我们的猜想,看看有哪些坑。
基本使用
首先是添加控件,项目里提供了三个控件:DanmakuSurfaceView、DanmakuTextureView和DanmakuView,使用其中三个任意一个都可以。我们选个DanmakuView方便分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
"http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> 省略一些布局... android:id="@+id/sv_danmaku" android:layout_width="match_parent" android:layout_height="match_parent" /> 省略一些布局...
|
然后是代码配置,先看一下初始化相关:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
@Override protectedvoidonCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViews(); } privatevoidfindViews(){
省略一些代码...
// DanmakuView mDanmakuView = (IDanmakuView) findViewById(R.id.sv_danmaku); // 设置最大显示行数 HashMap maxLinesPair = new HashMap(); maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL, 5); // 滚动弹幕最大显示5行 // 设置是否禁止重叠 HashMap overlappingEnablePair = new HashMap(); overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_RL, true); overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_TOP, true); //创建弹幕控件上下文,类似Context,里面可以进行一系列配置 mContext = DanmakuContext.create(); mContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 3)//设置描边样式 .setDuplicateMergingEnabled(false) //设置不合并相同内容弹幕 .setScrollSpeedFactor(1.2f) //设置弹幕滚动速度缩放比例,越大速度越慢 .setScaleTextSize(1.2f) //设置字体缩放比例 .setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter) // 图文混排使用SpannedCacheStuffer //.setCacheStuffer(new BackgroundCacheStuffer()) // 绘制背景使用BackgroundCacheStuffer .setMaximumLines(maxLinesPair) //设置最大行数策略 .preventOverlapping(overlappingEnablePair); //设置禁止重叠策略 省略一些代码...
} |
DanmakuContext设置setCacheStuffer(CacheStuffer, Proxy)时,如果不设置此方法,则CacheStuffer默认为SimpleTextCacheStuffer,proxy默认为null;第一个参数,项目例子中提供了BackgroundCacheStuffer和SpannedCacheStuffer,其实也可以自己扩展,第二个参数例子中也写了一个mCacheStufferAdapter,同理也可以自己扩展。这个sample中注释也写得比较明确,我们往下分析原理时会解释。
然后设置数据源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
//替换为A站弹幕数据源,因为A站弹幕数据是json,B站是xml,为了方便分析因此替换为A站源 //mParser = createParser(this.getResources().openRawResource(R.raw.comments)); try { mParser = createParser(this.getAssets().open("comment.json")); } catch (IOException e) { e.printStackTrace(); }
private BaseDanmakuParser createParser(InputStream stream){
if (stream == null) { returnnew BaseDanmakuParser() {
@Override protected Danmakus parse(){ returnnew Danmakus(); } }; }
// ILoader loader = DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_BILI); ILoader loader = DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_ACFUN);
try { loader.load(stream); } catch (IllegalDataException e) { e.printStackTrace(); } // BaseDanmakuParser parser = new BiliDanmukuParser(); BaseDanmakuParser parser = new AcFunDanmakuParser(); IDataSource> dataSource = loader.getDataSource(); parser.load(dataSource); return parser;
} |
最后启动弹幕:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//设置弹幕view相关回调 mDanmakuView.setCallback(new DrawHandler.Callback() { @Override publicvoidupdateTimer(DanmakuTimer timer){ }
@Override publicvoiddrawingFinished(){
}
@Override publicvoiddanmakuShown(BaseDanmaku danmaku){ //Log.d("DFM", "danmakuShown(): text=" + danmaku.text); }
@Override publicvoidprepared(){ Log.d("DFM", "MainActivity inline callback's method prepared"); mDanmakuView.start(); } }); mDanmakuView.prepare(mParser, mContext); mDanmakuView.showFPS(true); mDanmakuView.enableDanmakuDrawingCache(true); |
基本使用在项目的例子中都写的很清楚,这些应该难度不大。接下来应该是分析流程了。
流程分析
DanmakuFlameMaster流程确实十分复杂,因为变量实在太多了,所以分析时推荐先整体看个大概,然后一步一步打断点确认细节。
初始配置
上面写基本使用法,第一步是初始配置,我们看看到底初始化了哪些参数。对比上面的调用顺序,首先进入DanmakuContext看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//相关配置如下,主要初始化一下变量 mContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 3)//设置描边样式 .setDuplicateMergingEnabled(false) //设置不合并相同内容弹幕 .setScrollSpeedFactor(1.2f) //设置弹幕滚动速度缩放比例,越大速度越慢 .setScaleTextSize(1.2f) //设置字体缩放比例 .setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter) // 图文混排使用SpannedCacheStuffer //.setCacheStuffer(new BackgroundCacheStuffer()) // 绘制背景使用BackgroundCacheStuffer .setMaximumLines(maxLinesPair) //设置最大行数策略 .preventOverlapping(overlappingEnablePair); //设置禁止重叠策略
//DanmakuContext 类重要方法 /*------------DanmakuContext STAET-----------*/ privatefinal AbsDisplayer mDisplayer = new AndroidDisplayer();//创建DanmakuContext 对象时直接new了个mDisplayer 全局变量 /** * 设置缓存绘制填充器,默认使用SimpleTextCacheStuffer只支持纯文字显示, 如果需要图文混排请设置SpannedCacheStuffer * 如果需要定制其他样式请扩展SimpleTextCacheStuffer或者SpannedCacheStuffer */ public DanmakuContext setCacheStuffer(BaseCacheStuffer cacheStuffer, BaseCacheStuffer.Proxy cacheStufferAdapter){ this.mCacheStuffer = cacheStuffer; if (this.mCacheStuffer != null) { this.mCacheStuffer.setProxy(cacheStufferAdapter); mDisplayer.setCacheStuffer(this.mCacheStuffer); } returnthis; }
/*------------DanmakuContext END-----------*/ |
以上配置主要配置一些常规参数,记不住也没关系,我们可以打断点一一查看。
加载资源
然后就是加载弹幕源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
private BaseDanmakuParser createParser(InputStream stream){ ......
//创建A站弹幕加载器 ILoader loader = DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_ACFUN); try { //将数据流载入加载器里 loader.load(stream); } catch (IllegalDataException e) { e.printStackTrace(); } //创建弹幕解析器 BaseDanmakuParser parser = new AcFunDanmakuParser(); //取出数据源 IDataSource> dataSource = loader.getDataSource(); //解析器放入数据源 parser.load(dataSource); return parser;
} |
我们一步一步来,先创建A站弹幕加载器:
1 2 3 4 5 6 7 8 |
//根据不同标签创建不同加载器,可以根据不同业务自己扩展定制 publicstatic ILoader create(String tag){ if (TAG_BILI.equalsIgnoreCase(tag)) { return BiliDanmakuLoader.instance(); } elseif(TAG_ACFUN.equalsIgnoreCase(tag))//我们到了这里 return AcFunDanmakuLoader.instance(); returnnull; } |
载入数据流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
publicvoidload(InputStream in)throws IllegalDataException { try { dataSource = new JSONSource(in);//这里创建了一个JSONSource } catch (Exception e) { thrownew IllegalDataException(e); } } //JSONSource构造方法 publicJSONSource(InputStream in)throws JSONException{ init(in); } privatevoidinit(InputStream in)throws JSONException { ...... mInput = in; String json = IOUtils.getString(mInput);//将流转成字符串 init(json); } privatevoidinit(String json)throws JSONException { if(!TextUtils.isEmpty(json)){ mJSONArray = new JSONArray(json);//将json字符串保存到一个JSONArray全局变量 } } //取出JSONSource public JSONSource getDataSource(){ return dataSource; } |
载入数据流就是读取弹幕数据文件流,然后转成字符串,最后保存到一个JSONArray变量里存起来。
继续往下分析创建弹幕解析器、取出数据源、解析器放入数据源:
1 2 3 4 5 |
//AcFunDanmakuParser的load方法,将上一步得到的JSONSource放入到AcFunDanmakuParser中 public BaseDanmakuParser load(IDataSource> source){ mDataSource = source; returnthis; } |
到这里数据就载入到解析器里了,parser里有弹幕源数据了。
启动弹幕
启动弹幕重要的就是这一句:
1 |
mDanmakuView.prepare(mParser, mContext); |
此时mParser和mContext都已经初始化完成。
1 2 3 4 5 6 7 8 9 10 11 12 |
publicvoidprepare(BaseDanmakuParser parser, DanmakuContext config){ prepare();///创建一个 DrawHandler handler.setConfig(config); handler.setParser(parser); handler.setCallback(mCallback); handler.prepare();//然后调用DrawHandler的prepare方法 } //创建一个 DrawHandler privatevoidprepare(){ if (handler == null) handler = new DrawHandler(getLooper(mDrawingThreadType), this, mDanmakuVisible);//mDanmakuVisible为true } |
设置一些全局变量后,会调用DrawHandler的prepare方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
publicvoidprepare(){ sendEmptyMessage(DrawHandler.PREPARE); } publicvoidhandleMessage(Message msg){ int what = msg.what; switch (what) { case PREPARE: mTimeBase = SystemClock.uptimeMillis(); if (mParser == null || !mDanmakuView.isViewReady()) {// false || false sendEmptyMessageDelayed(PREPARE, 100); } else { prepare(new Runnable() {//会继续调用prepare重载方法 @Override publicvoidrun(){ pausedPosition = 0; mReady = true; if (mCallback != null) { mCallback.prepared(); } } }); } break; ...... } } private DanmakuTimer timer = new DanmakuTimer();//已经初始化timer privatevoidprepare(final Runnable runnable){ if (drawTask == null) {//会继续调用createDrawTask方法 drawTask = createDrawTask(mDanmakuView.isDanmakuDrawingCacheEnabled(), timer, mDanmakuView.getContext(), mDanmakuView.getWidth(), mDanmakuView.getHeight(), mDanmakuView.isHardwareAccelerated(), new IDrawTask.TaskListener() { @Override publicvoidready(){ initRenderingConfigs(); runnable.run(); } ...... }); } else { runnable.run(); } } //继续调用createDrawTask(true, timer, context, width, height, true, listener)方法 private IDrawTask createDrawTask(boolean useDrwaingCache, DanmakuTimer timer, Context context, int width, int height, boolean isHardwareAccelerated, IDrawTask.TaskListener taskListener) { mDisp = mContext.getDisplayer();//AndroidDisplayer赋给它,顾名思义,Displayer就是显示器 mDisp.setSize(width, height);//设置弹幕视图宽高 DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); mDisp.setDensities(displayMetrics.density, displayMetrics.densityDpi, displayMetrics.scaledDensity);//设置密度先关 mDisp.resetSlopPixel(mContext.scaleTextSize);//设置字体缩放比例,之前设过了1.2 mDisp.setHardwareAccelerated(isHardwareAccelerated);//硬件加速,true //useDrwaingCache 为true IDrawTask task = useDrwaingCache ? new CacheManagingDrawTask(timer, mContext, taskListener, 1024 * 1024 * AndroidUtils.getMemoryClass(context) / 3) : new DrawTask(timer, mContext, taskListener); task.setParser(mParser);//把存放数据源的mParser放入CacheManagingDrawTask中 task.prepare();//这个才是重点,调用CacheManagingDrawTask的prepare方法 obtainMessage(NOTIFY_DISP_SIZE_CHANGED, false).sendToTarget(); return task; } |
上述过程最后一个调用了createDrawTask方法,这里先初始化了一下AndroidDisplayer配置,就把他当做显示器吧,我猜ctiao当初设计时也是这么比喻的吧。
设置好弹幕显示相关的参数,然后就是创建绘制任务IDrawTask了。这里有两个选择,如果使用缓存就创建CacheManagingDrawTask,不使用就创建DrawTask。不过CacheManagingDrawTask比DrawTask复杂很多。
CacheManagingDrawTask绘制任务
我们的useDrwaingCache为true(其实把它改为false也没关系,并且这样就用不上那些so库了),则创建CacheManagingDrawTask绘制任务,然后调用prepare方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
publicCacheManagingDrawTask(DanmakuTimer timer, DanmakuContext config, TaskListener taskListener, int maxCacheSize){//传入定时器timer,config,listener,还有三分一应用分配内存大小的maxCacheSize super(timer, config, taskListener);//会调用父类DrawTask的构造方法 NativeBitmapFactory.loadLibs();//加载so库,用于创建bitmap,同时测试时候加载成功 mMaxCacheSize = maxCacheSize; if (NativeBitmapFactory.isInNativeAlloc()) {//true,将最大内存扩大到2倍 mMaxCacheSize = maxCacheSize * 2; } mCacheManager = new CacheManager(maxCacheSize, MAX_CACHE_SCREEN_SIZE); mRenderer.setCacheManager(mCacheManager); } //看看父类的构造方法 publicDrawTask(DanmakuTimer timer, DanmakuContext context, TaskListener taskListener) { ...... mContext = context; mDisp = context.getDisplayer(); mTaskListener = taskListener; mRenderer = new DanmakuRenderer(context); ...... initTimer(timer);//初始化相关定时器 ...... } protectedvoidinitTimer(DanmakuTimer timer){ mTimer = timer; mCacheTimer = new DanmakuTimer(); mCacheTimer.update(timer.currMillisecond); } |
CacheManagingDrawTask的构造方法设置了一些变量。其中NativeBitmapFactory.loadLibs()加载了用于创建bitmap的so文件,就是用skia图形处理库直接创建bitmap,Android对2D图形处理采用的就是skia,3D图形处理用的是OpenGLES。这样通过native层创建bitmap直接跳过Dalvik,毕竟java层内存用多了很容易oom。因为以前我就对native层比较感兴趣,所以我要任性的跟一遍源码 ^O.O^。为了怕跟完后自己晕了,找不到现在分析的地方了,所以在这里打个标签,mark一下。如不感兴趣,可以跳过= 。=
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//NativeBitmapFactory publicstaticvoidloadLibs(){ ...... System.loadLibrary("ndkbitmap");//载入so ...... //测试功能 if (nativeLibLoaded) { boolean libInit = init();//这是一个native方法 if (!libInit) { release(); notLoadAgain = true; nativeLibLoaded = false; } else {//初始化成功后 initField();//反射Bitmap.Config的nativeInt字段 boolean confirm = testLib();//测试例子 } } Log.e("NativeBitmapFactory", "loaded" + nativeLibLoaded); } //反射Bitmap.Config的nativeInt字段 staticvoidinitField(){ try { nativeIntField = Bitmap.Config.class.getDeclaredField("nativeInt"); nativeIntField.setAccessible(true); } catch (NoSuchFieldException e) { nativeIntField = null; e.printStackTrace(); } } privatestaticnativebooleaninit(); |
这里会调用测试方法testLib:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
privatestaticbooleantestLib(){ if (nativeIntField == null) { returnfalse; } Bitmap bitmap = null; Canvas canvas = null; ...... //用native方法创建一个bitmap bitmap = createNativeBitmap(2, 2, Bitmap.Config.ARGB_8888, true); boolean result = (bitmap != null && bitmap.getWidth() == 2 && bitmap.getHeight() == 2); ...... canvas = new Canvas(bitmap); Paint paint = new Paint(); paint.setColor(Color.RED); paint.setTextSize(20f); canvas.drawRect(0f, 0f, (float) bitmap.getWidth(), (float) bitmap.getHeight(), paint); canvas.drawText("TestLib", 0, 0, paint);
...... return result;
} privatestatic Bitmap createNativeBitmap(int width, int height, Config config, boolean hasAlpha){ int nativeConfig = getNativeConfig(config);//反射设置Bitmap.Config.ARGB_8888 return android.os.Build.VERSION.SDK_INT == 19 ? createBitmap19(width, height, nativeConfig, hasAlpha) : createBitmap(width, height, nativeConfig, hasAlpha); } publicstaticintgetNativeConfig(Bitmap.Config config){ try { if (nativeIntField == null) { return0; } return nativeIntField.getInt(config); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return0; }
privatestaticnativebooleaninit();
privatestaticnativebooleanrelease();
privatestaticnative Bitmap createBitmap(int width, int height, int nativeConfig, boolean hasAlpha);
privatestaticnative Bitmap createBitmap19(int width, int height, int nativeConfig, boolean hasAlpha); |
上述最终用native方法创建bitmap,C++文件地址为 https://github.com/Bilibili/NativeBitmapFactory ,接着继续查看native方法具体实现NativeBitmapFactory.cpp。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//先看java层init方法对应的本地方法 jboolean Java_tv_cjump_jni_NativeBitmapFactory_init(JNIEnv *env) { ...... //继续看Start方法 int r = Start(); return r == SUCCESS; } staticintStart() { //创建一个类型为ndkbitmap_obj 的结构体指针 ndkbitmap_obj = (ndkbitmap_object_t *)malloc(sizeof(*ndkbitmap_obj)); int r = Open(ndkbitmap_obj); ...... return SUCCESS; } staticintOpen(ndkbitmap_object_t *obj) { //创建一个类型为skbitmap_sys_t 的结构体指针 skbitmap_sys_t *sys = (skbitmap_sys_t *)malloc(sizeof (*sys)); ...... //打开libskia.so动态链接库,初始化一些参数并返回动态链接库的句柄 sys->libskia = InitLibrary(sys); ...... //打开libandroid_runtime.so动态链接库,初始化一些参数并返回动态链接库的句柄 sys->libjnigraphics = InitLibrary2(sys); ...... //将初始化过后的结构指针sys赋给结构体obj的sys成员 obj->sys = sys; return SUCCESS; } |
init方法主要是打开和skia相关的动态链接库,并初始化一些配置。(InitLibrary和InitLibrary2方法的细节我没有贴,里面实现需要一些专业知识,有兴趣的可以找资料钻研)然后就是createBitmap:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
jobject Java_tv_cjump_jni_NativeBitmapFactory_createBitmap(JNIEnv *env , jobject obj, jint w, jint h, jint config, jboolean hasAlpha) { return createBitmap(env, obj, w, h, config, hasAlpha, true, 0); }
jobject Java_tv_cjump_jni_NativeBitmapFactory_createBitmap19(JNIEnv *env , jobject obj, jint w, jint h, jint config, jboolean hasAlpha) { return createBitmap(env, obj, w, h, config, hasAlpha, 0x3, 19); }
jobject createBitmap(JNIEnv *env , jobject obj, jint w, jint h, jint config, jboolean hasAlpha, int isMuttable, int api) { void *bm = createSkBitmap(ndkbitmap_obj, config, w, h);//调用重载方法创建bitmap指针 if (bm == NULL) { return NULL; } jobject result = NULL; skbitmap_sys_t *p_sys = ndkbitmap_obj->sys; if(p_sys->libjnigraphics) { if(p_sys->gjni_createBitmap) {//SDK版本小于19 //通过这个函数指针把JNI层bitmap的转换对象return给java层 result = p_sys->gjni_createBitmap(env, bm, isMuttable, NULL, -1); } elseif(p_sys->gjni_createBitmap_19later) {//SDK版本19以后返回值 result = p_sys->gjni_createBitmap_19later(env, bm, NULL, isMuttable, NULL, NULL, -1); }
} return result; } //创建bitmap指针,并通过相关指针函数设置bitmap参数 inline void *createSkBitmap(ndkbitmap_object_t *obj, int config, int w, int h) { skbitmap_sys_t *p_sys = obj->sys; if (p_sys == NULL || p_sys->libskia == NULL) { return NULL; } //申请内存,创建skBitmap 指针 void *skBitmap = malloc(SIZE_OF_SKBITMAP); if (!skBitmap) { return NULL; } *((uint32_t *) ((uint32_t)skBitmap + SIZE_OF_SKBITMAP - 4)) = 0xbaadbaad; //ctor p_sys->sk_ctor(skBitmap); if (p_sys->sk_setConfig) { p_sys->sk_setConfig(skBitmap, config, w, h, 0); } elseif (p_sys->sk_setConfig_19later) { p_sys->sk_setConfig_19later(skBitmap, config, w, h, 0, (uint8_t)kPremul_SkAlphaType); } elseif (p_sys->sk_setInfo) { int imageInfo[4] = {w, h, SkBitmapConfigToColorType(config), kPremul_SkAlphaType}; p_sys->sk_setInfo(skBitmap, imageInfo, 0); } p_sys->sk_allocPixels(skBitmap, NULL, NULL); p_sys->sk_eraseARGB(skBitmap, 0, 0, 0, 0);
if (!(*((uint32_t *) ((uint32_t)skBitmap + SIZE_OF_SKBITMAP - 4)) == 0xbaadbaad) ) { free(skBitmap); return NULL; }
return skBitmap; } |
通过skia图形库创建bitmap流程大概就是这些,其实skia的东西也是巨多无比,如果是从事这一方面工作应该都轻车熟路,我是完全的小白,能力有限,只能先到这儿。
好了,继续回到上次打标签的地方。接着该调用CacheManagingDrawTask的prepare方法:
1 2 3 4 5 |
publicvoidprepare(){ assert (mParser != null); loadDanmakus(mParser); mCacheManager.begin(); } |
先调用loadDanmakus方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
protected IDanmakus danmakuList; protectedvoidloadDanmakus(BaseDanmakuParser parser){ danmakuList = parser.setConfig(mContext) .setDisplayer(mDisp) .setTimer(mTimer) .getDanmakus();//从parser中取出弹幕数据,做出相关处理
......
if(danmakuList != null) { mLastDanmaku = danmakuList.last(); } } |
parser设置完DanmakuContext,AndroidDisplayer,DanmakuTimer之后,再调用getDanmakus取出弹幕信息:
1 2 3 4 5 6 7 8 9 |
public IDanmakus getDanmakus(){ if (mDanmakus != null) return mDanmakus; mContext.mDanmakuFactory.resetDurationsData();//重庆内置一些变量为null mDanmakus = parse();//解析弹幕 releaseDataSource();//关闭JSONSource mContext.mDanmakuFactory.updateMaxDanmakuDuration();//修正弹幕最大时长 return mDanmakus; } |
进入AcFunDanmakuParser的parse方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
public Danmakus parse(){ if (mDataSource != null && mDataSource instanceof JSONSource) { JSONSource jsonSource = (JSONSource) mDataSource; return doParse(jsonSource.data());//go on } returnnew Danmakus(); } private Danmakus doParse(JSONArray danmakuListData){ Danmakus danmakus = new Danmakus(); if (danmakuListData == null || danmakuListData.length() == 0) { return danmakus; } for (int i = 0; i < danmakuListData.length(); i++) { try { JSONObject danmakuArray = danmakuListData.getJSONObject(i); if (danmakuArray != null) { danmakus = _parse(danmakuArray, danmakus);//解析每一条弹幕 } } catch (JSONException e) { e.printStackTrace(); } } return danmakus; } /** * {"c":"19.408,16777215,1,25,178252,1376325904","m":"金刚如来!"} // 0:时间(弹幕出现时间) // 1:颜色 // 2:类型(1从右往左滚动弹幕|6从右至左滚动弹幕|5顶端固定弹幕|4底端固定弹幕|7高级弹幕|8脚本弹幕) // 3:字号 // 4:用户id ? // 5:时间戳 ? */ private Danmakus _parse(JSONObject jsonObject, Danmakus danmakus){ if (danmakus == null) { danmakus = new Danmakus(); } if (jsonObject == null || jsonObject.length() == 0) { return danmakus; } for (int i = 0; i < jsonObject.length(); i++) { try { JSONObject obj = jsonObject; String c = obj.getString("c");//弹幕配置信息 String[] values = c.split(","); if (values.length > 0) { int type = Integer.parseInt(values[2]); // 弹幕类型 if (type == 7) // FIXME : hard code // TODO : parse advance danmaku json continue; long time = (long) (Float.parseFloat(values[0]) * 1000); // 出现时间 int color = Integer.parseInt(values[1]) | 0xFF000000; // 颜色 float textSize = Float.parseFloat(values[3]); // 字体大小 //使用弹幕工厂创建一条弹幕 BaseDanmaku item = mContext.mDanmakuFactory.createDanmaku(type, mContext); if (item != null) { item.time = time; item.textSize = textSize * (mDispDensity - 0.6f); item.textColor = color; item.textShadowColor = color <= Color.BLACK ? Color.WHITE : Color.BLACK; //弹幕文字内容,如果多行文本会拆分内容 DanmakuUtils.fillText(item, obj.optString("m", "....")); item.index = i; item.setTimer(mTimer);//将定时器设置给每一条弹幕 danmakus.addItem(item); } } } catch (JSONException e) { } catch (NumberFormatException e) { } } return danmakus; } //DanmakuUtilsdefillText方法,多行文本会拆分 publicstaticvoidfillText(BaseDanmaku danmaku, CharSequence text){ danmaku.text = text; //如果文本没有换行符则不用拆分 if (TextUtils.isEmpty(text) || !text.toString().contains(BaseDanmaku.DANMAKU_BR_CHAR)) { return; } //如果有换行符则拆分,然后将拆分的数组付给lines 属性 String[] lines = String.valueOf(danmaku.text).split(BaseDanmaku.DANMAKU_BR_CHAR, -1); if (lines.length > 1) { danmaku.lines = lines; } } } |
从JSONSource里解析每一条弹幕,接着我们看看弹幕工厂DanmakuFactory创建弹幕的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
public BaseDanmaku createDanmaku(int type, DanmakuContext context){ if (context == null) returnnull; sLastConfig = context; sLastDisp = context.getDisplayer(); return createDanmaku(type, sLastDisp.getWidth(), sLastDisp.getHeight(), CURRENT_DISP_SIZE_FACTOR, context.scrollSpeedFactor);// go on overload method } public BaseDanmaku createDanmaku(int type, int viewportWidth, int viewportHeight, float viewportScale, float scrollSpeedFactor) { return createDanmaku(type, (float) viewportWidth, (float) viewportHeight, viewportScale, scrollSpeedFactor); } public BaseDanmaku createDanmaku(int type, float viewportWidth, float viewportHeight, float viewportSizeFactor, float scrollSpeedFactor) { int oldDispWidth = CURRENT_DISP_WIDTH; // 默认是0 int oldDispHeight = CURRENT_DISP_HEIGHT; // 默认是0 //修正试图宽高,缩放比,弹幕时长 boolean sizeChanged = updateViewportState(viewportWidth, viewportHeight, viewportSizeFactor); //滚动弹幕的Duration赋值 if (MAX_Duration_Scroll_Danmaku == null) { MAX_Duration_Scroll_Danmaku = new Duration(REAL_DANMAKU_DURATION); MAX_Duration_Scroll_Danmaku.setFactor(scrollSpeedFactor); } elseif (sizeChanged) { MAX_Duration_Scroll_Danmaku.setValue(REAL_DANMAKU_DURATION); } //固定位置弹幕的Duration赋值 if (MAX_Duration_Fix_Danmaku == null) { MAX_Duration_Fix_Danmaku = new Duration(COMMON_DANMAKU_DURATION); } if (sizeChanged && viewportWidth > 0) {// true && true updateMaxDanmakuDuration();// 修正弹幕最长时长 ...... }
BaseDanmaku instance = null; switch (type) { case1: // 从右往左滚动 instance = new R2LDanmaku(MAX_Duration_Scroll_Danmaku); break; case4: // 底端固定 instance = new FBDanmaku(MAX_Duration_Fix_Danmaku); break; case5: // 顶端固定 instance = new FTDanmaku(MAX_Duration_Fix_Danmaku); break; case6: // 从左往右滚动 instance = new L2RDanmaku(MAX_Duration_Scroll_Danmaku); break; case7: // 特殊弹幕 instance = new SpecialDanmaku(); sSpecialDanmakus.addItem(instance); break; } return instance; } //修正试图宽高,缩放比,弹幕时长 publicbooleanupdateViewportState(float viewportWidth, float viewportHeight, float viewportSizeFactor) { boolean sizeChanged = false; if (CURRENT_DISP_WIDTH != (int) viewportWidth || CURRENT_DISP_HEIGHT != (int) viewportHeight || CURRENT_DISP_SIZE_FACTOR != viewportSizeFactor) { sizeChanged = true; //弹幕时长 t = 3800 * (1.2 * 视图宽 / 682) REAL_DANMAKU_DURATION = (long) (COMMON_DANMAKU_DURATION * (viewportSizeFactor * viewportWidth / BILI_PLAYER_WIDTH)); // t = min(t, 9000) REAL_DANMAKU_DURATION = Math.min(MAX_DANMAKU_DURATION_HIGH_DENSITY, REAL_DANMAKU_DURATION); // t = max(t, 4000) REAL_DANMAKU_DURATION = Math.max(MIN_DANMAKU_DURATION, REAL_DANMAKU_DURATION); CURRENT_DISP_WIDTH = (int) viewportWidth; CURRENT_DISP_HEIGHT = (int) viewportHeight; CURRENT_DISP_SIZE_FACTOR = viewportSizeFactor; } return sizeChanged; } //修正弹幕最长时长 publicvoidupdateMaxDanmakuDuration(){ long maxScrollDuration = (MAX_Duration_Scroll_Danmaku == null ? 0: MAX_Duration_Scroll_Danmaku.value), maxFixDuration = (MAX_Duration_Fix_Danmaku == null ? 0 : MAX_Duration_Fix_Danmaku.value), maxSpecialDuration = (MAX_Duration_Special_Danmaku == null ? 0: MAX_Duration_Special_Danmaku.value);
MAX_DANMAKU_DURATION = Math.max(maxScrollDuration, maxFixDuration); MAX_DANMAKU_DURATION = Math.max(MAX_DANMAKU_DURATION, maxSpecialDuration);
MAX_DANMAKU_DURATION = Math.max(COMMON_DANMAKU_DURATION, MAX_DANMAKU_DURATION); MAX_DANMAKU_DURATION = Math.max(REAL_DANMAKU_DURATION, MAX_DANMAKU_DURATION); } |
DanmakuFactory创建弹幕主要是计算了弹幕时长,然后根据不同类型创建不同的弹幕。
到此CacheManagingDrawTask的loadDanmakus方法走完了。loadDanmakus方法主要从 mParser里的JSONSource解析弹幕数据源,根据不同类型的type用DanmakuFactory创建不同的Danmaku,分别计算Duration,最后存放到一个Danmakus对象里。
继续回到刚才的prepare方法,往下继续执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@Override publicvoidprepare(){ assert (mParser != null); loadDanmakus(mParser);//走完了 mCacheManager.begin();//走这个 } //CacheManager的方法 publicvoidbegin(){ mEndFlag = false; //创建一个HandlerThread用于在工作线程处理事务 if (mThread == null) { mThread = new HandlerThread("DFM Cache-Building Thread"); mThread.start(); } //创建一个Handler和HandlerThread搭配用 if (mHandler == null) mHandler = new CacheHandler(mThread.getLooper()); mHandler.begin();// 走到这里 } //HandlerThread的begin方法 publicvoidbegin(){ sendEmptyMessage(PREPARE); ...... } |
我们可以看到创建了一个HandlerThread,然后创建了一个CacheHandler,所以CacheHandler发送消息后,处理消息内容都是在子线程。
然后发送了PREPARE消息,然后就是回调handleMessage方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
DrawingCachePoolManager mCachePoolManager = new DrawingCachePoolManager(); //创建一个缓存个数上限为800的FinitePool池 Pool mCachePool = Pools.finitePool(mCachePoolManager, 800); //Pools的finitePool方法 publicstatic > Pool finitePool(PoolableManager manager, int limit){ returnnew FinitePool(manager, limit); } //CacheHandler的handleMessage方法 publicvoidhandleMessage(Message msg){ int what = msg.what; switch (what) { case PREPARE: evictAllNotInScreen();//清除所有不在屏幕内的缓存,此时还没有缓存 for (int i = 0; i < 300; i++) {//在池里放300个预留缓存,以链式存储方式存放 mCachePool.release(new DrawingCache()); }
...... } } //FinitePool的release方法:回收缓存对象,并且用头插法,以链式存储(类似链表) publicvoidrelease(T element){ if (!element.isPooled()) { if (mInfinite || mPoolCount < mLimit) { mPoolCount++; element.setNextPoolable(mRoot); element.setPooled(true); mRoot = element; } mManager.onReleased(element); } else { System.out.print("[FinitePool] Element is already in pool: " + element); } } |
处理完PREPARE消息后,会继续进入DISPATCH_ACTIONS逻辑处理中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
...... case DISPATCH_ACTIONS: long delayed = dispatchAction();//走到这里 if (delayed <= 0) {// true //会没隔半条弹幕时间发送一次DISPATCH_ACTIONS消息 delayed = mContext.mDanmakuFactory.MAX_DANMAKU_DURATION / 2; } sendEmptyMessageDelayed(DISPATCH_ACTIONS, delayed); break; ...... /*----------dispatchAction方法START----------*/ privatelongdispatchAction(){ ...省略一些第一次不会执行的逻辑... removeMessages(BUILD_CACHES); sendEmptyMessage(BUILD_CACHES);//发送BUILD_CACHES消息 return0; } /*----------dispatchAction方法END----------*/ ...... case BUILD_CACHES: removeMessages(BUILD_CACHES); boolean repositioned = ((mTaskListener != null && mReadyState == false) || mSeekedFlag);// 为true prepareCaches(repositioned);//调用prepareCaches方法 if (repositioned) mSeekedFlag = false; if (mTaskListener != null && mReadyState == false) { mTaskListener.ready();//然后回到mTaskListener监听ready方法 mReadyState = true; } break; ...... |
我们发现,处理接着处理DISPATCH_ACTIONS消息时,会每隔半条弹幕时间发送一次DISPATCH_ACTIONS消息。
处理DISPATCH_ACTIONS消息内会执行dispatchAction方法,这个方法内逻辑情况比较多,我们先挖个坑,先把刚开始时会走的逻辑执行了,其他逻辑以后用时会填上。(挖坑 ^O_O^)
首次调用dispatchAction方法内发送了BUILD_CACHES消息消息,会先调用prepareCaches(true)方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
privatelongprepareCaches(boolean repositioned){ long curr = mCacheTimer.currMillisecond;// 0 //3条弹幕时间 long end = curr + mContext.mDanmakuFactory.MAX_DANMAKU_DURATION * mScreenSize; if (end < mTimer.currMillisecond) { return0; } long startTime = SystemClock.uptimeMillis(); IDanmakus danmakus = null; int tryCount = 0; boolean hasException = false; do { try { //截取三条弹幕时间中所有的弹幕 danmakus = danmakuList.subnew(curr, end); } catch (Exception e) { hasException = true; SystemClock.sleep(10); } } while (++tryCount < 3 && danmakus == null && hasException);//截取成功后跳出循环 if (danmakus == null) { mCacheTimer.update(end); return0; } ...... IDanmakuIterator itr = danmakus.iterator(); BaseDanmaku item = null; int sizeInScreen = danmakus.size(); while (!mPause && !mCancelFlag) {// boolean hasNext = itr.hasNext(); if (!hasNext) { break; } item = itr.next();
......
// build cache ,省略了一些障眼法,这才是重点,建立缓存 if (buildCache(item, false) == RESULT_FAILED) { break; } ...... } ...... if (item != null) {//截取的最后一条弹幕,更新缓存定时器时间 mCacheTimer.update(item.time); } else { mCacheTimer.update(end); } return consumingTime; } |
为截取的每一条弹幕建立缓存会调用buildCache(item, false)方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
privatebytebuildCache(BaseDanmaku item, boolean forceInsert){
// measure ,先测量每一条弹幕的宽高 if (!item.isMeasured()) { item.measure(mDisp, true); }
DrawingCache cache = null; try { // try to find reuseable cache, 在mCaches缓存的20条内查找和目标弹幕样式完全一样的弹幕(文字、大小、边框、下划线、颜色完全相同) BaseDanmaku danmaku = findReuseableCache(item, true, 20); if (danmaku != null) {//如果查找出了这样的弹幕 cache = (DrawingCache) danmaku.cache; } if (cache != null) {//如果找到的弹幕有缓存 cache.increaseReference();//则将引用计数 +1 item.cache = cache;//将目标弹幕缓存的引用指向查找出来的弹幕缓存,即多个引用指向同一个对象 //将这个目标弹幕的引用放入缓存Danmakus中(mCaches),同时更新已使用大小mRealSize mCacheManager.push(item, 0, forceInsert); return RESULT_SUCCESS; }
// try to find reuseable cache from timeout || no-refrerence caches //如果上述的查找样式完全相同的弹幕没有找到,则在前50条缓存中查找对比当前时间已经过时的 //,没有被重复引用的(只有上面那种情况才会增加引用计数,其他情况都不会) //,而且宽高和目标弹幕差值在规定范围内的弹幕 danmaku = findReuseableCache(item, false, 50); if (danmaku != null) {// 如果找到了这样的弹幕 cache = (DrawingCache) danmaku.cache; } if (cache != null) {//如果找到的弹幕有缓存 danmaku.cache = null;//先清除过时弹幕的缓存 //再根据目标弹幕样式,重新设置缓存(为每条弹幕创建一个bitmap和canvas,然后画出边框、下划线、文字等等) cache = DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache); //redraw item.cache = cache;//将缓存应用赋给目标弹幕 mCacheManager.push(item, 0, forceInsert);//将这个目标弹幕的引用放入缓存Danmakus中(mCaches),同时更新已使用大小mRealSize return RESULT_SUCCESS; }
//如果上述两次查找缓存都没找到,则进入下面逻辑 // guess cache size if (!forceInsert) {//如果forceInsert为false,则表示不检测内存超出 //计算此弹幕bitmap的大小,width * height * 4 //(因为用native创建的Bitmap的Config为ARGB_8888,所以一个像素占4个字节) int cacheSize = DanmakuUtils.getCacheSize((int) item.paintWidth, (int) item.paintHeight); //如果当前已经使用大小 + 此弹幕缓存大小 > 设置的最大内存(2/3 应用内存) if (mRealSize + cacheSize > mMaxSize) {//没有超 return RESULT_FAILED; } } //从FinitePool中的300个DrawingCache对象中取出来一个 cache = mCachePool.acquire(); //如果从上面的FinitePool取完了,则会直接new一个DrawingCache,配置DrawingCache cache = DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache); item.cache = cache; //将item存入mCaches缓存,同时更新已使用大小mRealSize boolean pushed = mCacheManager.push(item, sizeOf(item), forceInsert); if (!pushed) {//如果item存放失败(使用内存超出规定大小) releaseDanmakuCache(item, cache);//释放DrawingCache } return pushed ? RESULT_SUCCESS : RESULT_FAILED;
} catch (OutOfMemoryError e) { releaseDanmakuCache(item, cache); return RESULT_FAILED; } catch (Exception e) { releaseDanmakuCache(item, cache); return RESULT_FAILED; } } |
buildCache(item, false)为每一条弹幕建立缓存,其中有几处:
- 先测量弹幕的宽高
- 在mCaches缓存的20条内查找和目标弹幕样式完全一样的弹幕(文字、大小、边框、下划线、颜色完全相同)
- 如果上述的查找样式完全相同的弹幕没有找到,则在前50条缓存中查找对比当前时间已经过时的 ,没有被重复引用的(只有上面那种情况才会增加引用计数,其他情况都不会),而且宽高和目标弹幕差值在规定范围内的弹幕,再根据目标弹幕样式,重新设置缓存(为每条弹幕创建一个bitmap和canvas,然后画出边框、下划线、文字等等)
- 如果上述两次查找缓存都没找到,则从FinitePool中取出一个,没有就new一个,然后同上配置DrawingCache
1)我们一个一个来,先测量:
1 2 3 4 5 6 7 8 9 10 11 12 |
//弹幕的基类都是BaseDanmaku,只有子类R2LDanmaku重写了measure方法 @Override//R2LDanmaku的measure方法 publicvoidmeasure(IDisplayer displayer, boolean fromWorkerThread){ super.measure(displayer, fromWorkerThread);//调用了父类的方法 mDistance = (int) (displayer.getWidth() + paintWidth);//滚动弹幕的距离都是视图宽度+弹幕宽度,很好理解 mStepX = mDistance / (float) duration.value; //每秒步长就是总滚动距离除以弹幕时长 } //父类BaseDanmaku的measure方法 publicvoidmeasure(IDisplayer displayer, boolean fromWorkerThread){ displayer.measure(this, fromWorkerThread);//AndroidDisplayer的measure方法 this.measureResetFlag = flags.MEASURE_RESET_FLAG;//设置已经测量过了的标签 } |
接着会调用AndroidDisplayer的measure方法:
1 2 3 4 5 6 7 8 9 10 11 12 |
@Override publicvoidmeasure(BaseDanmaku danmaku, boolean fromWorkerThread){ ...设置画笔style,color,alpha,省略... calcPaintWH(danmaku, paint, fromWorkerThread);//计算宽高 ...设置画笔style,color,alpha,省略... } private BaseCacheStuffer sStuffer = new SimpleTextCacheStuffer();//默认是SimpleTextCacheStuffer privatevoidcalcPaintWH(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread){ sStuffer.measure(danmaku, paint, fromWorkerThread);//sStuffer就是我们在MainActivity里配置DanmakuContext时设置的,默认是SimpleTextCacheStuffer ...加上描边,padding等额外值,省略... } |
还记得在MainActivity里配置DanmakuContext吗?当时是这么写的:
.setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter) // 图文混排使用SpannedCacheStuffer
// .setCacheStuffer(new BackgroundCacheStuffer()) // 绘制背景使用BackgroundCacheStuffer
比如SpannedCacheStuffer的measure方法是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Override//SpannedCacheStuffer的measure方法 publicvoidmeasure(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread){ if (danmaku.text instanceof Spanned) { CharSequence text = danmaku.text; if (text != null) { //可看到将弹幕的宽高,文字等信息包在了一个StaticLayout对象中,然后付给danmaku的obj对象 StaticLayout staticLayout = new StaticLayout(text, paint, (int) Math.ceil(StaticLayout.getDesiredWidth(danmaku.text, paint)), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true); danmaku.paintWidth = staticLayout.getWidth(); danmaku.paintHeight = staticLayout.getHeight(); danmaku.obj = new SoftReference<>(staticLayout); return; } } super.measure(danmaku, paint, fromWorkerThread);//如果不是图文混排类型,则调用父类SimpleTextCacheStuffer的方法 } |
可以看到measure方法创建了一个StaticLayout对象,并将它的软引用赋给了danmaku的obj属性;如果是图文混排类型弹幕,则danmaku.obj不为空;如果是普通弹幕则danmaku.obj为空。
BackgroundCacheStuffer也差不多,都是对弹幕样式的一些改造。
然后我们看SimpleTextCacheStuffer的measure方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
//SimpleTextCacheStuffer的measure方法 publicvoidmeasure(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread){ if (mProxy != null) {//这个mProxy 是BaseCacheStuffer.Proxy类型的对象,也是初始化DanmakuContext调用setCacheStuffer(cacheStuffer, proxy)时设置的 mProxy.prepareDrawing(danmaku, fromWorkerThread);//根据你的条件检查是否需要需要更新弹幕 } float w = 0; Float textHeight = 0f; if (danmaku.lines == null) {//不是多行文本 if (danmaku.text == null) { w = 0; } else { w = paint.measureText(danmaku.text.toString());//测量出文字宽度 textHeight = getCacheHeight(danmaku, paint);//计算出文字高度 } danmaku.paintWidth = w; danmaku.paintHeight = textHeight; } else {//如果是多行文本 textHeight = getCacheHeight(danmaku, paint);//计算出单行文字高度 for (String tempStr : danmaku.lines) {//计算出多行文本总宽高 if (tempStr.length() > 0) { float tr = paint.measureText(tempStr); w = Math.max(tr, w); } } danmaku.paintWidth = w; danmaku.paintHeight = danmaku.lines.length * textHeight; } } privatefinalstatic Map sTextHeightCache = new HashMap();//key是字号大小,value是字体高度 protected Float getCacheHeight(BaseDanmaku danmaku, Paint paint){ Float textSize = paint.getTextSize(); Float textHeight = sTextHeightCache.get(textSize); if (textHeight == null) { Paint.FontMetrics fontMetrics = paint.getFontMetrics(); //Android对文字绘制有些特殊,基准点是baseline,也就是例如canvas.drawText(text, baseX, baseY, textPaint)中写入的baseY大小 //Ascent是baseline之上字符最高处的y值; //Descent是baseline之下字符最低处的y值; //Leading其实是上一行字符的descent到下一行的ascent之间的距离。 //所以文本高度就是descent - ascent + leading textHeight = fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading; sTextHeightCache.put(textSize, textHeight); } return textHeight; } |
这样就计算完了每一条弹幕的宽高,完成了测量。
2) 在mCaches缓存的20条内查找和目标弹幕样式完全一样的弹幕(文字、大小、边框、下划线、颜色完全相同):
先回到buildCache方法中这个位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
privatebytebuildCache(BaseDanmaku item, boolean forceInsert){//item, false
...测量已经完成... DrawingCache cache = null; try { // try to find reuseable cache, 在mCaches缓存的20条内查找和目标弹幕样式完全一样的弹幕(文字、大小、边框、下划线、颜色完全相同) BaseDanmaku danmaku = findReuseableCache(item, true, 20); if (danmaku != null) {//如果查找出了这样的弹幕 cache = (DrawingCache) danmaku.cache; } if (cache != null) {//如果找到的弹幕有缓存 cache.increaseReference();//则将引用计数 +1 item.cache = cache;//将目标弹幕缓存的引用指向查找出来的弹幕缓存,即多个引用指向同一个对象 //将这个目标弹幕的引用放入缓存Danmakus中(mCaches),同时更新已使用大小mRealSize mCacheManager.push(item, 0, forceInsert); return RESULT_SUCCESS; } ...... } //在mCaches缓存的20条内查找和目标弹幕样式完全一样的弹幕(文字、大小、边框、下划线、颜色完全相同) private BaseDanmaku findReuseableCache(BaseDanmaku refDanmaku, boolean strictMode, int maximumTimes) {//item, true, 20 IDanmakuIterator it = mCaches.iterator(); ...... int count = 0; while (it.hasNext() && count++ < maximumTimes) { // limit maximum times 20 BaseDanmaku danmaku = it.next(); IDrawingCache> cache = danmaku.getDrawingCache(); if (cache == null || cache.get() == null) { continue; } //对比mCaches中的弹幕和目标的内幕文字、大小、边框、下划线、颜色是否完全相同 if (danmaku.paintWidth == refDanmaku.paintWidth && danmaku.paintHeight == refDanmaku.paintHeight && danmaku.underlineColor == refDanmaku.underlineColor && danmaku.borderColor == refDanmaku.borderColor && danmaku.textColor == refDanmaku.textColor && danmaku.text.equals(refDanmaku.text)) { return danmaku; } if (strictMode) {//true continue; } ...... } returnnull; } //CacheManagingDrawTask.CacheManager的push方法 //将这个目标弹幕的引用放入缓存Danmakus中(mCaches),同时更新已使用大小mRealSize privatebooleanpush(BaseDanmaku item, int itemSize, boolean forcePush){//item,0,false int size = itemSize; //0 ...... //这里注意mCaches是Danmakus类型,addItem方法里面实现其实是类型为TreeSet的集合去添加,如果是同一个对象,则不会添加 this.mCaches.addItem(item); mRealSize += size;//因为已经存在相同的缓存,因此已经使用缓存总大小不再增加 returntrue; } //Danmakus的addItem方法 publicbooleanaddItem(BaseDanmaku item){ if (items != null) {//items 类型为TreeSet try { if (items.add(item)) {//如果是相同对象,则返回false,mSize个数不会增加 mSize++; returntrue; } } catch (Exception e) { e.printStackTrace(); } } returnfalse; } |
上述情况仅仅在相同样式,大小,颜色等都相同的弹幕第二次和以后的才会进入这段逻辑。对于不同的弹幕不会进入这个逻辑。(而且即使是相同弹幕,mCaches也只会存一个对象的,因为内部TreeSet的特性)
所以我们继续看下一种逻辑。
3)在前50条缓存中查找对比当前时间已经过时的 ,没有被重复引用的(只有上面那种情况才会增加引用计数,其他情况都不会),而且宽高和目标弹幕差值在规定范围内的弹幕,再根据目标弹幕样式,重新设置缓存(为每条弹幕创建一个bitmap和canvas,然后画出边框、下划线、文字等等):
继续回到buildCache方法这个位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
privatebytebuildCache(BaseDanmaku item, boolean forceInsert){//item, false ...测量过了... ...第一策略已经pass... // try to find reuseable cache from timeout || no-refrerence caches //如果上述的查找样式完全相同的弹幕没有找到,则在前50条缓存中查找对比当前时间已经过时的 //,没有被重复引用的(只有上面那种情况才会增加引用计数,其他情况都不会) //,而且宽高和目标弹幕差值在规定范围内的弹幕 danmaku = findReuseableCache(item, false, 50); if (danmaku != null) {// 如果找到了这样的弹幕 cache = (DrawingCache) danmaku.cache; } if (cache != null) {//如果找到的弹幕有缓存 danmaku.cache = null;//先清除过时弹幕的缓存 //再根据目标弹幕样式,重新设置缓存(为每条弹幕创建一个bitmap和canvas,然后画出边框、下划线、文字等等) cache = DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache); //redraw item.cache = cache;//将缓存应用赋给目标弹幕 mCacheManager.push(item, 0, forceInsert);//将这个目标弹幕的引用放入缓存Danmakus中(mCaches),同时更新已使用大小mRealSize return RESULT_SUCCESS; } ...... } private BaseDanmaku findReuseableCache(BaseDanmaku refDanmaku, boolean strictMode, int maximumTimes) {//item,false,50 IDanmakuIterator it = mCaches.iterator(); int slopPixel = 0; if (!strictMode) {//进入逻辑,非严苛模式 slopPixel = mDisp.getSlopPixel() * 2;//允许目标弹幕与mCaches中找到的弹幕宽高偏差 } int count = 0; while (it.hasNext() && count++ < maximumTimes) { // limit maximum times 20 BaseDanmaku danmaku = it.next(); IDrawingCache> cache = danmaku.getDrawingCache(); if (cache == null || cache.get() == null) { continue; } //在这种第二策略中这段逻辑根本不会执行,因为以已经被上面的第一策略拦截了 if (danmaku.paintWidth == refDanmaku.paintWidth && danmaku.paintHeight == refDanmaku.paintHeight && danmaku.underlineColor == refDanmaku.underlineColor && danmaku.borderColor == refDanmaku.borderColor && danmaku.textColor == refDanmaku.textColor && danmaku.text.equals(refDanmaku.text)) { return danmaku; } if (strictMode) {//false continue; } if (!danmaku.isTimeOut()) {//还必须在mCaches中过时的弹幕中查找 break; } if (cache.hasReferences()) {//如果是相同弹幕被重新引用的,第二策略没有这样的 continue; } //所以会走到这里,比较mCaches中过时的弹幕和目标弹幕宽高在不在允许的偏差内,如果在就返回查找出的这个弹幕 float widthGap = cache.width() - refDanmaku.paintWidth; float heightGap = cache.height() - refDanmaku.paintHeight; if (widthGap >= 0 && widthGap <= slopPixel && heightGap >= 0 && heightGap <= slopPixel) { return danmaku; } } returnnull; } |
如果在上述第二策略中,在过时的缓存中找到了和目标弹幕宽高差不多的缓存项,则根据目标弹幕样式,重新设置缓存(为每条弹幕创建一个bitmap和canvas,然后画出边框、下划线、文字等等),调用DanmakuUtils.buildDanmakuDrawingCache(item,mDisp, cache)方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
publicstatic DrawingCache buildDanmakuDrawingCache(BaseDanmaku danmaku, IDisplayer disp, DrawingCache cache) { if (cache == null) cache = new DrawingCache(); //组建弹幕缓存(bitmap,canvas) cache.build((int) Math.ceil(danmaku.paintWidth), (int) Math.ceil(danmaku.paintHeight), disp.getDensityDpi(), false); DrawingCacheHolder holder = cache.get(); if (holder != null) { //绘制弹幕内容 ((AbsDisplayer) disp).drawDanmaku(danmaku, holder.canvas, 0, 0, true); if(disp.isHardwareAccelerated()) {//如果有硬件加速 //超过一屏的弹幕要切割 holder.splitWith(disp.getWidth(), disp.getHeight(), disp.getMaximumCacheWidth(), disp.getMaximumCacheHeight()); } } return cache; } |
重新设置缓存分三步:1.组建弹幕缓存,2.绘制弹幕内容,3.切割超过一屏的弹幕。
No.1 组建弹幕缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
//DrawingCache的build方法 publicvoidbuild(int w, int h, int density, boolean checkSizeEquals){//checkSizeEquals为false final DrawingCacheHolder holder = mHolder; //每个DrawingCache都有一个DrawingCacheHolder holder.buildCache(w, h, density, checkSizeEquals);//DrawingCacheHolder的buildCache方法 mSize = mHolder.bitmap.getRowBytes() * mHolder.bitmap.getHeight();//返回创建的bitmap的大小 } //DrawingCacheHolder的buildCache方法 publicvoidbuildCache(int w, int h, int density, boolean checkSizeEquals){ boolean reuse = checkSizeEquals ? (w == width && h == height) : (w <= width && h <= height);//检测大小?宽高相等:小于已经缓存的bitmap宽高 if (reuse && bitmap != null) {//如果能够复用bitmap bitmap.eraseColor(Color.TRANSPARENT);//擦出之前的颜色 canvas.setBitmap(bitmap);//给Canvas重新预设bitmap recycleBitmapArray();//回收超过一屏弹幕切割后的bitmap数组,这个接下来会讲 return; } if (bitmap != null) {//如果不能复用,则回收旧的缓存bitmap recycle(); } width = w; height = h; bitmap = NativeBitmapFactory.createBitmap(w, h, Bitmap.Config.ARGB_8888);//用native方法创建一个bitmap if (density > 0) {//设置density mDensity = density; bitmap.setDensity(density); } //设置canvas if (canvas == null){ canvas = new Canvas(bitmap); canvas.setDensity(density); }else canvas.setBitmap(bitmap); } |
组建弹幕缓存就是为个DrawingCache根据目标弹幕大小创建bitmap和canvas。
No.2 绘制弹幕内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
publicsynchronizedvoiddrawDanmaku(BaseDanmaku danmaku, Canvas canvas, float left, float top, boolean fromWorkerThread) {//danmaku, holder.canvas, 0, 0, true float _left = left; float _top = top; ...一些杂项,忽略...
TextPaint paint = getPaint(danmaku, fromWorkerThread);//获取画笔 //绘制背景,sStuffer可以自己设置,默认是SimpleTextCacheStuffer,默认drawBackground为空 //这个可以自己扩展,上面讲过 sStuffer.drawBackground(danmaku, canvas, _left, _top); if (danmaku.lines != null) {//如果是多行文本 String[] lines = danmaku.lines; if (lines.length == 1) {//多行文本行数为1 if (hasStroke(danmaku)) {//如果有描边,则绘制描边 //重设画笔(绘制描边) applyPaintConfig(danmaku, paint, true); float strokeLeft = left; float strokeTop = top - paint.ascent(); ...... //绘制描边 sStuffer.drawStroke(danmaku, lines[0], canvas, strokeLeft, strokeTop, paint); } //再次重设画笔(绘制文字) applyPaintConfig(danmaku, paint, false); //绘制文字 sStuffer.drawText(danmaku, lines[0], canvas, left, top - paint.ascent(), paint, fromWorkerThread); } else {//多行文本行数大于1 //先计算每行文本的高度 float textHeight = (danmaku.paintHeight - 2 * danmaku.padding) / lines.length; //循环绘制每一行文本 for (int t = 0; t < lines.length; t++) { ...... if (hasStroke(danmaku)) {//如果有描边,则绘制描边 //重设画笔(绘制描边) applyPaintConfig(danmaku, paint, true); float strokeLeft = left; float strokeTop = t * textHeight + top - paint.ascent(); ...... //绘制描边 sStuffer.drawStroke(danmaku, lines[t], canvas, strokeLeft, strokeTop, paint); } //再次重设画笔(绘制文字) applyPaintConfig(danmaku, paint, false); //绘制文字 sStuffer.drawText(danmaku, lines[t], canvas, left, t * textHeight + top - paint.ascent(), paint, fromWorkerThread); } } } else {//如果是单行文本 if (hasStroke(danmaku)) {//如果有描边,则绘制描边 //重设画笔(绘制描边) applyPaintConfig(danmaku, paint, true); float strokeLeft = left; float strokeTop = top - paint.ascent(); ...... //绘制描边 sStuffer.drawStroke(danmaku, null, canvas, strokeLeft, strokeTop, paint); } //再次重设画笔(绘制文字) applyPaintConfig(danmaku, paint, false); //绘制文字 sStuffer.drawText(danmaku, null, canvas, left, top - paint.ascent(), paint, fromWorkerThread); }
// draw underline if (danmaku.underlineColor != 0) {//绘制下划线(if) Paint linePaint = getUnderlinePaint(danmaku); float bottom = _top + danmaku.paintHeight - UNDERLINE_HEIGHT; canvas.drawLine(_left, bottom, _left + danmaku.paintWidth, bottom, linePaint); }
//draw border if (danmaku.borderColor != 0) {//绘制外框 Paint borderPaint = getBorderPaint(danmaku); canvas.drawRect(_left, _top, _left + danmaku.paintWidth, _top + danmaku.paintHeight, borderPaint); }
} //设置画笔 privatevoidapplyPaintConfig(BaseDanmaku danmaku, Paint paint, boolean stroke){
...... if (stroke) { paint.setStyle(HAS_PROJECTION ? Style.FILL : Style.STROKE); paint.setColor(danmaku.textShadowColor & 0x00FFFFFF); int alpha = HAS_PROJECTION ? sProjectionAlpha : AlphaValue.MAX; paint.setAlpha(alpha); } else { paint.setStyle(Style.FILL); paint.setColor(danmaku.textColor & 0x00FFFFFF); paint.setAlpha(AlphaValue.MAX); } } |
上述就是绘制弹幕内容过程,主要就是sStuffer的drawStroke,drawText方法。如果你在DanmakuContext中没有设置CacheStuffer,则上述drawDanmaku方法中的sStuffer为默认的SimpleTextCacheStuffer。
drawStroke方法及其扩展都一样:
1 2 3 4 5 6 7 8 |
//SimpleTextCacheStuffer的drawStroke方法 publicvoiddrawStroke(BaseDanmaku danmaku, String lineText, Canvas canvas, float left, float top, Paint paint){ if (lineText != null) { canvas.drawText(lineText, left, top, paint); } else { canvas.drawText(danmaku.text.toString(), left, top, paint); } } |
我们设了SpannedCacheStuffer, drawText方法有些区别:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
//SimpleTextCacheStuffer的drawText方法 publicvoiddrawText(BaseDanmaku danmaku, String lineText, Canvas canvas, float left, float top, TextPaint paint, boolean fromWorkerThread){ if (lineText != null) { canvas.drawText(lineText, left, top, paint); } else { canvas.drawText(danmaku.text.toString(), left, top, paint); } } //SpannedCacheStuffer的drawText方法 publicvoiddrawText(BaseDanmaku danmaku, String lineText, Canvas canvas, float left, float top, TextPaint paint, boolean fromWorkerThread){ if (danmaku.obj == null) {//普通弹幕 super.drawText(danmaku, lineText, canvas, left, top, paint, fromWorkerThread); return; } //如果是图文混排弹幕 SoftReference reference = (SoftReference) danmaku.obj; StaticLayout staticLayout = reference.get(); //按位与,判断标志位是否有效。这里判断是否请求重新测量 boolean requestRemeasure = 0 != (danmaku.requestFlags & BaseDanmaku.FLAG_REQUEST_REMEASURE); //判断是否请求重绘 boolean requestInvalidate = 0 != (danmaku.requestFlags & BaseDanmaku.FLAG_REQUEST_INVALIDATE);
if (requestInvalidate || staticLayout == null) {//如果请求重绘或者staticLayout 软引用被回收了 if (requestInvalidate) { //与非操作,清除标志位。清除请求重绘标志位 danmaku.requestFlags &= ~BaseDanmaku.FLAG_REQUEST_INVALIDATE; } elseif (mProxy != null) {//这个在设置DanmakuContext时设置,上面讲过,可以自己扩展 mProxy.prepareDrawing(danmaku, fromWorkerThread); } CharSequence text = danmaku.text; if (text != null) { if (requestRemeasure) {//重新测量 staticLayout = new StaticLayout(text, paint, (int) Math.ceil(StaticLayout.getDesiredWidth(danmaku.text, paint)), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true); danmaku.paintWidth = staticLayout.getWidth(); danmaku.paintHeight = staticLayout.getHeight(); danmaku.requestFlags &= ~BaseDanmaku.FLAG_REQUEST_REMEASURE;//清除标志位 } else {//不用重新测量 staticLayout = new StaticLayout(text, paint, (int) danmaku.paintWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, true); } danmaku.obj = new SoftReference<>(staticLayout); } else { return; } } //staticLayout可以继续用 boolean needRestore = false; if (left != 0 && top != 0) { canvas.save(); canvas.translate(left, top + paint.ascent()); needRestore = true; } //绘制弹幕内容 staticLayout.draw(canvas); if (needRestore) { canvas.restore(); } } |
绘制弹幕内容就完了,主要是绘制描边,绘制文字,绘制下划线,边框等等。
No.3 切割超过一屏的弹幕:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
//DrawingCacheHolder的splitWith方法 publicvoidsplitWith(int dispWidth, int dispHeight, int maximumCacheWidth, int maximumCacheHeight){ recycleBitmapArray();//回收已存的bitmapArray数组 if (width <= 0 || height <= 0 || bitmap == null) { return; } //如果弹幕的宽高都没有超过屏幕宽高,则不切割bitmap if (width <= maximumCacheWidth && height <= maximumCacheHeight) { return; } //切割超过一屏的弹幕 maximumCacheWidth = Math.min(maximumCacheWidth, dispWidth); maximumCacheHeight = Math.min(maximumCacheHeight, dispHeight); //计算弹幕宽高是屏幕宽高的倍数,然后决定切割成多少块 int xCount = width / maximumCacheWidth + (width % maximumCacheWidth == 0 ? 0 : 1); int yCount = height / maximumCacheHeight + (height % maximumCacheHeight == 0 ? 0 : 1); //然后求切割后弹幕每一块宽和高的平均值 int averageWidth = width / xCount; int averageHeight = height / yCount; //建立二位bitmap数组,用于存放切割碎片 final Bitmap[][] bmpArray = new Bitmap[yCount][xCount]; if (canvas == null){ canvas = new Canvas(); if (mDensity > 0) { canvas.setDensity(mDensity); } } Rect rectSrc = new Rect(); Rect rectDst = new Rect(); //切割bitmap到bitmapArray中 for (int yIndex = 0; yIndex < yCount; yIndex++) { for (int xIndex = 0; xIndex < xCount; xIndex++) { //创建每一块小块bitmap Bitmap bmp = bmpArray[yIndex][xIndex] = NativeBitmapFactory.createBitmap( averageWidth, averageHeight, Bitmap.Config.ARGB_8888); if (mDensity > 0) { bmp.setDensity(mDensity); } //将弹幕的大bitmap绘制进每个小块bitmap中 canvas.setBitmap(bmp); int left = xIndex * averageWidth, top = yIndex * averageHeight; rectSrc.set(left, top, left + averageWidth, top + averageHeight); rectDst.set(0, 0, bmp.getWidth(), bmp.getHeight()); canvas.drawBitmap(bitmap, rectSrc, rectDst, null); } } canvas.setBitmap(bitmap); bitmapArray = bmpArray; } |
切割超过一屏的弹幕,就像玩切田字格游戏一样,完成后保存了一个bitmapArray数组。
到这里我们buildCache(item, false)的策略二中的重新设置缓存DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache)就走完了。然后将这个目标弹幕的引用放入缓存Danmakus中(mCaches),同时更新已使用大小mRealSize。同时注意mCaches内部成员items是TreeSet类型,不能添加相同的对象。
策略二设计的挺复杂的,我们可以看到这个策略应该是弹幕已经播放时不断执行的,对过时弹幕缓存的重复利用。不过我们刚开始,这一策略还未起作用,所以跳过,进入下一阶段:
4)如果上述两次查找缓存都没找到,则从FinitePool中取出一个,没有就new一个,然后同上配置DrawingCache:
继续回到buildCache方法这个位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
privatebytebuildCache(BaseDanmaku item, boolean forceInsert){//item, false ...测量过了... ...第一策略已经pass... ...第二策略已经pass... //如果上述两次查找缓存都没找到,则进入下面逻辑 // guess cache size if (!forceInsert) {//如果forceInsert为false,则表示不检测内存超出 //计算此弹幕bitmap的大小,width * height * 4 //(因为用native创建的Bitmap的Config为ARGB_8888,所以一个像素占4个字节) int cacheSize = DanmakuUtils.getCacheSize((int) item.paintWidth, (int) item.paintHeight); //如果当前已经使用大小 + 此弹幕缓存大小 > 设置的最大内存(2/3 应用内存) if (mRealSize + cacheSize > mMaxSize) {//没有超 return RESULT_FAILED; } } //从FinitePool中的300个DrawingCache对象中取出来一个 cache = mCachePool.acquire(); //如果从上面的FinitePool取完了,则会直接new一个DrawingCache,配置DrawingCache cache = DanmakuUtils.buildDanmakuDrawingCache(item, mDisp, cache); item.cache = cache; //将item存入mCaches缓存,同时更新已使用大小mRealSize boolean pushed = mCacheManager.push(item, sizeOf(item), forceInsert); if (!pushed) {//如果item存放失败(使用内存超出规定大小) releaseDanmakuCache(item, cache);//释放DrawingCache } return pushed ? RESULT_SUCCESS : RESULT_FAILED; ...... } //FinitePool的acquire方法,从缓存链表头取出一个对象 public T acquire(){ T element; //mRoot 就是缓存链表表头指向的对象 if (mRoot != null) { element = mRoot; mRoot = element.getNextPoolable(); mPoolCount--; } else { element = mManager.newInstance(); } if (element != null) { element.setNextPoolable(null); element.setPooled(false); mManager.onAcquired(element); } return element; } |
上述策略三是直接新建一个缓存DrawingCache,然后根据目标弹幕样式等配置它然后将它付给目标弹幕,再将目标弹幕放入缓存mCaches中。
刚开始时会执行策略三,因为刚开始时还没有缓存供我们使用,所以只能新建。
到此buildCache方法就走完了。我们可以看到buildCache主要截取了从当前时间开始的3倍弹幕时间内所有弹幕,然后为每一条弹幕建立缓存(创建DrawingCache对象,然后测量弹幕大小,再绘制弹幕内容,最后将信息保存到DrawingCache中,然后将它赋给目标弹幕的cache属性),并将这些弹幕保存到缓存mCaches中。
再次回顾一下上面的逻辑:
- 子线程从发送PREPARE消息开始,然后接着发送了DISPATCH_ACTIONS消息;
- DISPATCH_ACTIONS消息处理逻辑内部又会发送DISPATCH_ACTIONS消息,时间间隔为半条弹幕时间就这样不断循环发送;
- DISPATCH_ACTIONS消息处理会调用dispatchAction方法,dispatchAction方法会发送BUILD_CACHES消息;
- BUILD_CACHES消息处理会调用prepareCaches方法,prepareCaches方法内部会调用buildCache方法为从当前时间开始的3倍弹幕时间内所有的弹幕做缓存。
buildCache走完后,赶紧回到它之前调用方法的地方,不要把自己搞晕了= 。=
回到CacheManagingDrawTask的prepareCaches方法中,最后更新一下缓存定时器的时间,到缓存的最后一条弹幕的出现时间:
1 2 3 4 5 6 7 8 9 10 |
privatelongprepareCaches(boolean repositioned){
...截取三倍弹幕时间内所有弹幕,并为他们一一建立缓存... if (item != null) {//截取的最后一条弹幕,更新缓存定时器时间到它的出现时间 mCacheTimer.update(item.time); } else { mCacheTimer.update(end); } } |
prepareCaches方法走完后,回到处理原先处理BUILD_CACHES消息的逻辑中,继续执行剩余部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//CacheHandler的handleMessage方法 publicvoidhandleMessage(Message msg){ ...... case BUILD_CACHES: removeMessages(BUILD_CACHES); boolean repositioned = ((mTaskListener != null && mReadyState == false) || mSeekedFlag);// 为true prepareCaches(repositioned);//首次建立缓存已经完毕 if (repositioned) mSeekedFlag = false; if (mTaskListener != null && mReadyState == false) { mTaskListener.ready();//然后回到mTaskListener监听ready方法 mReadyState = true;//将mReadyState标志位置为true,下次BUILD_CACHES不会进入这段逻辑了 } break; ...... } |
执行mTaskListener.ready()方法,得回到上层逻辑DrawHandler的prepare(runnable)方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//DrawHandler的prepare方法 privatevoidprepare(final Runnable runnable){ if (drawTask == null) { drawTask = createDrawTask(mDanmakuView.isDanmakuDrawingCacheEnabled(), timer, mDanmakuView.getContext(), mDanmakuView.getWidth(), mDanmakuView.getHeight(), mDanmakuView.isHardwareAccelerated(), new IDrawTask.TaskListener() { @Override publicvoidready(){ initRenderingConfigs();//初始化一些渲染参数 runnable.run();//执行runnable的run方法,继续追踪 } ...... }); } else { runnable.run(); } } //DrawHandler的initRenderingConfigs方法 privatevoidinitRenderingConfigs(){ long averageFrameConsumingTime = 16;//平均每帧渲染间隔 mCordonTime = Math.max(33, (long) (averageFrameConsumingTime * 2.5f));//40,警戒值1 mCordonTime2 = (long) (mCordonTime * 2.5f);//100,警戒值2 mFrameUpdateRate = Math.max(16, averageFrameConsumingTime / 15 * 15);//16,每帧渲染间隔 mThresholdTime = mFrameUpdateRate + 3;//19,渲染间隔阀值 } |
初始化一些渲染参数,主要就是计算一下警戒时间和渲染频率。然后继续追踪runnable.run()方法,这个得回到DrawHandler的handleMessage方法中处理DrawHandler.PREPARE逻辑处:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//DrawHandler的handleMessage方法 publicvoidhandleMessage(Message msg){ int what = msg.what; switch (what) { case PREPARE: ...... prepare(new Runnable() { @Override publicvoidrun(){//会回调到这里 pausedPosition = 0; mReady = true;//将mReady 标志位置为true if (mCallback != null) { mCallback.prepared();//回调callback监听 } } }); ...... break; } |
继续追踪mCallback.prepared(),会回到MainActivity当中我们设置DanmakuView的地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//MainActivity中设置mDanmakuView mDanmakuView.setCallback(new master.flame.danmaku.controller.DrawHandler.Callback() { ...... @Override publicvoidprepared(){ mDanmakuView.start(); } }); //继续产看DanmaKuView的start方法 publicvoidstart(){ start(0); }
publicvoidstart(long postion){ ...... handler.obtainMessage(DrawHandler.START, postion).sendToTarget();//DrawHandler发送START消息 } |
然后就是DrawHandler发送START消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
//DrawHandler的handleMessage方法 publicvoidhandleMessage(Message msg){ ...... case START: Long startTime = (Long) msg.obj;//0 if (startTime != null) { pausedPosition = startTime;//0 } else { pausedPosition = 0; } case SEEK_POS: ...... case RESUME: quitFlag = false; if (mReady) {//true ...... mTimeBase = SystemClock.uptimeMillis() - pausedPosition;//将时间基线设为当前时间 timer.update(pausedPosition);//更新主定时器时间到初始位置,为0 removeMessages(RESUME); sendEmptyMessage(UPDATE);//发送UPDATE消息 drawTask.start();//CacheManagingDrawTask的start方法 ...... } else { ...... } break; case UPDATE: if (mUpdateInNewThread) {//在DrawHandler构造方法里赋值的变量,只有当可用CPU个数大于3时才为true updateInNewThread();//四核,八核的请进 } else { updateInCurrentThread();//单核,双核的请进 } break; ...... } |
上述逻辑最后会进入RESUME消息处理中,先调用CacheManagingDrawTask的start方法,然后处理UPDATE消息。我们先看看CacheManagingDrawTask的start方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//CacheManagingDrawTask的start方法 publicvoidstart(){ ...... mCacheManager.resume();//CacheManager的resume方法 } //继续跟CacheManager的resume方法 publicvoidresume(){ ...... mHandler.resume();//CacheManagingDrawTask的resume方法 ...... } //继续跟CacheManagingDrawTask的resume方法 publicvoidresume(){ mCancelFlag = false; mPause = false; removeMessages(DISPATCH_ACTIONS); sendEmptyMessage(DISPATCH_ACTIONS);//发送DISPATCH_ACTIONS消息,我们上面分析过,就是建立缓存 sendEmptyMessageDelayed(CLEAR_TIMEOUT_CACHES, mContext.mDanmakuFactory.MAX_DANMAKU_DURATION);//延时发送CLEAR_TIMEOUT_CACHES消息 } |
我们可以看到CacheManagingDrawTask的start方法最终做了两件事,一件是发送DISPATCH_ACTIONS再次建立缓存,这个流程我们上面分析过;第二件是延时发送CLEAR_TIMEOUT_CACHES消息。
所以我们看看CLEAR_TIMEOUT_CACHES消息处理逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
//CacheHandler的handleMessage方法 publicvoidhandleMessage(Message msg){ ....... case CLEAR_TIMEOUT_CACHES: clearTimeOutCaches();//继续跟这个 break; ...... } //调用 clearTimeOutCaches方法 privatevoidclearTimeOutCaches(){ clearTimeOutCaches(mTimer.currMillisecond);//调用重载方法,参数为主定时器当前时间 } //调用重载方法,参数为主定时器当前时间 privatevoidclearTimeOutCaches(long time){ IDanmakuIterator it = mCaches.iterator();//从之前buildCache中建立的缓存中一一遍历 while (it.hasNext() && !mEndFlag) {//mEndFlag = false BaseDanmaku val = it.next(); if (val.isTimeOut()) {//如果缓存的弹幕已经超时 ...... entryRemoved(false, val, null);//销毁缓存 it.remove();//从缓存mCaches中移除此引用 } else { break; } } } |
顺着逻辑看看entryRemoved(false, val, null)方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
protectedvoidentryRemoved(boolean evicted, BaseDanmaku oldValue, BaseDanmaku newValue){//第1个和第3个参数没用到 IDrawingCache> cache = oldValue.getDrawingCache(); if (cache != null) { long releasedSize = clearCache(oldValue);//调用了clearCache方法 if (oldValue.isTimeOut()) { //这个方法最终会调用我们最初设置DanmakuContext.setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter) //中第二个参数类型为BaseCacheStuffer.Proxy的releaseResource方法, //方法注释是这么写的 TODO 重要:清理含有ImageSpan的text中的一些占用内存的资源例如drawable mContext.getDisplayer().getCacheStuffer().releaseResource(oldValue); } if (releasedSize <= 0) return; mRealSize -= releasedSize;//真正缓存大小减去需要释放的缓存大小 mCachePool.release((DrawingCache) cache);//将Drawingcache放回到FinitePool中,已供下次取出 } } //往下看,看看clearCache方法 privatelongclearCache(BaseDanmaku oldValue){ IDrawingCache> cache = oldValue.cache; if (cache == null) { return0; } if (cache.hasReferences()) {//如果DrawingCache缓存还被重复引用 cache.decreaseReference();//则将引用计数-1 oldValue.cache = null; return0;//不销毁缓存(bitmap,canvas等),只有等到引用计数为0时才会销毁 } long size = sizeOf(oldValue);//计算缓存的bitmap大小 cache.destroy();//同时销毁bitmap等 oldValue.cache = null; return size; } //缓存的bitmap的大小 protectedintsizeOf(BaseDanmaku value){ if (value.cache != null && !value.cache.hasReferences()) { return value.cache.size();//返回的是Drawing中bitmap对象的大小,上面讲过的 } return0; } |
CLEAR_TIMEOUT_CACHES消息处理就分析完了,就是移除缓存弹幕mCache中过时的弹幕,并且销毁他们持有的DrawingCache,同时销毁内部的bitmap、canvas等。
缓存机制
现在重点来了!还记得我们之前挖的一个大坑么?就是妹子图那个地方。那是CacheHandler给工作线程发送DISPATCH_ACTIONS消息时调用的dispatchAction方法。因为CacheHandler每个半条弹幕时间就会发DISPATCH_ACTIONS消息,所以我们得仔细分析一下dispatchAction方法的各种情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
privatelongdispatchAction(){ //如果上一次buildCache完成后得到的缓存弹幕末尾项的时间(上面分析过,这个值存在mCacheTimer.currMillisecond中) //和主定时器当前时间之间的时间差值已经大于一条弹幕时间, //则会清除所有不在屏幕内的缓存,然后重新buildCache建立缓存 if (mCacheTimer.currMillisecond <= mTimer.currMillisecond - mContext.mDanmakuFactory.MAX_DANMAKU_DURATION) { evictAllNotInScreen();//则会清除所有不在屏幕内的缓存 mCacheTimer.update(mTimer.currMillisecond); sendEmptyMessage(BUILD_CACHES);//重新建立缓存 return0; } float level = getPoolPercent();//获得缓存实际大小占设置最大内存的百分比 BaseDanmaku firstCache = mCaches.first(); //TODO 如果firstcache大于当前时间超过半屏并且水位在0.5f以下,就要往里蓄水 long gapTime = firstCache != null ? firstCache.time - mTimer.currMillisecond : 0; long doubleScreenDuration = mContext.mDanmakuFactory.MAX_DANMAKU_DURATION * 2; if (level < 0.6f && gapTime > mContext.mDanmakuFactory.MAX_DANMAKU_DURATION) { mCacheTimer.update(mTimer.currMillisecond); removeMessages(BUILD_CACHES); sendEmptyMessage(BUILD_CACHES);//重新建立缓存 return0; } elseif (level > 0.4f && gapTime < -doubleScreenDuration) {//如果水位在0.5以上,并且上一次蓄水距离现在已经超过两条弹幕时间了,就要开闸放水 // clear timeout caches removeMessages(CLEAR_TIMEOUT_CACHES); sendEmptyMessage(CLEAR_TIMEOUT_CACHES);//CLEAR_TIMEOUT_CACHES消息刚分析过了,清除过时缓存 return0; }
if (level >= 0.9f) {//水位快满了,等待下次放水 return0; } // check cache time long deltaTime = mCacheTimer.currMillisecond - mTimer.currMillisecond; //缓存的第一条弹幕已经过时了,并且缓存弹幕末尾时间和现在时间差值已经超过一条弹幕时间了 if (firstCache != null && firstCache.isTimeOut() && deltaTime < -mContext.mDanmakuFactory.MAX_DANMAKU_DURATION) { mCacheTimer.update(mTimer.currMillisecond); sendEmptyMessage(CLEAR_OUTSIDE_CACHES);//先清除过时缓存 sendEmptyMessage(BUILD_CACHES);//再重组缓存 return0; } elseif (deltaTime > doubleScreenDuration) {//如果缓存的最后一条弹幕时间距离现在还有双倍弹幕时间多,则啥都不做 return0; } //剩余情况组建缓存 removeMessages(BUILD_CACHES); sendEmptyMessage(BUILD_CACHES); return0; } //则会清除所有不在屏幕内的缓存 privatevoidevictAllNotInScreen(){ evictAllNotInScreen(false); } privatevoidevictAllNotInScreen(boolean removeAllReferences){ if (mCaches != null) { IDanmakuIterator it = mCaches.iterator(); while (it.hasNext()) { BaseDanmaku danmaku = it.next(); ...... if (danmaku.isOutside()) {//如果弹幕已经走完了,超过屏幕 entryRemoved(true, danmaku, null);//回收缓存 it.remove(); } } } mRealSize = 0; } //获得缓存实际大小占设置最大内存的百分比 publicfloatgetPoolPercent(){ if (mMaxSize == 0) { return0; } return mRealSize / (float) mMaxSize; } |
dispatchAction方法主要分为以下几种规则:
- 如果上一次buildCache完成后得到的缓存弹幕末尾项的时间(上面分析过,这个值存在mCacheTimer.currMillisecond中)和主定时器当前时间之间的时间差值已经大于一条弹幕时间, 则会清除所有不在屏幕内的缓存,然后重新buildCache建立缓存;
- 如果缓存弹幕的第一项出现时间大于当前时间超过半屏,并且总缓存大小在规定最大值一半以下, 就要重新建立缓存;
- 如果总缓存大小在规定最大值一半以上,并且上一次建立缓存距离现在已经超过两条弹幕时间了,就要清除超时缓存;
- 如果总缓存大小快达到规定最大值,就等待下一次清除超时缓存;
- 缓存的第一条弹幕已经过时了,并且缓存弹幕末尾时间和现在时间差值已经超过一条弹幕时间了,先清除过时缓存,再重组缓存;
- 如果缓存的最后一条弹幕时间距离现在还有双倍弹幕时间多,则啥都不做;
- 剩余情况就是重组缓存。
因为DISPATCH_ACTIONS消息是每隔半条弹幕时间发送一次,所以会不断执行dispatchAction方法。然后根据上述出现的情况不断BUILD_CACHES和CLEAR_TIMEOUT_CACHES,这样工作线程就形成了一套缓存机制。
绘制弹幕界面
到此CacheManagingDrawTask的start方法就分析完了,继续回到DrawHandler的handleMessage方法,接着处理UPDATE消息:
1 2 3 4 5 6 7 8 9 10 11 |
//DrawHandler的handleMessage方法 publicvoidhandleMessage(Message msg){ case UPDATE: if (mUpdateInNewThread) {//在DrawHandler构造方法里赋值的变量,只有当可用CPU个数大于3时才为true updateInNewThread();//四核,八核的请进 } else { updateInCurrentThread();//单核,双核的请进 } break; ...... } |
到这里,我们应该能猜到接下要进行应该就是绘制工作了。其实updateInNewThread和updateInCurrentThread做的事情是一样的,只不过其中一个新开了子线程去做这些事情。两者的工作原理都是更新定时器,然后postInvalidate,使DanmakuView重绘,然后再发UPDATE消息,重复上述过程。
鉴于目前四核手机已经烂大街了,我们也就挑个多核的方法进去看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
privatevoidupdateInNewThread(){ if (mThread != null) { return; } mThread = new UpdateThread("DFM Update") { @Override publicvoidrun(){ long lastTime = SystemClock.uptimeMillis(); long dTime = 0; while (!isQuited() && !quitFlag) { long startMS = SystemClock.uptimeMillis(); dTime = SystemClock.uptimeMillis() - lastTime; long diffTime = mFrameUpdateRate - dTime;//mFrameUpdateRate 为16,之前计算过 if (diffTime > 1) {//如果间隔时间太短,则会延时,一定要等够16毫秒,达到绘制时间间隔 SystemClock.sleep(1); continue; }d //上面逻辑是为了延时,稳定帧率 lastTime = startMS; long d = syncTimer(startMS);//同步主定时器时间 ...... d = mDanmakuView.drawDanmakus();//开始postInvalidate,绘制弹幕,同时返回绘制时间 //这种情况出现在绘制时间内,绘制时子线程在wait,等待绘制结束,然后返回差值必定大于警戒值100 if (d > mCordonTime2) { // this situation may be cuased by ui-thread waiting of DanmakuView, so we sync-timer at once timer.add(d);//绘制完成后更新主定时器时间 mDrawTimes.clear(); } ...... } } }; mThread.start(); } |
updateInNewThread主要做了两件事:延时然后同步主定时器时间,然后通知DanmakuView重绘。
我们先看同步主定时器时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
privatefinallongsyncTimer(long startMS){
...... long d = 0; long time = startMS - mTimeBase;//当前时间到初始时间的时间差 ...... long gapTime = time - timer.currMillisecond;//总时间差减去上一次绘制完成时间,得到绘制间隙时间 long averageTime = Math.max(mFrameUpdateRate, getAverageRenderingTime());//计算绘制间隙平均时间,大于等于16(getAverageRenderingTime方法是计算加入mDrawTimes队列的已经绘制过的时间总和除以帧数,得到平均时间,这个下面会讲到) //若果距离上次间隙时间过长||上次渲染时间大于第一警戒时间(40 ms)||上一步计算的绘制间隙平均时间大于第一警戒时间 if (gapTime > 2000 || mRenderingState.consumingTime > mCordonTime || averageTime > mCordonTime) { d = gapTime; gapTime = 0; } else {//如果是普通情况 d = averageTime + gapTime / mFrameUpdateRate;//将绘制间隙平均时间赋给d,后面的项值不大,可以忽略 d = Math.max(mFrameUpdateRate, d);//大于等于固定绘制间隔16 d = Math.min(mCordonTime, d);//小于第一警戒时间40 ...... } ...... timer.add(d);//更新主定时器时间,加上计算的时间间隔
...... return d; } //计算平均绘制间隔时间 privatesynchronizedlonggetAverageRenderingTime(){ int frames = mDrawTimes.size(); if(frames <= 0) return0; long dtime = mDrawTimes.getLast() - mDrawTimes.getFirst(); return dtime / frames; } |
syncTimer主要是计算了一下绘制间隔时间,然后同步一下主定时器。
然后我们看看通知DanmakuView重绘部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
//DanmakuView的drawDanmakus方法 publiclongdrawDanmakus(){ long stime = SystemClock.uptimeMillis(); lockCanvas();//再看看lockCanvas return SystemClock.uptimeMillis() - stime;//返回等待时间差 } //DanmakuView的lockCanvas方法 privatevoidlockCanvas(){ ...... postInvalidateCompat();//通知view重绘 synchronized (mDrawMonitor) { while ((!mDrawFinished) && (handler != null)) {//mDrawFinished标志位为false,所以会进入循环。只有onDraw方法的绘制走完了才会将他置为true,才会跳出循环 try { mDrawMonitor.wait(200);//onDraw没走完就会一直循环等待 } catch (InterruptedException e) { if (mDanmakuVisible == false || handler == null || handler.isStop()) { break; } else { Thread.currentThread().interrupt(); } } } mDrawFinished = false;//绘制结束后,将标志位置为false,一边下次进入方法后再次进入上述等待逻辑 } } privatevoidpostInvalidateCompat(){ mRequestRender = true;//将mRequestRender 标志位置为true,一遍onDraw方法逻辑执行 //通知view重绘 if(Build.VERSION.SDK_INT >= 16) { this.postInvalidateOnAnimation(); } else { this.postInvalidate(); } } |
这样就能保证保证每隔一定时间(这个时间通过syncTimer计算),更新主定时器(就是从0开始,往后每次加上(间隔时间 + 绘制时间)),然后执行postInvalidate通知DanmakuView重绘。
postInvalidate后,View重绘,会重走onDraw方法,所以我们进入DanmakuView的onDraw方法看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//DanmakuView的onDraw方法 protectedvoidonDraw(Canvas canvas){ if ((!mDanmakuVisible) && (!mRequestRender)) {//如果没有请求重绘则mRequestRender为false,不会绘制弹幕 super.onDraw(canvas); return; } ...... if (handler != null) { RenderingState rs = handler.draw(canvas);//DrawHandler的draw方法 ...... } ...... //绘制结束后将mRequestRender 标志位重新设为false, //以便下一次发绘制消息时进入等待逻辑等候绘制结束,这个上面DanmakuView的drawDanmakus方法提到过 mRequestRender = false; unlockCanvasAndPost();//通知UpdateThread绘制完成 } privatevoidunlockCanvasAndPost(){ synchronized (mDrawMonitor) { mDrawFinished = true;//将mDrawFinished 置为true,以便DanmakuView的lockCanvas方法跳出循环,这个上面也提到过 mDrawMonitor.notifyAll(); } } |
DanmakuView的onDraw回调逻辑会执行DrawHandler的draw方法,我们继续跟进去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public RenderingState draw(Canvas canvas){ ...... mDisp.setExtraData(canvas);//将canvas一些信息设置给AndroidDisplayer mRenderingState.set(drawTask.draw(mDisp));//绘制部分是drawTask.draw(mDisp) recordRenderingTime();//记录绘制结束时间 return mRenderingState; } //还记得上面的DrawHandler的syncTimer方法吗?里面调用了getAverageRenderingTime计算绘制平均间隔时间, //其中用到的mDrawTimes变量就是在这里添加元素的 privatesynchronizedvoidrecordRenderingTime(){ long lastTime = SystemClock.uptimeMillis(); mDrawTimes.addLast(lastTime);//将绘制结束时间加入到类型为LinkedList的mDrawTimes集合中 int frames = mDrawTimes.size(); if (frames > MAX_RECORD_SIZE) {//最大容量为500个绘制时间,超出了则移除第一个 mDrawTimes.removeFirst(); } } |
上述逻辑中,我的注释部分先分析了记录绘制结束时间部分,填了上边syncTimer时的坑。
然后应该进入主要绘制部分了drawTask.draw(mDisp),也就是CacheManagingDrawTask的draw方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
//CacheManagingDrawTask的draw方法 public RenderingState draw(AbsDisplayer displayer){ RenderingState result = super.draw(displayer);//会调用父类的draw方法 ...... return result; } //DrawTask的draw方法 publicsynchronized RenderingState draw(AbsDisplayer displayer){ return drawDanmakus(displayer,mTimer);//又调用了drawDanmakus方法 } //DrawTask的drawDanmakus方法 protected RenderingState drawDanmakus(AbsDisplayer disp, DanmakuTimer timer){ ...... if (danmakuList != null) { Canvas canvas = (Canvas) disp.getExtraData();//取出DanmakuView的canvas //当前时间 - 1屏弹幕时间 -100 (多减100是为了下次重新截取弹幕组时让绘制边界做到无缝衔接) long beginMills = timer.currMillisecond - mContext.mDanmakuFactory.MAX_DANMAKU_DURATION - 100; //当前时间 + 1屏弹幕时间 long endMills = timer.currMillisecond + mContext.mDanmakuFactory.MAX_DANMAKU_DURATION; //每过了一屏的弹幕时间,就会进入如下if逻辑,截取以当前时间为基准的前后两屏弹幕; //如果距离上次截取时间不到一屏弹幕时间,则不会进入if的逻辑 if(mLastBeginMills > beginMills || timer.currMillisecond > mLastEndMills) { IDanmakus subDanmakus = danmakuList.sub(beginMills, endMills); if(subDanmakus != null) { danmakus = subDanmakus; } mLastBeginMills = beginMills; mLastEndMills = endMills; } else {//距离上次截取时间不到一屏时间 ...... } if (danmakus != null && !danmakus.isEmpty()) {//开始绘制弹幕 RenderingState renderingState = mRenderingState = mRenderer.draw(mDisp, danmakus, mStartRenderTime); ...... } } |
我们可以看到第一次进入会截取以当前时间为基准的前后两屏弹幕。以后每过一屏弹幕时间,会重新截取当时时间为基准的前后两屏弹幕,如果不到一屏时间则不截取,还是以前的弹幕数据。
截取完弹幕数据后,就是绘制了,继续执行下面逻辑(mRenderer.draw(mDisp,danmakus, mStartRenderTime)),开始绘制工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
//DanmakuRenderer的draw方法 public RenderingState draw(IDisplayer disp, IDanmakus danmakus, long startRenderTime){ ...... IDanmakuIterator itr = danmakus.iterator(); ......
BaseDanmaku drawItem = null; while (itr.hasNext()) {
drawItem = itr.next();
...... //如果弹幕还没有到出现时间,则检查它有没有缓存,如果没有则为它建立缓存 if (drawItem.isLate()) { IDrawingCache> cache = drawItem.getDrawingCache(); if (mCacheManager != null && (cache == null || cache.get() == null)) { mCacheManager.addDanmaku(drawItem); } break; } ......
// measure 测量,我们之前prepareCache已经为他们在buildCache是测量过了 if (!drawItem.isMeasured()) { drawItem.measure(disp, false); }
// layout 布局,计算弹幕在屏幕上应该显示的位置 mDanmakusRetainer.fix(drawItem, disp, mVerifier);
// draw //绘制弹幕 if (!drawItem.isOutside() && drawItem.isShown()) { if (drawItem.lines == null && drawItem.getBottom() > disp.getHeight()) { continue; // skip bottom outside danmaku ,忽略超过视图底部的弹幕 } //开始绘制 int renderingType = drawItem.draw(disp); if(renderingType == IRenderer.CACHE_RENDERING) {//如果是使用缓存bitmap绘制的 ...... } elseif(renderingType == IRenderer.TEXT_RENDERING) {//如果使用缓存绘制失败,则会使用原声方法Canvas去draw ...... if (mCacheManager != null) { mCacheManager.addDanmaku(drawItem);//再次为词条弹幕构建缓存,以便下次使用缓存bitmap绘制 } } ...... } |
从截取的弹幕中遍历每一个,然后一一绘制。绘制步骤有如下几步:
- 如果弹幕还没有到出现时间,则检查它有没有缓存,如果没有则为它建立缓存;
- measure 测量,我们之前prepareCache已经为他们在buildCache时测量过了;
- layout 布局,计算弹幕在屏幕上应该显示的位置;
- draw 绘制弹幕。
我们一步一步分析:
1)弹幕未到出现时间,检查是否建立缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//调用CacheManagingDrawTask的addDanmaku方法 publicvoidaddDanmaku(BaseDanmaku danmaku){ if (mHandler != null) { ...... //CacheHandler mHandler.obtainMessage(CacheHandler.ADD_DANMAKKU, danmaku).sendToTarget(); ...... } } //CacheHandler publicvoidhandleMessage(Message msg){ case ADD_DANMAKKU: BaseDanmaku item = (BaseDanmaku) msg.obj; addDanmakuAndBuildCache(item);//调用了addDanmakuAndBuildCache方法 break; } //调用了addDanmakuAndBuildCache方法 privatefinalvoidaddDanmakuAndBuildCache(BaseDanmaku danmaku){ //过时了 || 并且弹幕时间不在3屏弹幕时间内(因为mCaches只缓存了3屏时间内的所有弹幕,上面说过的),并且它不是直播弹幕。则不建立缓存 if (danmaku.isTimeOut() || (danmaku.time > mCacheTimer.currMillisecond + mContext.mDanmakuFactory.MAX_DANMAKU_DURATION && !danmaku.isLive)) { return; } //优先级为0或者在过滤规则内,不建立缓存 if (danmaku.priority == 0 && danmaku.isFiltered()) { return; } IDrawingCache> cache = danmaku.getDrawingCache(); if (cache == null || cache.get() == null) {//如果弹幕没有缓存 buildCache(danmaku, true);//建立缓存(buildCache方法我们上面分析过,就是用来建立缓存的) } } |
2)测量,这个我们上面再buildCache时分析过了,不再赘述;
3)布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//调用DanmakusRetainer的fix方法 publicvoidfix(BaseDanmaku danmaku, IDisplayer disp, Verifier verifier){ int type = danmaku.getType(); switch (type) { case BaseDanmaku.TYPE_SCROLL_RL: rldrInstance.fix(danmaku, disp, verifier); break; case BaseDanmaku.TYPE_SCROLL_LR: lrdrInstance.fix(danmaku, disp, verifier); break; case BaseDanmaku.TYPE_FIX_TOP: ftdrInstance.fix(danmaku, disp, verifier); break; case BaseDanmaku.TYPE_FIX_BOTTOM: fbdrInstance.fix(danmaku, disp, verifier); break; case BaseDanmaku.TYPE_SPECIAL: danmaku.layout(disp, 0, 0); break; } } |
类型太多了,我们只分析TYPE_SCROLL_RL类型弹幕其他的就不分析,有兴趣的可以自己分析一下其他的。接着会调用AlignTopRetainer的fix方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
//保存需要显示的弹幕容器类(保存的一行只有一条弹幕,下面会说明的),内部持有一个以弹幕的y坐标排序的TreeSet集合,这个需要注意 protected Danmakus mVisibleDanmakus = new Danmakus(Danmakus.ST_BY_YPOS); //AlignTopRetainer的fix方法 publicvoidfix(BaseDanmaku drawItem, IDisplayer disp, Verifier verifier){ if (drawItem.isOutside())//如果弹幕已经滚动到视图边界外,则不会为它布局 return; float topPos = 0;//弹幕的y坐标 int lines = 0;//弹幕在第几行显示 boolean shown = drawItem.isShown();//弹幕是否已经显示 boolean willHit = !shown && !mVisibleDanmakus.isEmpty();//是否会和其他弹幕碰撞 boolean isOutOfVertialEdge = false;//弹幕y值是否超过试图高度 BaseDanmaku removeItem = null;//需要移除的弹幕 //为即将显示的弹幕确认位置 if (!shown) { mCancelFixingFlag = false; // 确定弹幕位置开始 IDanmakuIterator it = mVisibleDanmakus.iterator(); //这四个变量分别为: //insertItem ---- 确认目标弹幕插入到哪一行的同行参考弹幕 //firstItem ---- 已经布局过的弹幕保存容器中的第一项 //lastItem ---- 已经布局过的弹幕保存容器中最后一项 //minRightRow ---- 已经布局过弹幕中x值最小的弹幕,即最左边的弹幕 BaseDanmaku insertItem = null, firstItem = null, lastItem = null, minRightRow = null; boolean overwriteInsert = false;//是否超出插入范围 //遍历已经绘制过的弹幕,因为mVisibleDanmakus 内弹幕以y值排序的,所以按y值从小到大遍历 while (!mCancelFixingFlag && it.hasNext()) { lines++;//每次循环都会将行号+1 BaseDanmaku item = it.next(); if(item == drawItem){//如果已经布局过了,说明已经存在自己位置了 insertItem = item;//将布局过的弹幕复制给参考弹幕insertItem lastItem = null;//置空 lastItem shown = true;//shown 置为true,以便末尾不再执行加入mVisibleDanmakus逻辑 willHit = false;//本身已经存在自己位置了,当然没有碰壁一说 break;//怕被下面干扰晕的可以跳出去继续看 } if (firstItem == null)//找到已经布局过的弹幕第一项 firstItem = item; //如果插入目标弹幕后,y值超过了视图高度 if (drawItem.paintHeight + item.getTop() > disp.getHeight()) { overwriteInsert = true;//则将超出插入范围标签置为true break;//怕晕的跳出循环 } //找出最左边的弹幕 if (minRightRow == null) { minRightRow = item; } else { if (minRightRow.getRight() >= item.getRight()) { minRightRow = item; } }
// 检查如果插入目标弹幕是否会和正在遍历的已经布局过的参考弹幕碰撞 willHit = DanmakuUtils.willHitInDuration(disp, item, drawItem, drawItem.getDuration(), drawItem.getTimer().currMillisecond); if (!willHit) {//如果没有碰撞 insertItem = item;//则将它复制给参考弹幕insertItem break;//然后跳出循环,下去确定位置 }/*如果有碰撞,则继续弹幕缩小添加范围,寻找可以添加的条件,最后出while循环,下去布局*/ lastItem = item;//暂时找到已经布局过的弹幕最后一项,然后继续循环 } boolean checkEdge = true; if (insertItem != null) {//已经布局过了||目标弹幕不会碰壁可以插入 if (lastItem != null)//目标弹幕插入,y值即为上一次遍历的弹幕的底部 topPos = lastItem.getBottom(); else//已经布局过了,则y的位置不变 topPos = insertItem.getTop(); if (insertItem != drawItem){//如果目标弹幕可以插入 //这里需要注意,因为一行可以放n多条弹幕,只要前后不碰撞就行; //所以下次我们在同一行插入弹幕判断碰壁时,当然要和这行最后一条弹幕去判断; //因此我们移除前一条弹幕,放入插入的目标弹幕,下次添加弹幕判断时就和目标弹幕判断,然后这么循环下去 removeItem = insertItem; shown = false;//置为false,以便mVisibleDanmakus 添加还未布局的新弹幕 } } elseif (overwriteInsert && minRightRow != null) {//没有空行可以插入 topPos = minRightRow.getTop();//暂时放到最最左边的弹幕那一行(excuse me ???) checkEdge = false;//不做范围检查 shown = false; } elseif (lastItem != null) {//找不到插入的位置 topPos = lastItem.getBottom();//暂时放到最低位置的弹幕下面,下面检测边界时会酌情河蟹 willHit = false;//置false碰壁标志 } elseif (firstItem != null) {////mVisibleDanmakus只有第一条数据,截取弹幕集的第二条弹幕没有和第一条碰壁时 topPos = firstItem.getTop();//此时第二条弹幕和第一条在同一行 removeItem = firstItem; shown = false; } else {//mVisibleDanmakus 没有数据,截取弹幕集的第一条弹幕 topPos = 0;//第一条弹幕当然在最上面 } if (checkEdge) {//如果检查范围 //检查是否超出布局范围 isOutOfVertialEdge = isOutVerticalEdge(overwriteInsert, drawItem, disp, topPos, firstItem, lastItem); } if (isOutOfVertialEdge) {//如果超出布局范围,等待河蟹 topPos = 0; willHit = true; lines = 1; } elseif (removeItem != null) {//上面可以插入目标弹幕的逻辑用上了 lines--;//因为参考弹幕和目标弹幕在同一行,但是每进入while循环一次就将行号+1,所有要减回去和参考弹幕保持相同行号 } if (topPos == 0) {//方便加入容器 shown = false; } } //这是河蟹规则,都是在设置DanmakuContext时指定的,比如最大行数限制,重复限制等等。 //这里限于篇幅已经太长了,也实在写不动了,就不再跟下去了。内部逻辑也不难,大家有兴趣可以自己看看。 if (verifier != null && verifier.skipLayout(drawItem, topPos, lines, willHit)) { return; }
if (isOutOfVertialEdge) {//mVisibleDanmakus中所有弹幕绘制出来都超出范围了 clear(); } //这才是真正确认弹幕位置的地方 drawItem.layout(disp, drawItem.getLeft(), topPos);
if (!shown) {//如果还未显示,则加入即将显示的容器中。可以看到,最终会把所有截取的弹幕加入到这个容器里 mVisibleDanmakus.removeItem(removeItem);//移除同一行之前的参考弹幕,保持保存的一行只有一条弹幕,上面说明过 mVisibleDanmakus.addItem(drawItem); }
} //清除容器,重新放入新的内容 publicvoidclear(){ mCancelFixingFlag = true; mVisibleDanmakus.clear(); } |
这绝对是我写的注释最多的方法了ToT。。。。。。其实思路挺好理解的,通俗地讲就是这样的过程:
- 先添往最第一行添加一条弹幕,把它存到一个容器里(这个容器会把新添加进来的弹幕按照y值从小到大排序,而且容器只保存每一行的最后一条弹幕)。
- 然后添加第二条弹幕,从第一行开始添加,先判断和第一条弹幕会不会碰壁,如果不会碰壁则添加到这一行,然后容器内移除之前第一条的弹幕,保存这一条弹幕;如果会碰壁则添加到下一行,然后容器保存这条弹幕;
- 然后添加第三条,继续从第一行开始添加,先判断和第一条……(重复第二条的逻辑)……;
。。。。。。
就是这么个思路,但是写起来真心不是随意就能写出来的。即使先不说写,把这个思路想出来,让我去设计一套规则,估计都相当困难啊。唉,人与人之间的差距始终在思维。。。。。。
扯远了,我们继续回归正题,上面逻辑完成了弹幕定位规则(内部那个layout接下来再讲),限于篇幅,我只挑一个检查碰撞的代码贴出来分析,其它的请有兴趣者自行跟踪。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
publicstaticbooleanwillHitInDuration(IDisplayer disp, BaseDanmaku d1, BaseDanmaku d2, long duration, long currTime) {//disp, item, drawItem, drawItem.getDuration(), drawItem.getTimer().currMillisecond finalint type1 = d1.getType(); finalint type2 = d2.getType(); // allow hit if different type 不同类型的弹幕允许碰撞 if(type1 != type2) returnfalse; if(d1.isOutside()){//item已经跑出视图了,不存在碰撞问题 returnfalse; } long dTime = d2.time - d1.time; if (dTime <= 0)//drawItem在item前面,已经碰撞了 returntrue; //两者出现时间已经相差一条弹幕时间了 || item超时跑出去了 || drawItem超时,都不会碰撞 if (Math.abs(dTime) >= duration || d1.isTimeOut() || d2.isTimeOut()) { returnfalse; } //item和drawItem都是顶部或者底部固定弹幕,因为在同一行,必定碰撞 if (type1 == BaseDanmaku.TYPE_FIX_TOP || type1 == BaseDanmaku.TYPE_FIX_BOTTOM) { returntrue; } //调用checkHitAtTime方法 return checkHitAtTime(disp, d1, d2, currTime) || checkHitAtTime(disp, d1, d2, d1.time + d1.getDuration()); } //调用checkHitAtTime方法 privatestaticbooleancheckHitAtTime(IDisplayer disp, BaseDanmaku d1, BaseDanmaku d2, long time){//time = currTime || time = item.time + item.duration finalfloat[] rectArr1 = d1.getRectAtTime(disp, time);//time获得item在视图的(l,t,r,b) finalfloat[] rectArr2 = d2.getRectAtTime(disp, time);//time获得drawItem在视图的(l,t,r,b) if (rectArr1 == null || rectArr2 == null) returnfalse; return checkHit(d1.getType(), d2.getType(), rectArr1, rectArr2); } //调用checkHit方法 privatestaticbooleancheckHit(int type1, int type2, float[] rectArr1, float[] rectArr2) { if(type1 != type2) returnfalse; if (type1 == BaseDanmaku.TYPE_SCROLL_RL) {//只要drawItem的left小于item的right就碰撞了 // hit if left2 < right1 return rectArr2[0] < rectArr1[2]; } if (type1 == BaseDanmaku.TYPE_SCROLL_LR){ // hit if right2 > left1 return rectArr2[2] > rectArr1[0]; } returnfalse; } //R2LDanmaku的getRectAtTime方法 publicfloat[] getRectAtTime(IDisplayer displayer, long time) {//time = currTime || time = item.time + item.duration if (!isMeasured()) returnnull; float left = getAccurateLeft(displayer, time);//获得此时弹幕在视图的x坐标 if (RECT == null) { RECT = newfloat[4]; } RECT[0] = left;//left RECT[1] = y;//top RECT[2] = left + paintWidth;//right RECT[3] = y + paintHeight;//bottom return RECT; } //R2LDanmaku的getAccurateLeft方法 protectedfloatgetAccurateLeft(IDisplayer displayer, long currTime){//currTime = timer.currTime || currTime = item.time + item.duration long elapsedTime = currTime - time;//当前时间 - 弹幕出现时间 ...... //因此返回弹幕位于视图的x坐标,即视图宽度 - 弹幕已经显示了多少秒 * 每秒移动步长 return displayer.getWidth() - elapsedTime * mStepX; } |
检查碰撞逻辑比较简单,就是先根据当前时间就算出两条弹幕的位置(l1,t1,r1,b1),看看是否前面弹幕的 r1 小于后面弹幕的 l1;再根据前面弹幕的结束时间,计算出两条弹幕的位置(l2,t2,r2,b2)再次看看是否前面弹幕的 r2小于后面弹幕的 l2。只有两条都满足才不会碰撞。
好了检测碰撞就先到这里,然后继续回到AlignTopRetainer的fix方法,还有一个drawItem.layout(disp,drawItem.getLeft(), topPos);没讲呢,这才是真正确认弹幕位置的地方,继续查看L2RDanmaku的layout方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
publicvoidlayout(IDisplayer displayer, float x, float y){//disp, drawItem.getLeft(), topPos if (mTimer != null) { long currMS = mTimer.currMillisecond; long deltaDuration = currMS - time;//计算出出现时间和当前时间的时间差 if (deltaDuration > 0 && deltaDuration < duration.value) {//如果还没有到出现时间或者超出弹幕时间 this.x = getAccurateLeft(displayer, currMS);//计算出当前时间弹幕的x坐标,上面刚讲过 if (!this.isShown()) { this.y = y;//把上面计算好的y值赋过来 this.setVisibility(true); } mLastTime = currMS; return; } mLastTime = currMS; } this.setVisibility(false); } |
这样弹幕的位置也就确定了,layout步骤就走完了。下一步就是draw步骤了。
4)绘制弹幕:
赶紧回到DanmakuRenderer的draw方法,这个时候千万不要把自己搞晕了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
//DanmakuRenderer的draw方法 public RenderingState draw(IDisplayer disp, IDanmakus danmakus, long startRenderTime){ ...... IDanmakuIterator itr = danmakus.iterator(); ......
BaseDanmaku drawItem = null; while (itr.hasNext()) {
drawItem = itr.next();
...... ...检查是否建立缓存... ......
...是否测量...
...layout布局...
// draw //绘制弹幕 if (!drawItem.isOutside() && drawItem.isShown()) { if (drawItem.lines == null && drawItem.getBottom() > disp.getHeight()) { continue; // skip bottom outside danmaku ,忽略超过视图底部的弹幕 } //开始绘制 int renderingType = drawItem.draw(disp); if(renderingType == IRenderer.CACHE_RENDERING) {//如果是使用缓存bitmap绘制的 ...... } elseif(renderingType == IRenderer.TEXT_RENDERING) {//如果使用缓存绘制失败,则会使用原声方法Canvas去draw ...... if (mCacheManager != null) { mCacheManager.addDanmaku(drawItem);//再次为词条弹幕构建缓存,以便下次使用缓存bitmap绘制 } } ...... } |
继续跟踪int renderingType = drawItem.draw(disp) 这里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
//BaseDanmaku的draw方法 publicintdraw(IDisplayer displayer){ return displayer.draw(this);//调用AndroidDisplayer的draw方法 } //调用AndroidDisplayer的draw方法 publicintdraw(BaseDanmaku danmaku){ float top = danmaku.getTop();//弹幕在视图的y值 float left = danmaku.getLeft();//弹幕在视图的x值 if (canvas != null) {
...... // drawing cache boolean cacheDrawn = false; int result = IRenderer.CACHE_RENDERING; IDrawingCache> cache = danmaku.getDrawingCache(); if (cache != null) {//如果弹幕有缓存 //取出缓存 DrawingCacheHolder holder = (DrawingCacheHolder) cache.get(); if (holder != null) { //DrawingCacheHolder的draw方法,我们在上面的buildCache时分析过了,将每一条弹幕的bitmap绘制到视图的canvas上 cacheDrawn = holder.draw(canvas, left, top, alphaPaint); } } if (!cacheDrawn) {//如果缓存绘制失败 ...... //则使用Android原生的canvas.drawText等方法绘制,drawDanmaku方法我们上面buildCache时也分析过 drawDanmaku(danmaku, canvas, left, top, false); result = IRenderer.TEXT_RENDERING; }
return result; }
return IRenderer.NOTHING_RENDERING; } |
上面逻辑比较简单,先查看弹幕有没有缓存,如果有,就使用缓存绘制。在上面的buildCache时我们知道,缓存绘制的每一条弹幕都是一条bitmap,所以这里用缓存也是将bitmap绘制到视图的Canvas中。如果使用缓存绘制失败,会调用drawDanmaku方法,这个方法我们在上面的buildCache也分析过,则使用Android原生的canvas.drawText等绘制。
这样弹幕就被绘制到视图界面上了。
终于完了,以上就是DanmakuFlameMaster的流程分析过程了,分析的快吐学了ToT。。。。。。
TODO
上面刚开始奖CacheManagingDrawTask时曾经说过,也可以不用CacheManagingDrawTask,直接使用DrawTask,只要将DanmakuView的mEnableDanmakuDrwaingCache变量改为false就可以了。这样改动之后就用不上工程里那些so库了,也就不用建立那么复杂的缓存机制。
还有一点区别就是使用CacheManagingDrawTask画出来的每一条弹幕都是bitmap,而用DrawTask的弹幕都是Canvas.drawText画出来的。
限于篇幅,DrawTask就不分析了,逻辑比CacheManagingDrawTask简单多了,大家有兴趣的自己看看。
结语
DanmakuFlameMaster到此就分析完全了,简单总结一下流程就是:
- 加载弹幕资源
- 开启缓存机制,不断建立缓存和回收
- 开始绘制任务,根据定时器时间确定弹幕位置,绘制弹幕
这篇文章写的过程中也是十分蛋疼的,写的我差点over了。因为DanmakuFlameMaster源码实在太复杂了,坑非常多,所以很多细节都没有顾及。下次我绝对不会再写这么长的文章了,身体和脑力真心伤不起啊。赶紧休息一下~~~~~