Nico随笔


  • 首页

  • 归档

  • 标签

My Son

发表于 2018-08-01 | 分类于 项目经历

牛哔的对话

牛哔的对话是一种新型的对话体小说,颠覆了以往的小说阅读模式。用户可以听故事,语音评论、实时打赏、围观直播。

官网:http://www.niubichat.com

1. 首页

prepare

2. 牛哔的大脑在线答题

prepare

question

blackboard

lottory

3. 创建故事

live

4. 围观直播

live

live

作者端夜间模式

live

5. 配音故事

live

live

微历

微历是一款多人协作日程的工具日历

官网: http://wecal.ai/

1. 首页

live
live

2. 新建日程

live
live

3. 日程详情与协作

live

live

DailyTop

DailyTop是一款针对印度市场的新闻聚合阅读工具,目标是成为印度版的今日头条

http://m.zcool.com.cn/work/ZMjI5MTU5OTI=.html

live

live

Glide源码解析

发表于 2018-04-08 | 分类于 Android

see

1.请求流程

GlideApp.with(mActivity)
    .load(thumbnail)
    .placeholder(defaultResId)
    .into(this); 
  1. GlideApp.with(mActivity) 获取RequestManager
  2. into(this)

    Request previous = target.getRequest();
    
    if (previous != null) {
     requestManager.clear(target);
    }
    
    requestOptions.lock();
    Request request = buildRequest(target);
    target.setRequest(request);
    requestManager.track(target, request);  
    
  3. com.bumptech.glide.request.SingleRequest#begin 开始量尺寸

    if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
      onSizeReady(overrideWidth, overrideHeight);
    } else {
      target.getSize(this);
    }
    

onSizeReady后engine正式启动请求

loadStatus = engine.load(
glideContext,
model,
requestOptions.getSignature(),
this.width,
this.height,
requestOptions.getResourceClass(),
transcodeClass,
priority,
requestOptions.getDiskCacheStrategy(),
requestOptions.getTransformations(),
requestOptions.isTransformationRequired(),
requestOptions.isScaleOnlyOrNoTransform(),
requestOptions.getOptions(),
requestOptions.isMemoryCacheable(),
requestOptions.getUseUnlimitedSourceGeneratorsPool(),
requestOptions.getUseAnimationPool(),
requestOptions.getOnlyRetrieveFromCache(),
this);
  1. 尝试重cache 和activeResource里面获取资源

    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,

    resourceClass, transcodeClass, options);
    

    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
    if (active != null) {
    cb.onResourceReady(active, DataSource.MEMORY_CACHE);
    if (Log.isLoggable(TAG, Log.VERBOSE)) {

    logWithTimeAndKey("Loaded resource from active resources", startTime, key);
    

    }
    return null;
    }

    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
    if (cached != null) {
    cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
    if (Log.isLoggable(TAG, Log.VERBOSE)) {

    logWithTimeAndKey("Loaded resource from cache", startTime, key);
    

    }
    return null;
    }

  1. 从sd卡和网络获取数据
EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
if (current != null) {
  current.addCallback(cb);
  if (Log.isLoggable(TAG, Log.VERBOSE)) {
    logWithTimeAndKey("Added to existing load", startTime, key);
  }
  return new LoadStatus(cb, current);
}

EngineJob<R> engineJob =
    engineJobFactory.build(
        key,
        isMemoryCacheable,
        useUnlimitedSourceExecutorPool,
        useAnimationPool,
        onlyRetrieveFromCache);

DecodeJob<R> decodeJob =
    decodeJobFactory.build(
        glideContext,
        model,
        key,
        signature,
        width,
        height,
        resourceClass,
        transcodeClass,
        priority,
        diskCacheStrategy,
        transformations,
        isTransformationRequired,
        isScaleOnlyOrNoTransform,
        onlyRetrieveFromCache,
        options,
        engineJob);

jobs.put(key, engineJob);

engineJob.addCallback(cb);
engineJob.start(decodeJob);

最终会通过Glide里面定义的executor 获取数据

public void start(DecodeJob<R> decodeJob) {
  this.decodeJob = decodeJob;
  GlideExecutor executor = decodeJob.willDecodeFromCache()
      ? diskCacheExecutor
      : getActiveSourceExecutor();
  executor.execute(decodeJob);
}
  1. 获取数据的过程

最开进来时候是在 GlideExecutor diskCacheExecutor

com.bumptech.glide.load.engine.DecodeJob#runWrapped

private void runWrapped() {
    switch (runReason) {
     case INITIALIZE:
       stage = getNextStage(Stage.INITIALIZE);
       currentGenerator = getNextGenerator();
       runGenerators();
       break;
     case SWITCH_TO_SOURCE_SERVICE:
       runGenerators();
       break;
     case DECODE_DATA:
       decodeFromRetrievedData();
       break;
     default:
       throw new IllegalStateException("Unrecognized run reason: " + runReason);
   }
 }

       private void runGenerators() {
           currentThread = Thread.currentThread();
           startFetchTime = LogTime.getLogTime();
           boolean isStarted = false;
           while (!isCancelled && currentGenerator != null
               && !(isStarted = currentGenerator.startNext())) {
             stage = getNextStage(stage);
             currentGenerator = getNextGenerator();

             if (stage == Stage.SOURCE) {
               reschedule();
               return;
             }
           }
           // We've run out of stages and generators, give up.
           if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {
             notifyFailed();
           }
         }

