sendBroadcast静态广播源码分析

Android  源码分析  2023年12月15日 pm12:12发布10个月前更新 91es.com站长
135 0 0

前言

本次分析一下Android P开机后发送静态广播源码分析,记录一下,方便自己查阅。

部分流程跟前面的源码分析《startActivity源码分析》和《startService源码分析》都比较类似,所以重复的步骤就省略。

PS:启动startActivity的比较复杂,流程多,但搞懂了这个,startService和sendBroadcast的分析就更简单。

正文

静态和动态广播区别

  1. 生存期

    静态广播的生存期比动态广播的长很多。

  2. 优先级

    动态广播的优先级比静态广播高。

  3. 注册方式

    (1)静态广播需要AndroidManifest.xml注册和声明

    (2)动态广播通过registerReceiver()注册和unregisterReceiver()反注册

这先分析静态广播

#MainActivity.java中
Intent intent = new Intent();
intent.setPackage("com.biumall.server");
intent.setAction("com.biumall.server.ACTION_ONE");
sendBroadcast(intent);

静态广播注册,在AndroidManifest.xml中添加

<receiver
    android:name="com.biumall.server.receiver.StaticReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter android:priority="9999">
        <action android:name="com.biumall.server.ACTION_ONE" />
    </intent-filter>
</receiver>

广播的发送最终在ContextImpl.java中,我们从这里开始跟踪。

ContextImpl.java

sendBroadcast()
@Override
public void sendBroadcast(Intent intent) {
    warnIfCallingFromSystemProcess();
    String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
    try {
        intent.prepareToLeaveProcess(this);
        //ActivityManager.getService()获取的是ActivityManagerService
        ActivityManager.getService().broadcastIntent(
                mMainThread.getApplicationThread(), intent, resolvedType, null,
                Activity.RESULT_OK, null, null, null, AppOpsManager.OP_NONE, null, false, false,
                getUserId());
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

进入ActivityManagerService.broadcastIntent()

ActivityManagerService.java

broadcastIntent()
public final int broadcastIntent(IApplicationThread caller,
        Intent intent, String resolvedType, IIntentReceiver resultTo,
        int resultCode, String resultData, Bundle resultExtras,
        String[] requiredPermissions, int appOp, Bundle bOptions,
        boolean serialized, boolean sticky, int userId) {
    //略
    synchronized(this) {
        //验证广播合法性
        intent = verifyBroadcastLocked(intent);
        //略
        //继续,进入broadcastIntentLocked
        int res = broadcastIntentLocked(callerApp,
                callerApp != null ? callerApp.info.packageName : null,
                intent, resolvedType, resultTo, resultCode, resultData, resultExtras,
                requiredPermissions, appOp, bOptions, serialized, sticky,
                callingPid, callingUid, userId);
        Binder.restoreCallingIdentity(origId);
        return res;
    }
}
broadcastIntentLocked()

很长,不重要的大部分省略。

@GuardedBy("this")
final int broadcastIntentLocked(ProcessRecord callerApp,
        String callerPackage, Intent intent, String resolvedType,
        IIntentReceiver resultTo, int resultCode, String resultData,
        Bundle resultExtras, String[] requiredPermissions, int appOp, Bundle bOptions,
        boolean ordered, boolean sticky, int callingPid, int callingUid, int userId) {
    intent = new Intent(intent);
    //略 发送的是普通的广播,
    final String action = intent.getAction();
    //bOptions为null 
    if (bOptions != null) {
       //略
    }
    final boolean isProtectedBroadcast;
    try {
        //没有添加入包含名单,false
        isProtectedBroadcast = AppGlobals.getPackageManager().isProtectedBroadcast(action);
    } catch (RemoteException e) {
        return ActivityManager.BROADCAST_SUCCESS;
    }

    final boolean isCallerSystem;
    //判断是否系统应用发送
    switch (UserHandle.getAppId(callingUid)) {
        case ROOT_UID:
        case SYSTEM_UID:
        case PHONE_UID:
        case BLUETOOTH_UID:
        case NFC_UID:
        case SE_UID:
            isCallerSystem = true;
            break;
        default:
            isCallerSystem = (callerApp != null) && callerApp.persistent;
            break;
    }
    //我这是系统应用发送
    if (!isCallerSystem) {
        //略
    }
    //略
    //不是粘性广播,上面传入的也是false
    if (sticky) { 
        //略
    }
    //略
    List<BroadcastFilter> registeredReceivers = null;
    //获取广播接收者list
    if ((intent.getFlags()&Intent.FLAG_RECEIVER_REGISTERED_ONLY)== 0) {
        //这部分搜集的后面有空跟着一下
        receivers = collectReceiverComponents(intent, resolvedType, callingUid, users);
    }
    //发送广播时没有指定
    if (intent.getComponent() == null) {
        //略
    }
    //false
    final boolean replacePending =
            (intent.getFlags()&Intent.FLAG_RECEIVER_REPLACE_PENDING) != 0;
    //registeredReceivers为null,没有进入上面的赋值
    int NR = registeredReceivers != null ? registeredReceivers.size() : 0;
    //传入的ordered = false, NR= 0,跳过
    if (!ordered && NR > 0) {
        //略
    }
    //略
    //isCallerSystem为true没我这是系统应用发送广播
    if (isCallerSystem) {
        //检查权限
        checkBroadcastFromSystem(intent, callerApp, callerPackage, callingUid,
                isProtectedBroadcast, receivers);
    }
    //receivers广播接收者列表,不为null,且size()大于0
    if ((receivers != null && receivers.size() > 0)
            || resultTo != null) {
        //广播队列,这里会判断是前台广播还是后台广播
        BroadcastQueue queue = broadcastQueueForIntent(intent);
        //广播记录
        BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp,
                callerPackage, callingPid, callingUid, callerInstantApp, resolvedType,
                requiredPermissions, appOp, brOptions, receivers, resultTo, resultCode,
                resultData, resultExtras, ordered, sticky, false, userId);
        //此处replacePending为false
        final BroadcastRecord oldRecord =
                replacePending ? queue.replaceOrderedBroadcastLocked(r) : null;
        //oldrecord为null
        if (oldRecord != null) {
            //略
        } else {
            //[重]
            //添加BroadcastRecord到队列 mOrderedBroadcasts.add();
            queue.enqueueOrderedBroadcastLocked(r);
            //安排发送广播
            queue.scheduleBroadcastsLocked();
        }
    } else {
        //略
    }
    return ActivityManager.BROADCAST_SUCCESS;
}

BroadcastQueue.java

enqueueOrderedBroadcastLocked()
public void enqueueOrderedBroadcastLocked(BroadcastRecord r) {
    //添加广播到mOrderedBroadcasts
    mOrderedBroadcasts.add(r);
    enqueueBroadcastHelper(r);
}
scheduleBroadcastsLocked()
public void scheduleBroadcastsLocked() {
    if (mBroadcastsScheduled) {
        return;
    }
    //发送BROADCAST_INTENT_MSG
    mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_INTENT_MSG, this));
    //这里会吧mBroadcastsScheduled置为true
    mBroadcastsScheduled = true;
}
handleMessage()
@Override
public void handleMessage(Message msg) {
    switch (msg.what) {
        case BROADCAST_INTENT_MSG: {
            //发送下一个广播
            processNextBroadcast(true);
        } break;
        case BROADCAST_TIMEOUT_MSG: {
            //广播超时处理
            synchronized (mService) {
                broadcastTimeoutLocked(true);
            }
        } break;
    }
}
processNextBroadcast()
final void processNextBroadcast(boolean fromMsg) {
    synchronized (mService) {
        //fromMsg=true
        processNextBroadcastLocked(fromMsg, false);
    }
}
processNextBroadcastLocked()

进入这里就分两步了

  1. 广播接收者的进程启动过,就走processCurBroadcastLocked()

  2. 广播接收者的进程没启动过,需先启动进程startProcessLocked(),然后出了队列中的广播

如果进程没有启动,需要先启动(多了这部分),最后也会启动processCurBroadcastLocked()

当然,我们下面也发现了,优先处理了动态注册的的广播,然后再去执行后面静态广播。