这个地方实际上就是不断的生成NextGenerator ,在里面DataFetcherGenerator里面的startNext执行下一个操作,最后当NextStage为Stage.SOURCE时候切换回 GlideExecutor sourceExecutor;

private Stage getNextStage(Stage current) {
   switch (current) {
     case INITIALIZE:
       return diskCacheStrategy.decodeCachedResource()
           ? Stage.RESOURCE_CACHE : getNextStage(Stage.RESOURCE_CACHE);
     case RESOURCE_CACHE:
       return diskCacheStrategy.decodeCachedData()
           ? Stage.DATA_CACHE : getNextStage(Stage.DATA_CACHE);
     case DATA_CACHE:
       // Skip loading from source if the user opted to only retrieve the resource from cache.
       return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
     case SOURCE:
     case FINISHED:
       return Stage.FINISHED;
     default:
       throw new IllegalArgumentException("Unrecognized stage: " + current);
   }
 }

PS:判断使用何种ModuleLoader就是在SourceGenerator里实现的

  1. 数据获取到后回调

可能是直接从DecodeJob切回主线程 ,也可能是从SourceGenerator切回去的

com.bumptech.glide.load.engine.DecodeJob#notifyComplete

callback.onResourceReady(resource, dataSource);

com.bumptech.glide.load.engine.SourceGenerator#onDataReady

DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      dataToCache = data;

      cb.reschedule();
    } else {
      cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
          loadData.fetcher.getDataSource(), originalKey);
    }

具体切换的实现逻辑:

@Override
public void onResourceReady(Resource<R> resource, DataSource dataSource) {
    this.resource = resource;
    this.dataSource = dataSource;
    MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();
  }

 @Override
 public void onLoadFailed(GlideException e) {
    this.exception = e;
    MAIN_THREAD_HANDLER.obtainMessage(MSG_EXCEPTION, this).sendToTarget();
  }

子线程转回主线程的方法:

   private static final Handler MAIN_THREAD_HANDLER =
         new Handler(Looper.getMainLooper(), new MainThreadCallback());   

private static class MainThreadCallback implements Handler.Callback {

       @Synthetic
       @SuppressWarnings("WeakerAccess")
       MainThreadCallback() { }

       @Override
       public boolean handleMessage(Message message) {
         EngineJob<?> job = (EngineJob<?>) message.obj;
         switch (message.what) {
           case MSG_COMPLETE:
             job.handleResultOnMainThread();
             break;
           case MSG_EXCEPTION:
             job.handleExceptionOnMainThread();
             break;
           case MSG_CANCELLED:
             job.handleCancelledOnMainThread();
             break;
           default:
             throw new IllegalStateException("Unrecognized message: " + message.what);
         }
         return true;
       }
     }
  1. 显示数据及缓存

    com.bumptech.glide.load.engine.EngineJob#handleResultOnMainThread  {
    
       ..... 
       engineResource.acquire();
        listener.onEngineJobComplete(this, key, engineResource);
    
        int size = cbs.size();
        for (int i = 0; i < size; i++) {
          ResourceCallback cb = cbs.get(i);
          if (!isInIgnoredCallbacks(cb)) {
            engineResource.acquire();
            cb.onResourceReady(engineResource, dataSource);
          }
        }
        // Our request is complete, so we can release the resource.
        engineResource.release();
    
        release(false /*isRemovedFromQueue*/);
    }
    

ps: 其实在cb.onResourceReady(engineResource, dataSource); 里面有release();

2.核心技术点

2.1 AppGlideModule和ModuleLoader

AppGlideModule#applyOptions 配置builder参数

@Override
public void applyOptions(Context context, GlideBuilder builder) {
    final int diskCacheSizeBytes = 200 * 1024 * 1024; // 200 MB
    builder.setDiskCache(new DiskLruCacheFactory(MidData.ImageDir, diskCacheSizeBytes));
    judgeIfIsLowDevice(context,builder);
    super.applyOptions(context, builder);
}

根据尺寸加载不同的url

 @Override
    public void registerComponents(Context context, Glide glide, Registry registry) {
        registry.append(GlideUrl.class, InputStream.class, new SizeCompatLoader.SizeCompatFactory());

//        registry.prepend(String.class, ByteBuffer.class, new Base64ModelLoaderFactory());
    }

moudleloader的定义demo

public class SizeCompatLoader extends OkHttpUrlLoader {

    public SizeCompatLoader(Call.Factory client) {
        super(client);
    }

    @Override
    public boolean handles(GlideUrl url) {
        return super.handles(url);
    }

    @Override
    public LoadData<InputStream> buildLoadData(GlideUrl model, int width, int height, Options options) {
        ELog.e("url:"+model.toStringUrl());
        return super.buildLoadData(model, width, height, options);
    }
    ........

    public static class SizeCompatFactory extends  OkHttpUrlLoader.Factory{
    private static volatile Call.Factory internalClient;
    private Call.Factory client;