final void processNextBroadcastLocked(boolean fromMsg, boolean skipOomAdj) {
    BroadcastRecord r;
    //略
    //fromMsg传入的为true
    if (fromMsg) {
        mBroadcastsScheduled = false;
    }
    //处理动态注册的广播,我们这是第一次给应用发静态广播
    //这里size()=0
    while (mParallelBroadcasts.size() > 0) {
        r = mParallelBroadcasts.remove(0);
        r.dispatchTime = SystemClock.uptimeMillis();
        r.dispatchClockTime = System.currentTimeMillis();
        final int N = r.receivers.size();
        for (int i=0; i<N; i++) {
            Object target = r.receivers.get(i);
            //发送动态注册的广播
            deliverToRegisteredReceiverLocked(r, (BroadcastFilter)target, false, i);
        }
        addBroadcastToHistoryLocked(r);
    }

    //mPendingBroadcast为null
    //mPendingBroadcast是在后面进行赋值,的表示Pending中的广播。
    //不过,这里不是很了解,跳过吧
    if (mPendingBroadcast != null) {
        //略
    }
    boolean looped = false;
    do {
        //mOrderedBroadcasts在enqueueOrderedBroadcastLocked添加的
        //mOrderedBroadcasts.size()>0
        if (mOrderedBroadcasts.size() == 0) {
            //略
            return;
        }
        //获取列表中的第一个
        r = mOrderedBroadcasts.get(0);
        boolean forceReceive = false;
        //numReceivers有一个,就是我们要发送的广播
        //当然,我们只针对我们发送的分析。
        int numReceivers = (r.receivers != null) ? r.receivers.size() : 0;
        //mService.mProcessesReady为true
        // r.dispatchTime=0,还在队列中,没法安排发送
        if (mService.mProcessesReady && r.dispatchTime > 0) {
            //略
        }
        //r.state 为 0 ,BroadcastRecord.IDLE 空闲
        if (r.state != BroadcastRecord.IDLE) {
            return;
        }
        //r.receivers 广播接收者
        //r.nextReceiver 下一个广播接收者,我这只有一个,所以为0
        //r.resultAbort false 终止广播值?这不太懂
        //forceReceive false 强制接受? 哈哈也不懂,,
        //反正,不满足条件
        if (r.receivers == null || r.nextReceiver >= numReceivers
                || r.resultAbort || forceReceive) {
            //略
        }
    } while (r == null);
    //上面知道nextReceiver为0
    //先赋值,后++,所以recIdx = 0
    int recIdx = r.nextReceiver++;
    //赋值接收时间
    r.receiverTime = SystemClock.uptimeMillis();
    //recIdx为0
    if (recIdx == 0) {
        //设置投递时间
        r.dispatchTime = r.receiverTime;
        r.dispatchClockTime = System.currentTimeMillis();
    }
    //mPendingBroadcastTimeoutMessage为false
    //还没设置超时广播
    if (! mPendingBroadcastTimeoutMessage) {
        long timeoutTime = r.receiverTime + mTimeoutPeriod;
        //设置广播超时60s时ANR(mTimeoutPeriod = 60)
        setBroadcastTimeoutLocked(timeoutTime);
    }

    final BroadcastOptions brOptions = r.options;
    //就是获取第一个广播接收者
    final Object nextReceiver = r.receivers.get(recIdx);
    // nextReceiver是ResolveInfo对象。,不是BroadcastFilter实例
    //不进入下面
    if (nextReceiver instanceof BroadcastFilter) {
        //略
        return;
    }
    ResolveInfo info =(ResolveInfo)nextReceiver;
    ComponentName component = new ComponentName(
            info.activityInfo.applicationInfo.packageName,
            info.activityInfo.name);
    boolean skip = false;
    //略
    //获取进程记录
    ProcessRecord app = mService.getProcessRecordLocked(targetProcess,
            info.activityInfo.applicationInfo.uid, false);
    //开头说了,我开机第一次就发送静态广播,接收广播的进程是没提前启动的
    //因此获取的app是为null

    //略

    r.manifestCount++;
    r.delivery[recIdx] = BroadcastRecord.DELIVERY_DELIVERED;
    r.state = BroadcastRecord.APP_RECEIVE;
    r.curComponent = component;
    r.curReceiver = info.activityInfo;
    //略
    // Broadcast is being executed, its package can't be stopped.
    try {
        AppGlobals.getPackageManager().setPackageStoppedState(
                r.curComponent.getPackageName(), false, UserHandle.getUserId(r.callingUid));
    } catch (RemoteException e) {
    } catch (IllegalArgumentException e) {
        Slog.w(TAG, "processNextBroadcastLocked Failed trying to unstop package "
                + r.curComponent.getPackageName() + ": " + e);
    }
    //如果不为null,表示进程已经启动
    //如果启动了就走这里
    if (app != null && app.thread != null && !app.killed) {
        try {
            app.addPackage(info.activityInfo.packageName,
                    info.activityInfo.applicationInfo.versionCode, mService.mProcessStats);
            //处理当前广播
            processCurBroadcastLocked(r, app, skipOomAdj);
            return;
        } catch (RemoteException e) {
            Slog.w(TAG, "processNextBroadcastLocked Exception when sending broadcast to "
                  + r.curComponent, e);
        } catch (RuntimeException e) {
            logBroadcastReceiverDiscardLocked(r);
            finishReceiverLocked(r, r.resultCode, r.resultData,
                    r.resultExtras, r.resultAbort, false);
            scheduleBroadcastsLocked();
            r.state = BroadcastRecord.IDLE;
            return;
        }
    }
    //走这里是进程没有启动
    //启动新的进程ActivityManagerService.startProcessLocked()
    if ((r.curApp=mService.startProcessLocked(targetProcess,
            info.activityInfo.applicationInfo, true,
            r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND,
            "broadcast", r.curComponent,
            (r.intent.getFlags()&Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0, false, false))
                    == null) {
        //启动失败就走这里,否则不进来
        //略
        return;
    }

    //mPendingBroadcast赋值,记录BroadcastRecord
    mPendingBroadcast = r;
    mPendingBroadcastRecvIndex = recIdx;
}

由于开头说了,我们是第一次发送静态广播,也即是App=null

也就是执行ActivityManagerService.startProcessLocked()流程。

进程的启动

这部分跟《startActivity之进程启动》重合了,都是一样的流程。最后启动ActivityThread.main()。

而ActivityThread.main()到ActivityManagerService.attachApplicationLocked()这部分代码跟《startService()源码分析》和《startActivity源码分析2》基本一致。

因此,我们直接跳过重复的部分,直接进入ActivityManagerService.attachApplicationLocked()

ActivityManagerService.java

attachApplicationLocked()
@GuardedBy("this")
private final boolean attachApplicationLocked(IApplicationThread thread,
        int pid, int callingUid, long startSeq) {

    //重复部分略,有需要的可看对应文章


    //启动Activity 
    if (normalMode) {
        try {
            //Activity
            if (mStackSupervisor.attachApplicationLocked(app)) {
                didSomething = true;
            }
        } catch (Exception e) {
            badApp = true;
        }
    }
    //启动服务
    if (!badApp) {
        try {
            //Service
            didSomething |= mServices.attachApplicationLocked(app, processName);
        } catch (Exception e) {
            badApp = true;
        }
    }
    //启动广播
    if (!badApp && isPendingBroadcastProcessLocked(pid)) {
        try {
            //Broadcasts
            didSomething |= sendPendingBroadcastsLocked(app);
        } catch (Exception e) {
            badApp = true;
        }
    }
    //略
    return true;
}

这里我们只关注广播的发送

if (!badApp && isPendingBroadcastProcessLocked(pid)) {
    try {
        didSomething |= sendPendingBroadcastsLocked(app);
    } catch (Exception e) {
        badApp = true;
    }
}
sendPendingBroadcastsLocked()
// The app just attached; send any pending broadcasts that it should receive
boolean sendPendingBroadcastsLocked(ProcessRecord app) {
    boolean didSomething = false;
    for (BroadcastQueue queue : mBroadcastQueues) {
        //根据app进行查询前台和后台的BroadcastQueue
        //看BroadcastQueue.sendPendingBroadcastsLocked()
        didSomething |= queue.sendPendingBroadcastsLocked(app);
    }
    return didSomething;
}

mBroadcastQueues包含两个变量,在ActivityManagerService()中初始化

public ActivityManagerService(Context systemContext) {
    //略
    //BROADCAST_FG_TIMEOUT 前台 10s
    mFgBroadcastQueue = new BroadcastQueue(this, mHandler,
        "foreground", BROADCAST_FG_TIMEOUT, false);
    //BROADCAST_BG_TIMEOUT 后台 60s
    mBgBroadcastQueue = new BroadcastQueue(this, mHandler,
            "background", BROADCAST_BG_TIMEOUT, true);
    mBroadcastQueues[0] = mFgBroadcastQueue;
    mBroadcastQueues[1] = mBgBroadcastQueue;
    //略
}

BroadcastQueue.java

sendPendingBroadcastsLocked()
public boolean sendPendingBroadcastsLocked(ProcessRecord app) {
    boolean didSomething = false;
    final BroadcastRecord br = mPendingBroadcast;
    //之前存储的是后台BroadcastQueue,(之前备注了60s超时ANR处)
    if (br != null && br.curApp.pid > 0 && br.curApp.pid == app.pid) {
        if (br.curApp != app) {
            return false;
        }
        try {
            mPendingBroadcast = null;
            //满足条件后就执行这个,处理广播
            processCurBroadcastLocked(br, app, false);
            didSomething = true;
        } catch (Exception e) {
            logBroadcastReceiverDiscardLocked(br);
            finishReceiverLocked(br, br.resultCode, br.resultData,
                    br.resultExtras, br.resultAbort, false);
            scheduleBroadcastsLocked();
            br.state = BroadcastRecord.IDLE;
            throw new RuntimeException(e.getMessage());
        }
    }
    return didSomething;
}
processCurBroadcastLocked()

这里也不是真正处理广播的,最终又调用了ActivityThread0

private final void processCurBroadcastLocked(BroadcastRecord r,
        ProcessRecord app, boolean skipOomAdj) throws RemoteException {
    if (app.thread == null) {
        throw new RemoteException();
    }
    if (app.inFullBackup) {
        skipReceiverLocked(r);
        return;
    }
    r.receiver = app.thread.asBinder();
    r.curApp = app;
    app.curReceivers.add(r);
    app.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER);
    mService.updateLruProcessLocked(app, false, null);
    if (!skipOomAdj) {
        mService.updateOomAdjLocked();
    }
    r.intent.setComponent(r.curComponent);
    boolean started = false;
    try {
        mService.notifyPackageUse(r.intent.getComponent().getPackageName(),
                                  PackageManager.NOTIFY_PACKAGE_USE_BROADCAST_RECEIVER);
        //看这里ActivityThread.ApplicationThread.scheduleReceiver
        app.thread.scheduleReceiver(new Intent(r.intent), r.curReceiver,
                mService.compatibilityInfoForPackageLocked(r.curReceiver.applicationInfo),
                r.resultCode, r.resultData, r.resultExtras, r.ordered, r.userId,
                app.repProcState);
        started = true;
    } finally {
        if (!started) {
            r.receiver = null;
            r.curApp = null;
            app.curReceivers.remove(r);
        }
    }
}

ActivityThread.java

是内部ApplicationThread的方法

public final void scheduleReceiver(Intent intent, ActivityInfo info,
        CompatibilityInfo compatInfo, int resultCode, String data, Bundle extras,
        boolean sync, int sendingUser, int processState) {
    updateProcessState(processState, false);
    ReceiverData r = new ReceiverData(intent, resultCode, data, extras,
            sync, false, mAppThread.asBinder(), sendingUser);
    r.info = info;
    r.compatInfo = compatInfo;
    //发送H.RECEIVER
    sendMessage(H.RECEIVER, r);
}
handleMessage()
public void handleMessage(Message msg) {
    //略
    case RECEIVER:
        handleReceiver((ReceiverData)msg.obj);
        break;
    //略
}
handleReceiver()

这里才真正发送广播给接收者:receiver.onReceive()

private void handleReceiver(ReceiverData data) {
    unscheduleGcIdler();
    String component = data.intent.getComponent().getClassName();
    LoadedApk packageInfo = getPackageInfoNoCheck(
            data.info.applicationInfo, data.compatInfo);
    IActivityManager mgr = ActivityManager.getService();
    Application app;
    BroadcastReceiver receiver;
    ContextImpl context;
    try {
        //获取Application,之前makeApplication过
        app = packageInfo.makeApplication(false, mInstrumentation);
        context = (ContextImpl) app.getBaseContext();
        if (data.info.splitName != null) {
            context = (ContextImpl) context.createContextForSplit(data.info.splitName);
        }
        java.lang.ClassLoader cl = context.getClassLoader();
        data.intent.setExtrasClassLoader(cl);
        data.intent.prepareToEnterProcess();
        data.setExtrasClassLoader(cl);
        //获取广播接收者
        receiver = packageInfo.getAppFactory()
                .instantiateReceiver(cl, data.info.name, data.intent);
    } catch (Exception e) {
        data.sendFinished(mgr);
        throw new RuntimeException(
            "Unable to instantiate receiver " + component
            + ": " + e.toString(), e);
    }

    try {
        sCurrentBroadcastIntent.set(data.intent);
        receiver.setPendingResult(data);
        //调用onReceive()
        receiver.onReceive(context.getReceiverRestrictedContext(),
                data.intent);
    } catch (Exception e) {
        data.sendFinished(mgr);
        if (!mInstrumentation.onException(receiver, e)) {
            throw new RuntimeException(
                "Unable to start receiver " + component
                + ": " + e.toString(), e);
        }
    } finally {
        sCurrentBroadcastIntent.set(null);
    }

    if (receiver.getPendingResult() != null) {
        data.finish();
    }
}