    private static Call.Factory getInternalClient() {
        if (internalClient == null) {
            synchronized (Factory.class) {
                if (internalClient == null) {
                    internalClient = new OkHttpClient();
                }
            }
        }
        return internalClient;
    }

    /**
     * Constructor for a new Factory that runs requests using a static singleton client.
     */
    public SizeCompatFactory() {
        this(getInternalClient());
    }

    /**
     * Constructor for a new Factory that runs requests using given client.
     *
     * @param client this is typically an instance of {@code OkHttpClient}.
     */
    public SizeCompatFactory(Call.Factory client) {
        this.client = client;
    }

    @Override
    public ModelLoader<GlideUrl, InputStream> build(MultiModelLoaderFactory multiFactory) {
        return new SizeCompatLoader(client);
    }

    @Override
    public void teardown() {
        // Do nothing, this instance doesn't own the client.
    }
    }
}  

实际上可以注册多个moudleloader,使用过程中,通过handle(GlideUrl url) 决定是否可以使用这个加载器
在请求流程里面是在SourceGenerator判断moudleloader使用哪一个的:

public boolean startNext() {
  if (dataToCache != null) {
    Object data = dataToCache;
    dataToCache = null;
    cacheData(data);
  }

  if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
    return true;
  }
  sourceCacheGenerator = null;

  loadData = null;
  boolean started = false;
  while (!started && hasNextModelLoader()) {
    loadData = helper.getLoadData().get(loadDataListIndex++);
    if (loadData != null
        && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
        || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
      started = true;
      loadData.fetcher.loadData(helper.getPriority(), this);
    }
  }
  return started;
}

see

2.2 内存缓存

  • cache (LRUCache) :不在使用的会放入到 cache里面 (因为每次从resource.acquire 会导致计数器加1,不再使用时候resource.release 会导致计数器减1,当计数器的值减为0时候就会放入cache中)

  • activeresource(HashMap+WeakPreference) : 正在使用的会放入到activeresource里面

内存缓存的读取

see
see

activeresource的put

  1. 在最后onEngenJobComplete时候
    see

activeresource的移除

  1. 在从activeresource里面获取资源后

    WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
        if (activeRef != null) {
          active = activeRef.get();
          if (active != null) {
            active.acquire();
          } else {
            activeResources.remove(key);
          }
        }
    
  2. 当资源被释放后

    public void onResourceReleased(Key cacheKey, EngineResource resource) {
      Util.assertMainThread();
      activeResources.remove(cacheKey);
      if (resource.isCacheable()) {
        cache.put(cacheKey, resource);
      } else {
        resourceRecycler.recycle(resource);
      }
    }
    

有两种原因导致资源被立即释放

a. 最后 handleResultOnMainThread时候 每次enginResource计数器减1,当为0,时候会移入cache,同时从activeresource移除
see

b. 当into(Target)时候判断上一个还有绑定的资源时候的clear过程
see

最终会导致realease过程
see

cache的put操作

  1. 读取内存时候从activeresource获取到了元素:

  2. 当resource的计数器变成0的时候

2.3 生命周期的绑定(通过一个隐形的fragment与activity的生命周期绑定)

com.bumptech.glide.manager.RequestManagerRetriever#get(android.content.Context)

public RequestManager get(Activity activity) {
    if (Util.isOnBackgroundThread()) {
      return get(activity.getApplicationContext());
    } else {
      assertNotDestroyed(activity);
      android.app.FragmentManager fm = activity.getFragmentManager();
      return fragmentGet(activity, fm, null /*parentHint*/);
    }
  }

private RequestManager fragmentGet(Context context, android.app.FragmentManager fm,
android.app.Fragment parentHint) {
        RequestManagerFragment current = getRequestManagerFragment(fm, parentHint);
        RequestManager requestManager = current.getRequestManager();
        if (requestManager == null) {
        // TODO(b/27524013): Factor out this Glide.get() call.
        Glide glide = Glide.get(context);
        requestManager =
        factory.build(glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode());
        current.setRequestManager(requestManager);
}
return requestManager;
}

其中RequestManagerFragment hook了所有的Fragment的生命周期

关键属性ActivityFragmentLifecycle lifecycle:会在这里面pause request

2.3 自定自定义transform

GlideApp.with(activity)
        .load(thumbnail)
        .transition(new DrawableTransitionOptions().crossFade()) //自定义 淡入淡出效果
        .transform(new MultiTransformation<Bitmap>(new CenterCrop(), new BlurTransformation())) //自定义图片后续处理比如 缩放、旋转、蒙灰 、或者高斯模糊
        .into(this);

高斯模糊BlurTransformation:

public class BlurTransformation extends BitmapTransformation {

.....

@Override
protected Bitmap transform(@NonNull Context context, @NonNull BitmapPool pool,
                           @NonNull Bitmap toTransform, int outWidth, int outHeight) {

    int width = toTransform.getWidth();
    int height = toTransform.getHeight();
    int scaledWidth = width / sampling;
    int scaledHeight = height / sampling;

    Bitmap bitmap = pool.get(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);

    Canvas canvas = new Canvas(bitmap);
    canvas.scale(1 / (float) sampling, 1 / (float) sampling);
    Paint paint = new Paint();
    paint.setFlags(Paint.FILTER_BITMAP_FLAG);
    canvas.drawBitmap(toTransform, 0, 0, paint);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
        try {
            bitmap = BlurRS.blur(context, bitmap, radius);
        } catch (RSRuntimeException e) {
            bitmap = BlurFast.blur(bitmap, radius, true);
        }
    } else {
        bitmap = BlurFast.blur(bitmap, radius, true);
    }

    return bitmap;
}

......

}

5 Glide预加载

参考文章:https://blog.csdn.net/qq_27429143/article/details/78562477

Glide Recyclerview 预加载插件,可以显著节约时间

compile (“com.github.bumptech.glide:recyclerview-integration:4.3.0”) { 
  // Excludes the support library because it’s already included by Glide. 
  transitive = false 
} 

原理是监控RecycleView的滚动 提前请求图片数据

PreloadSizeProvider sizeProvider = 
    new FixedPreloadSizeProvider(imageWidthPixels, imageHeightPixels);
PreloadModelProvider modelProvider = new MyPreloadModelProvider();
RecyclerViewPreloader<Photo> preloader = 
    new RecyclerViewPreloader<>(
        Glide.with(this), modelProvider, sizeProvider, 10 /*maxPreload*/);

RecyclerView myRecyclerView = (RecyclerView) result.findViewById(R.id.recycler_view);
myRecyclerView.addOnScrollListener(preloader);

// Finish setting up your RecyclerView etc.
myRecylerView.setLayoutManager(...);
myRecyclerView.setAdapter(...);     

PreLoader:

private class MyPreloadModelProvider implements PreloadModelProvider {
  @Override
  @NonNull
  List<U> getPreloadItems(int position) {
    String url = myUrls.get(position);
    if (TextUtils.isEmpty(url)) {
      return Collections.emptyList();
    }
    return Collections.singletonList(url);
  }

  @Override
  @Nullable
  RequestBuilder getPreloadRequestBuilder(String url) {
    return 
      GlideApp.with(fragment)
        .load(url) 
        .override(imageWidthPixels, imageHeightPixels);
  }
}

注意从 getPreloadRequestBuilder 中返回的 RequestBuilder ,必须与你从 onBindViewHolder 里启动的请求使用完全相同的一组选项 (占位符, 变换等) 和完全相同的尺寸。

4 参考资料

Glide整体流程: http://www.cnblogs.com/android-blogs/p/5735655.html

Glide缓存: http://www.cnblogs.com/android-blogs/p/5737611.html

Glide绑定activity生命周期: http://www.jishux.com/plus/view-662982-1.html

关于图片所占内存的大小 http://dev.qq.com/topic/591d61f56793d26660901b4e

埋点系统设计

发表于 2018-04-01 | 分类于 Android

埋点系统的设计

  1. 插件化
  2. 回调统计
  3. 支持滚动列表的展示统计
  4. 支持时长统计
  5. 数据批量打包上传
  6. 完善的统计上传支持,中途断网数据不丢

1. 整体流程图

image

接口封装

/**
 * @param event_type  事件类型
 * @param c_id  事件Id(服务器定义)
 * @param md  moudleId
 * @param is_anchor  是否立即上传
 * @param args  附加参数
 */
public static void eventTongji(String event_type, int c_id, int md, int is_anchor, String args) 

2. 插件

2.1 插件的初始化

public boolean initController(Context initContext)

2.2 插件统计入口

埋点数据在主App里面会把数据转成Json,然后以反射的形式调用插件里面UGCLoader的addUgcEvent方法

public void addEventUGC(final Context context, final String eventData)  

然后会把把事件解析成 UGCEvent,根据实际情况需要需要回调Url的话会直接开始回调给第三方统计平台.最后事件会封装成LoadEventUGCRequest:

private static class LoadEventUGCRequest implements LoadRequest {
        public TongjiEvent bean;

        public LoadEventUGCRequest(TongjiEvent bean) {
            this.bean = bean;
        }

        @Override
        public void processRequest(UGCLoader dataLoader) {

        }
    }  

常见的几种LoadRequest

类名 作用
LoadEventUGCRequest 打点事件
StoreUGCRequest 存储事件
UploadEventUGCRequest 强制上传事件

2.3 LoaderThread

事件加入阻塞队列loaderQueue后,负责取出事件,执行其中processEvent

 while (true) {
    try {
            LoadRequest request = queue.take();
            ......
            request.processRequest(loader);
    } catch (InterruptedException e) {
       e.printStackTrace();
    }

}

2.4 数据暂存和上传

数据取出来后悔有再次加入到等待上传队列eventList
当数据达到一定条件后执行上传操作

if (bean.is_anchor == 1 || eventList.size() >= PeacockController.UPLOAD_LIMIT_COUNT) {
                   //满30条上传或is_anchor立即上传不为0
                   UgcUploadManager manager = new UgcUploadManager();
                   ArrayList<TongjiEvent> uploadBeans = new ArrayList<TongjiEvent>();
                   uploadBeans.addAll(eventList);
                   eventList.clear();
                   manager.uploadLogUgc(mContext, uploadBeans);
               }