至此,静态广播发送分析就结束了。

参考文章

直接本站搜索关键字即可。

 历史上的今天

  1. 2021: ListView的item中当文本出现阿拉伯语时会显示怪异(0条评论)
版权声明 1、 本站名称: 91易搜
2、 本站网址: 91es.com3xcn.com
3、 本站内容: 部分来源于网络,仅供学习和参考,若侵权请留言
3、 本站申明: 个人流水账日记,内容并不保证有效

暂无评论

暂无评论...

随机推荐

舒婷:人心的法则

为一朵花而死去是值得的冷漠的车轮粗暴的靴底使春天的彩虹在所有眸子里黯然失色既不能阻挡又无处诉说那么,为抗议而死去是值得的 为一句话而沉默是值得的远胜于大潮雪崩似地跌落这句话被嘴唇紧紧封锁汲取一生全部诚实与勇气这句话,不能说那么,为不背叛而沉默是值得的...

徐志摩:再别康桥

再别康桥轻轻的我走了,正如我轻轻的来;我轻轻的招手,作别西天的云彩。 那河畔的金柳,是夕阳中的新娘;波光里的艳影,在我的心头荡漾。 软泥上的青荇,油油的在水底招摇;在康河的柔波里,我甘心做一条水草! 那榆荫下的一潭,不是清泉,是天上虹...