上传流程

  • 先判断EventLogTable是否有未上传的事件,有则加入到eventList,同时清空EventLogTable
  • 再判断UuidDataCache是否有上传失败的报文,统一放入post参数中
  • 然后把这条即将上传报文插入UuidDataCache

因此最后待上传的报文uploadData包含有:

[   currentUploadEventData,
    faliedUploadEventData_0,
    faliedUploadEventData_1
] 

ps:最后的本地生成的UUID 其实没有作用

  • 加密压缩上传
  • 上传成功后删除UuidDataCache里面对应的UUID报文

3.2 埋点自查

  • 一个系统级的浮层

image

  • mock接口自查
    直接mock这个中间数据解析,统计结果,方便单客户端自查

3.高阶封装

3.1 页面时长统计

在基类的onResume开始计时,在onPause时候停止计时,然后上报,即可统计本次展示时长

3.2 列表展示统计

在ListView Idle时候,递归循环遍历所有的子View,找出所有的TongjiLayout,然后调用其统计方法(统计数据已经预设进去了)

    /**
     * 获取ViewGroup里所有展现的 TongjiLayout(坑位)
     * @param top    统计区间的顶部坐标
     * @param bottom 统计区间的底部坐标
     */
    public synchronized static void viewAllTongjiLayouts(ViewGroup group, int top, int bottom) {
        try {
            if (group == null) {
                return;
            }

        if (group.getVisibility() == View.VISIBLE && group instanceof TongjiLayout) {
            TongjiLayout layout = (TongjiLayout) group;
            layout.tongjiView(top, bottom);
            return;
        }

        int count = group.getChildCount();
        for (int i = 0; i < count; i++) {
            View child = group.getChildAt(i);
            if (child.getVisibility() == View.VISIBLE && child instanceof ViewGroup) {
                if (child instanceof TongjiLayout) {
                    TongjiLayout layout = (TongjiLayout) child;
                    layout.tongjiView(top, bottom);
                } else {
                    viewAllTongjiLayouts((ViewGroup) child, top, bottom);
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

3.2.1 PV统计的限制条件

  • 展示1/2之上
/**
 * 检测是否该坑位坐标满足,漏出1/2即满足条件
 */
public boolean checkIsItemLocalRight(int top, int bottom) {
    try {
        int[] localtion = new int[2];
        /**获取该View在屏幕中的位置*/
        getLocationOnScreen(localtion);
        //高和宽都显示大于一半则为true
        return localtion[1] > top - getHeight()/2 && localtion[1] < bottom - getHeight() / 2&& localtion[0] > -getWidth() / 2 && localtion[0] < MidData.main_screenWidth - getWidth() / 2;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}
  • 10s内统计一次
String key = ad_item_id + "#" + md + "#" + pos + "#" + args + "#" + card_id;
if (ETADUtils.getItemTimeMap().containsKey(key)) {
    return;
}

3.2.2 fragment的生命周期统计

参考文章
https://blog.csdn.net/tongcpp/article/details/41978751
https://www.jianshu.com/p/850556d33f63
同时也可以考虑统计View的可见性

参考文档:

Umeng统计设计: https://developer.umeng.com/docs/67953/detail/68140

埋点的设计问题

必须分模块,因为现在的组件都是相互集成,不知道,有可能在别的App集成了这个模块,这时候需要注意埋点参数必须有至少一个subCat表述所在位置

Moudle
SubMoudle
ItemId
EventType
InstantUpload
Properties

Hugo方法耗时监控

发表于 2018-03-02 | 分类于 Android

使用方法

  1. 在Android Project的build.gradle添加dependence:

    buildscript {
        repositories {
            jcenter()
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:2.3.3'
            classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'//关键所在
        }
    }
    
  2. 在Module的build.gradle文件中添加Hugo Plugin的应用

    apply plugin:'com.jakewharton.hugo'//关键所在  
    
  3. 在相应的方法前,添加@DebugLog

    import hugo.weaving.DebugLog;

    ……

    @DebugLog //关键所在
    private int add(int a, int b){

    return a+b;
    

    }

ps:调用Hugo后,Hugo自动生效。由于仅在Debug中生效,因此,可以不用关闭

扩展思维

“apply plugin”的作用

作用申明 构建的项目类型

butterknife原理

  1. 第一步 定义注解类型:InjectView
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView {
    /**控件的id*/
    int id() default -1;
}
  1. 根据解析当前类的注解

    /**根据注解自动解析控件*/
    private void analyseInjectView(){
        try {
            Class clazz = this.getClass();
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields){
                InjectView injectView = field.getAnnotation(InjectView.class);
                if (injectView != null){
                    int id = injectView.id();
                    Log.e("lyc", "id->"+id);
                    if (id > 0){
                        field.setAccessible(true);
                        field.set(this, findViewById(id));
                    }
                }
            }
        }catch (Exception e){}
    }
    
  2. 在onCreate方法里面调用解析注解的方法

    analyseInjectView()
    

实际上apply plugin 就会在编译时候添加上解析注解的逻辑

类型值注解

interface GenderStatus {
     /**
      * 性别
      */
     String F = "F";
     String M = "M";
     String N = "N";


     //使用@IntDef的使用 代替enum
     //用 @IntDef "包住" 常量;
     // @Retention 定义策略
     // 声明构造器
     @StringDef({F, M, N})
     @Retention(RetentionPolicy.SOURCE)
     @interface GenderType {
     }
 }

StringDef定义:

@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface StringDef {
    /** Defines the allowed constants for this element */
    String[] value() default {};
}

参考文章

使用方法:http://blog.csdn.net/daihuimaozideren/article/details/78231983
原理分析:http://blog.csdn.net/xxxzhi/article/details/53048476
Retention、Target注解: http://blog.csdn.net/limj625/article/details/70242773
Java中的注解:http://www.cnblogs.com/peida/archive/2013/04/24/3036689.html

公众号爬取

发表于 2018-02-26 | 分类于 Python

源码:https://github.com/liuyicheng3/wxbSpider

目标

获取文章的h5链接,文章的title,文章的,文章的封面

Attention:

不要直接存储文章的内容,这样版权风险很大,
存文章的链接的话,我们可以直接显示文章的链接出处来声明来源

方案一

一个是通过 按键精灵加上proxy代理 拦截网络请求
也可以通过charles的auto save 功能(tools/Auto save)自动保存拦截到的数据,然后通过python的watchdog 监控目录下的数据变化(可以获得一个事件),解析每次网络请求的返回值了
参考资料:
https://github.com/lijinma/wechat_spider
https://github.com/251321639/wechat_brain

方案二 :第三方的微信接口

  1. 搜狗微信搜索
  2. newsrank
  3. 微小宝

2.1 搜狗搜索

问题:

  1. 文章的链接是有时效性的
    https://mp.weixin.qq.com/s?src=11&timestamp=1519960349&ver=729&signature=rR4FhNbqgqOhLeFrdvvRWr5cRF79uzrOuKGimM5FRK5xPMxF*28MHYaN*q9fawJIcGbdbK9qxiiOPfv7AY-dy4J6DYOg3Ub505vGoNC3uzt644CvvDiHTh7i*1Bgd0cU&new=1
    过了12h后就打不开了
  2. 无法确定获取的量,直接爬取分类的话量不足

2.2 新榜

公众号(可遍历)的量级:22*50
每个公众号可返回最新发布的10条
而每个公众号一天更新的量一般是5条,所以总量大约是2200,满足不了要求

2.3 微小宝

其中韦小宝在公众号列表里面的22个分类,一共大约有1w个公众号,每个公众号的日更大约有5篇
所以一共大约有5w条记录

微小宝爬虫需要注意的细节:

  1. 要登录
  2. 直接爬接口(接口请求时间间隔越短越容易被发现),过一段时间就会告知是robot,这时候请求一下首页输入一下验证码

  3. 请求公众号的详情时候 注意加一下 refer字段,这样避免被屏蔽

  4. 验证码是和session 绑定的,所以请求验证码也需要添加session 和 refer,然后直接调用unlock接口。由于第一次无法获取验证码的图片,建议点击一下验证码,重新获取一下验证码(开发者模式查看具体url规则)

温馨提示:

法律上的规定是,抓取公共展示的信息不违法,允许抓取网站内容,对公众展示内容必须提供来源及源站地址,若有版权纠纷,必须配合版权方进行内容下架,否则可以到工信部投诉举报侵权网站,投诉多了有取消域名备案的风险。
涉及版权的经济损失,可以提起诉讼申请经济赔偿。

其他

这个地方如果仅仅是爬到链接位置,有的链接不确定是否还可以访问,可以用 requests.head(url),请求一下判断resp.status_code 是不是200

数据统计和分析

发表于 2018-02-15 | 分类于 Android

1. Umeng高阶功能:

1.1 umeng 在线参数

可以用于 初始化接口以及A/B测试
(现在已经更新为plus 原在线参数功能不再接入新的App http://bbs.umeng.comthread-15553-1-1.html)

1.2 Umeng 用户时长分析

用户参与度 > 使用时长:
可以具体分析到某一天的ge个使用时长用户的占比 (可以选择一个月前同一天对比)
单次使用时长的过短就说明首页的分发,效果差。

1.3 用户活跃度

留存分析 > 用户活跃度:
具体看一天各个用户群体的占比情况
(看几天活跃用户)

1.4 功能使用

页面访问路径:看到用户的访问路径
自定义事件

2. growingIO

号称无埋点,但是要收费

2.1 热区

可以具体看到一个界面的用户点击的热点区域

3. 自定义pv统计

核心点在与统计用户看见了哪些东西

3.1 长容器统计

  1. 在容器停止滚动时候查看view的在屏幕的显示区域是在屏幕范围内

    view.getLocationOnScreen(locations);  
    

露出1/3即可算看见了

  1. 滚动停止就统计一次(如果一直不滚动就始终算1一次pv),另外10s只统计一次(防止用户来回滚动)

首页Cache导致崩溃

发表于 2018-02-13 | 分类于 Android

场景

由于首页cache错误导致Crash,然后每次启动都必crash(因为首先读取的是cache)

方案

step1:全局crash 捕获

实现UncaughtExceptionHandler


mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);

参考资料: http://blog.csdn.net/luck_apple/article/details/7768064

step2: 记录

记录crash 信息到本地(时间,连续次数)

step3: 第一个界面判断

如果判断连续次数达到一定数量,直接跳往BugFix界面,上传错误信息,同时服务器配合打热补丁
这个地方需要区别错误类型进行更细致的修复

参考文章:

iOS连续启动crash: https://yq.aliyun.com/articles/377294?spm=5176.10695662.1996646101.searchclickresult.65e96a04bcql83

EventBus3.0

发表于 2018-02-10 | 分类于 Android

EventBus3.0 优点:

EventBus 3由于使用了注解,比起使用反射来遍历方法的2.4版本逊色不少。但开挂之后(启用了索引)远远超出之前的版本。

性能对比

参考资料:https://www.cnblogs.com/bugly/p/5475034.html

开挂方法:

  1. 在当前moudle的gradle 添加

    apply plugin: 'com.neenbedankt.android-apt'   
    
  2. 在dependencies里面添加

    dependencies 
    compile 'org.greenrobot:eventbus:3.0.0'
    apt 'org.greenrobot:eventbus-annotation-processor:3.0.1'   
    
  1. 在使用时候

    @Subscribe
    public void helloEventBus(UserReLoginEvent  event){
        // TODO: 17/6/27  这是是为了测试而使用的
        UtilsManager.toast(mContext,"这是eventbus 3.0 可以支持的");
    }
    

事件源定位:

为了防止事件环路对EventBus加一个Wrapper,每次发送Event时候打印一下路径

private static void printWrapPath(String tagStr, Object... objects) {

   StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
   final StackTraceElement ste = stackTrace[5];

   StringBuilder stringBuilder = new StringBuilder();
   String className = ste.getFileName();
   if (!TextUtils.isEmpty(className)) {
       String methodName = ste.getMethodName();
       int lineNumber = ste.getLineNumber();
       stringBuilder.append("(").append(className).append(":").append(lineNumber).append(") #").append(methodName).append(" : ");
   } else {
       stringBuilder.append(" ");
   }

   String tag = (tagStr == null ? LOG_DEFAULT_TAG : tagStr);
   String msg = (objects == null) ? "null" : getObjectsString(objects);
   String headString = stringBuilder.toString();

    Log.println(type, tagStr, headString + msg);

}

混淆问题 :

混淆作为版本发布必备的流程,经常会闹出很多奇奇怪怪的问题,且不方便定位,尤其是EventBus这种依赖反射技术的库。通常情况下都会把相关的类和回调方法都keep住,但这样其实会留下被人反编译后破解的后顾之忧,所以我们的目标是keep最少的代码。

首先,因为EventBus 3弃用了反射的方式去寻找回调方法,改用注解的方式。作者的意思是在混淆时就不用再keep住相应的类和方法。但是我们在运行时,却会报java.lang.NoSuchFieldError: No static field POSTING。网上给出的解决办法是keep住所有eventbus相关的代码:

-keep class de.greenrobot.** {*;}  

其实我们仔细分析,可以看到是因为在SubscriberMethodFinder的findUsingReflection方法中,在调用Method.getAnnotation()时获取ThreadMode这个enum失败了,所以我们只需要keep住这个enum就可以了(如下)。

-keep public enum org.greenrobot.eventbus.ThreadMode { public static *; }   

这样就能正常编译通过了,但如果使用了索引加速,是不会有上面这个问题的。因为在找方法时,调用的不是findUsingReflection,而是findUsingInfo。但是使用了索引加速后,编译后却会报新的错误:Could not find subscriber method in XXX Class. Maybe a missing ProGuard rule?

这就很好理解了,因为生成索引GeneratedSubscriberIndex是在代码混淆之前进行的,混淆之后类名和方法名都不一样了(上面这个错误是方法无法找到),得keep住所有被Subscribe注解标注的方法:

-keepclassmembers class * {
    @de.greenrobot.event.Subscribe <methods>;
}

所以又倒退回了EventBus2.4时不能混淆onEvent开头的方法一样的处境了。所以这里就得权衡一下利弊:使用了注解不用索引加速,则只需要keep住EventBus相关的代码,现有的代码可以正常的进行混淆。而使用了索引加速的话,则需要keep住相关的方法和类。

生成的索引的 gradle配置:

apt {
    arguments {
        eventBusIndex "com.example.myapp.MyEventBusIndex"
    }
}   

生成的索引demo:

putIndex(new SimpleSubscriberInfo(com.lyc.MainActivity.class, true, new SubscriberMethodInfo[] {
           new SubscriberMethodInfo("helloEventBus", com.lyc.eventbus.UserReLoginEvent.class),
       }));

Android APT

参考资料:https://segmentfault.com/a/1190000005100468

生成分享图片

发表于 2018-02-10 | 分类于 Android

方法1

1.1 截取Activity

public static Bitmap takeScreenShot(Activity activity) {
       View view = activity.getWindow().getDecorView();
       if (view == null) {
           return null;
       }
       view.setDrawingCacheEnabled(true);
       view.buildDrawingCache();
       Bitmap b1 = view.getDrawingCache();
       if (b1 == null) {
           return null;
       }
       // 获取状态栏高度
       Rect frame = new Rect();
       activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
       int statusBarHeight = frame.top;

       // 获取屏幕长和高
       int width = activity.getWindowManager().getDefaultDisplay().getWidth();

       Bitmap b = Bitmap.createBitmap(b1, 0, statusBarHeight, width, b1.getHeight() - statusBarHeight);
       view.destroyDrawingCache();
       b1.recycle();
       return b;
   }

1.3 截取ViewGroup

public static Bitmap takeScreenShotView(LinnerLayout viewGroup) {
       Bitmap bitmap = null;
       if (viewGroup != null) {
           int h = 0;
           for (int i = 0; i < viewGroup.getChildCount(); i++) {
               h += viewGroup.getChildAt(i).getHeight();
           }
           if (viewGroup.getHeight() > h) {
               h = viewGroup.getHeight();
           }
           bitmap = Bitmap.createBitmap(viewGroup.getWidth(), h, Config.ARGB_8888);
           final Canvas c = new Canvas(bitmap);
           c.drawColor(Color.WHITE);
           viewGroup.draw(c);
       }
       return bitmap;
   }

方法2

自己生成bitmap绘制,这个地方由于自己绘制的比较麻烦,一般就是在一张图片上绘制一些文字即可

方法3

生成未加载的布局文件的分享图片

View targetView = LayoutInflater.from(mActivity).inflate(R.layout.view_share, null);
targetView.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),      View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));
targetView.layout(0, 0, width, height);
bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.WHITE);
view.draw(canvas);

核心是布局文件没有加载,也即没有measure和layout 就无法进行绘制(实际上一个view之所以能被绘制上去就是因为父容器measure和layout了它)才能进行绘制

一个知识点: view 不是不可以在后台线程修改的,而是需只有原始创建这个视图层次(view hierachy)的线程才能修改它的视图(view)

扩展思维

如何生成listview和recycleView的截屏

方案1

原理:我们可以知道第一个和最后一个view在屏幕上的位置,也可以获取获取整个屏幕的截图,所谓我们可以滑动一屏截取一屏(我们可以估算出一屏大约有多少元素,滚动固定长度即可)的内容,然后最后拼接出来

方案2

原理:可以逐个生成itemView的bitmap(采用方法3),然后拼接出来

pythonXML解析

发表于 2018-01-03 | 分类于 Python

常用模块

bs4 xml.dom.minidom xml.etree re

1. bs4用法

详细使用文档 https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.zh.html
http://www.cnblogs.com/twinsclover/archive/2012/04/26/2471704.html

from bs4 import BeautifulSoup

soup = BeautifulSoup(content, 'xml')  
itemList= soup.select('html > body > div[class="area"] > ul.plist2.cf.ulList > li > a')  
itemList2= soup.select('div[id="newsList"] > ul[id="v2"] > li[class="item"] > a ')   

判断书是否有属性的方法 tem.contents[0].attrs.has_key

for item in itemList:
   if item.contents[0].attrs.has_key('src'):
       avatar = item.contents[0]['src']
   elif item.contents[0].attrs.has_key('lz_src'):
       avatar = item.contents[0]['lz_src']
   else:
       avatar='unset'
       print item
   star = StarItem(item.text,avatar)
   currentPageStars.append(star)  

判断是否有子元素的方法 newsItemDiv.p
获取元素标签里面值的方法newsItemDiv.p.string

for index,newsItemDiv in enumerate(itemList):
   tranItem = NewsItem(newsItemDiv.p.string if newsItemDiv.p else "",newsItemDiv.img['data-src'] if newsItemDiv.img else "",proto+"/"+domain+newsItemDiv['href'])
   newsItems.append(str(tranItem)+",\n")

ps:ul.plist2.cf.ulList 达标这个ul 使用了多种样式 plist2 cf ulList

2. minidom

使用minidom解析器打开 XML 文档

DOMTree = xml.dom.minidom.parse(xmlPath)
collection = DOMTree.documentElement

在集合中获取所有colors

colors = collection.getElementsByTagName("color")

namelist = []
valuelist = []

打印每部电影的详细信息

for color in colors:
    if color.hasAttribute("name"):
        colorname=color.getAttribute("name")
        colorvalue = color.childNodes[0].data
        if not (colorname in namelist):
            namelist.append(colorname)
            currentIndex = namelist.index(colorname) 

3. etree

import xml.etree.ElementTree
tree = ElementTree.parse(lastYearPath )
allItems = tree.findall('data/item')
for pos,treeItem in enumerate(allItems):
    holidayYear = int(treeItem.attrib['year'])
    holidayMonth = int(treeItem.attrib['month'])
    holidayDate = int(treeItem.attrib['date'])

4. 正则

对于xml里面的注释很难读取出来建议使用

import re  
names = re.findall(r"<!\[CDATA\[(.*?)\]\]",data)
12…4
Liuycheng

Liuycheng

34 日志
8 分类
34 标签
© 2018 Liuycheng
由 Hexo 强力驱动
主题 - NexT.Muse