JNI静态注册

前言之前其实写过,代码不见了,为了走一下流程,重新简单的写了一个。PS:设计NDK环境配置这里不介绍哈正文静态注册先由Java得到本地方法的声明,然后再通过JNI实现该声明方法。优点: 理解和使用方式简单, 属于傻瓜式操作, 使用相关工具按流程操作就行, 出错率低缺点: 当需...

Android应用被杀的日志分析记录

前言最近Android项目中出现一个问题,应用开机源记忆拉起,突然被强制性退了(看Activity的生命周期),搞得我一时懵逼了。日志有这几个打印BufferQueueConsumer( 419): [Splash Screen com.la.media#0](this:0x7ccbe90...

刘瑜:适应孤独,就像适应一种残疾

前两天有个网友给我写信,问我如何克服寂寞。她跟我刚来美国时一样,英文不够好,朋友少,一个人等着天亮,一个人等着天黑。“每天学校、家、图书馆、健身房,几点一线。”我说我没什么好招儿,因为我从来就没有克服过这个问题。这些年来我学会的,就是适应它。正如有人所言:“适应孤独,就像适应一种残疾。”我觉得,...

[转]Jhuster:Android 音视频开发入门指南

本文转载于Jhuster的《Android 音视频开发入门指南 [直通车]》,只为了自己方便查询,决定系统的学习多媒体开发,感谢大牛的共享。原文如下:最近收到很多网友通过邮件或者留言说想学习音视频开发,该如何入门,我今天专门写篇文章统一回复下吧。音视频这块,目前的确没有比较系统的教程或者书籍,...